feat(PROJ-29): Tenant-Quotas & Usage-Limits vollständig implementiert

- storage/quota.go: SQL-Bug gefixt (emails.size → size_bytes, email_refs JOIN)
- tenantstore/quota.go: GetUsage nutzt jetzt email_refs JOIN für korrekte Tenant-Isolation
- smtpd: ErrQuotaExceeded → SMTP 452 statt 554 (MTA-retry statt permanent reject)
- admin_handlers: handleCreateUser prüft max_users-Quota → HTTP 402 bei Überschreitung
- quota_handlers: handleGetTenantUsage gibt jetzt warnings-Feld mit soft-limit-Prozenten zurück
- server.go: spec-konforme Alias-Route GET /api/admin/tenants/{id}/usage registriert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-04-04 01:27:59 +02:00
parent 22cbfb5df6
commit 4ef5897e68
8 changed files with 87 additions and 12 deletions
+7 -4
View File
@@ -48,14 +48,17 @@ func (s *Store) GetQuota(ctx context.Context, tenantID int64) (TenantQuota, erro
}
// GetUsage returns the current resource usage for a tenant.
// Email count and storage bytes are aggregated via email_refs (tenant-aware dedup).
func (s *Store) GetUsage(ctx context.Context, tenantID int64) (*TenantUsage, error) {
u := &TenantUsage{}
// Email count and storage bytes from emails table
// Email count and storage bytes via email_refs JOIN emails (correct tenant isolation).
// size_bytes is the column name in the emails table (not size).
err := s.pool.QueryRow(ctx, `
SELECT COUNT(*), COALESCE(SUM(size), 0)
FROM emails
WHERE tenant_id = $1
SELECT COUNT(r.id), COALESCE(SUM(e.size_bytes), 0)
FROM email_refs r
JOIN emails e ON e.id = r.email_id
WHERE r.tenant_id = $1
`, tenantID).Scan(&u.EmailCount, &u.StorageBytes)
if err != nil {
return nil, fmt.Errorf("tenantstore: get usage emails: %w", err)