2bab61209c
Go-Modul in go.mod und allen 45 Go-Dateien umbenannt.
160 lines
4.7 KiB
Go
160 lines
4.7 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"archivmail/internal/audit"
|
|
"archivmail/internal/storage"
|
|
"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})
|
|
}
|