Files
archivmail/internal/tenantstore/quota.go
T
sysops 4ef5897e68 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>
2026-04-04 01:27:59 +02:00

77 lines
2.4 KiB
Go

package tenantstore
import (
"context"
"fmt"
)
// TenantQuota holds the configurable limits for a tenant (nil = unlimited).
type TenantQuota struct {
MaxStorageBytes *int64 `json:"max_storage_bytes"`
MaxUsers *int `json:"max_users"`
MaxEmails *int64 `json:"max_emails"`
}
// TenantUsage holds the current resource usage for a tenant.
type TenantUsage struct {
StorageBytes int64 `json:"storage_bytes"`
UserCount int64 `json:"user_count"`
EmailCount int64 `json:"email_count"`
}
// SetQuota updates the quota fields for a tenant.
// Pass nil to remove a limit (unlimited).
func (s *Store) SetQuota(ctx context.Context, tenantID int64, q TenantQuota) error {
tag, err := s.pool.Exec(ctx,
`UPDATE tenants SET max_storage_bytes=$1, max_users=$2, max_emails=$3 WHERE id=$4`,
q.MaxStorageBytes, q.MaxUsers, q.MaxEmails, tenantID,
)
if err != nil {
return fmt.Errorf("tenantstore: set quota: %w", err)
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("tenantstore: tenant %d not found", tenantID)
}
return nil
}
// GetQuota returns the quota configuration for a tenant.
func (s *Store) GetQuota(ctx context.Context, tenantID int64) (TenantQuota, error) {
var q TenantQuota
err := s.pool.QueryRow(ctx,
`SELECT max_storage_bytes, max_users, max_emails FROM tenants WHERE id=$1`, tenantID,
).Scan(&q.MaxStorageBytes, &q.MaxUsers, &q.MaxEmails)
if err != nil {
return q, fmt.Errorf("tenantstore: get quota: %w", err)
}
return q, nil
}
// 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 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(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)
}
// User count from users table
err = s.pool.QueryRow(ctx,
`SELECT COUNT(*) FROM users WHERE tenant_id = $1`, tenantID,
).Scan(&u.UserCount)
if err != nil {
return nil, fmt.Errorf("tenantstore: get usage users: %w", err)
}
return u, nil
}