Gitfed
bastien-mrq/gitfed / cmd / gitfed-web / handlers_admin.go
package main

import (
	"bytes"
	"html/template"
	"net/http"

	"git.neuromancer.ovh/bastien-mrq/gitfed/internal/i18n"
	"git.neuromancer.ovh/bastien-mrq/gitfed/internal/store"
)

var adminIndexTpl = newTpl("admin-index", `
<div class="gf-page-head"><h1>{{t .Lang "admin.title"}}</h1></div>

<div class="gf-admin-grid">
  <div class="gf-admin-card">
    <div class="top"><span class="l">{{t .Lang "admin.card_users"}}</span></div>
    <div class="n">{{.UserCount}}</div>
    <p>{{t .Lang "admin.card_users_sub" .AdminCount .RegularCount}}</p>
    <a class="go" href="/admin/users">{{t .Lang "admin.card_users_link"}} →</a>
  </div>
  <div class="gf-admin-card">
    <div class="top">
      <span class="l">{{t .Lang "admin.card_trust"}}</span>
      {{if .PendingCount}}<span class="badge pending">{{.PendingCount}} {{t .Lang "admin.card_trust_pending"}}</span>{{end}}
    </div>
    <div class="n">{{.TrustCount}}</div>
    <p>{{t .Lang "admin.card_trust_sub"}}</p>
    <a class="go" href="/admin/trust">{{t .Lang "admin.card_trust_link"}} →</a>
  </div>
  <div class="gf-admin-card">
    <div class="top"><span class="l">{{t .Lang "admin.card_audit"}}</span></div>
    <div class="n">{{.AuditCount}}</div>
    <p>{{t .Lang "admin.card_audit_sub"}}</p>
    <a class="go" href="/admin/audit">{{t .Lang "admin.card_audit_link"}} →</a>
  </div>
  <div class="gf-admin-card">
    <div class="top"><span class="l">{{t .Lang "admin.card_repos"}}</span></div>
    <div class="n">{{humanBytes .RepoBytes}}</div>
    <p>{{t .Lang "admin.card_repos_sub" .RepoCount}}</p>
    <a class="go" href="/admin/repos">{{t .Lang "admin.card_repos_link"}} →</a>
  </div>
</div>

{{if .RecentAudit}}
<h3 style="font-size:0.95rem; margin-bottom:0.6rem;">{{t .Lang "admin.recent_activity"}}</h3>
<div class="gf-card gf-audit-mini">
{{range .RecentAudit}}
  <div class="gf-audit-row">
    <span class="t">{{.Time.Local.Format "15:04:05"}}</span>
    <span class="who">{{.Principal}} · {{.Action}}{{if .Repo}} · {{.Repo}}{{end}}</span>
    {{if .Allowed}}<span class="badge trusted">{{t $.Lang "admin.audit_allow"}}</span>{{else}}<span class="badge pending">{{t $.Lang "admin.audit_deny"}}</span>{{end}}
  </div>
{{end}}
</div>
{{end}}
`)

func (s *server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
	users, err := s.ops.ListUsers()
	if err != nil {
		s.serverError(w, r, err)
		return
	}
	adminCount := 0
	for _, u := range users {
		if u.IsAdmin {
			adminCount++
		}
	}

	trust, err := s.ops.ListTrustedCAs()
	if err != nil {
		s.serverError(w, r, err)
		return
	}
	pendingCount := 0
	for _, t := range trust {
		if t.Status == store.TrustPending {
			pendingCount++
		}
	}

	audit, err := s.ops.ListAudit(200)
	if err != nil {
		s.serverError(w, r, err)
		return
	}
	recent := audit
	if len(recent) > 5 {
		recent = recent[:5]
	}

	repos, err := s.ops.ListRepos()
	if err != nil {
		s.serverError(w, r, err)
		return
	}
	var repoBytes int64
	for _, repo := range repos {
		// Best-effort, same as the repos list page — a vanished repo
		// directory shouldn't take down the admin dashboard.
		if size, err := s.ops.RepoDiskUsage(repo.Name); err == nil {
			repoBytes += size
		}
	}

	lang := s.lang(r)
	var buf bytes.Buffer
	_ = adminIndexTpl.Execute(&buf, struct {
		UserCount, AdminCount, RegularCount int
		TrustCount, PendingCount            int
		AuditCount                          int
		RepoCount                           int
		RepoBytes                           int64
		Lang                                string
		RecentAudit                         []store.AuditEvent
	}{len(users), adminCount, len(users) - adminCount, len(trust), pendingCount, len(audit), len(repos), repoBytes, string(lang), recent})
	s.render(w, r, i18n.T(lang, "admin.title"), "admin", template.HTML(buf.String()))
}