package api import ( "encoding/json" "fmt" "net/http" "strconv" "github.com/archivmail/internal/audit" "github.com/archivmail/internal/storage" "github.com/archivmail/internal/tenantstore" ) // handleGetTenantUsage returns current quota config and usage for a tenant. // GET /api/admin/tenant/{id}/quota — superadmin only (PROJ-29). // GET /api/admin/tenants/{id}/usage — superadmin only (PROJ-29, spec-conformant alias). func (s *Server) handleGetTenantUsage(w http.ResponseWriter, r *http.Request) { tenantID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } quota, err := s.tenantStore.GetQuota(r.Context(), tenantID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } usage, err := s.tenantStore.GetUsage(r.Context(), tenantID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } // Compute soft-limit warnings (≥80% = soft, ≥100% = hard). type warnings struct { StoragePct *float64 `json:"storage_pct"` UsersPct *float64 `json:"users_pct"` EmailsPct *float64 `json:"emails_pct"` SoftLimitReached bool `json:"soft_limit_reached"` } w80 := warnings{} softReached := false if quota.MaxStorageBytes != nil && *quota.MaxStorageBytes > 0 { pct := float64(usage.StorageBytes) / float64(*quota.MaxStorageBytes) * 100 w80.StoragePct = &pct if pct >= 80 { softReached = true } } if quota.MaxUsers != nil && *quota.MaxUsers > 0 { pct := float64(usage.UserCount) / float64(*quota.MaxUsers) * 100 w80.UsersPct = &pct if pct >= 80 { softReached = true } } if quota.MaxEmails != nil && *quota.MaxEmails > 0 { pct := float64(usage.EmailCount) / float64(*quota.MaxEmails) * 100 w80.EmailsPct = &pct if pct >= 80 { softReached = true } } w80.SoftLimitReached = softReached writeJSON(w, http.StatusOK, map[string]interface{}{ "storage_bytes": usage.StorageBytes, "user_count": usage.UserCount, "email_count": usage.EmailCount, "quotas": map[string]interface{}{ "max_storage_bytes": quota.MaxStorageBytes, "max_users": quota.MaxUsers, "max_emails": quota.MaxEmails, }, "warnings": w80, }) } // handleSetTenantQuota sets quota limits for a tenant. // PUT /api/admin/tenant/{id}/quota — superadmin only (PROJ-29). func (s *Server) handleSetTenantQuota(w http.ResponseWriter, r *http.Request) { tenantID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } var body tenantstore.TenantQuota if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "invalid body") return } if err := s.tenantStore.SetQuota(r.Context(), tenantID, body); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } // Invalidate quota cache so the new limit takes effect immediately storage.InvalidateQuotaCache(tenantID) sess := sessionFromCtx(r.Context()) if s.audlog != nil { s.audlog.Log(audit.Entry{ EventType: "tenant_quota_changed", Username: sess.Username, IPAddress: s.remoteIP(r), Success: true, Detail: fmt.Sprintf("tenant_id=%d", tenantID), }) } writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true}) } // handleGetAllTenantUsage returns quota and usage for all tenants. // GET /api/admin/quotas — superadmin only (PROJ-29). func (s *Server) handleGetAllTenantUsage(w http.ResponseWriter, r *http.Request) { tenants, err := s.tenantStore.List(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } type tenantWithUsage struct { ID int64 `json:"id"` Name string `json:"name"` Slug string `json:"slug"` MaxStorageBytes *int64 `json:"max_storage_bytes"` MaxUsers *int `json:"max_users"` MaxEmails *int64 `json:"max_emails"` StorageBytes int64 `json:"storage_bytes"` UserCount int64 `json:"user_count"` EmailCount int64 `json:"email_count"` } result := make([]tenantWithUsage, 0, len(tenants)) for _, t := range tenants { usage, err := s.tenantStore.GetUsage(r.Context(), t.ID) if err != nil { usage = &tenantstore.TenantUsage{} } result = append(result, tenantWithUsage{ ID: t.ID, Name: t.Name, Slug: t.Slug, MaxStorageBytes: t.MaxStorageBytes, MaxUsers: t.MaxUsers, MaxEmails: t.MaxEmails, StorageBytes: usage.StorageBytes, UserCount: usage.UserCount, EmailCount: usage.EmailCount, }) } writeJSON(w, http.StatusOK, map[string]interface{}{"tenants": result}) }