feat(PROJ-29): Tenant-Quotas & Usage-Limits
- DB: max_storage_bytes, max_users, max_emails per Tenant (NULL = unlimited)
- storage/quota.go: CheckQuota() mit 60s-Cache, ErrQuotaExceeded
- Save() prüft Quota vor dem Schreiben — Ablehnung bei Hard-Limit
- tenantstore/quota.go: SetQuota(), GetQuota(), GetUsage()
- API: GET/PUT /api/admin/tenant/{id}/quota, GET /api/admin/quotas
- QuotaTab: Usage-Balken (Speicher/Nutzer/Mails), Edit-Dialog, Warnung ab 80%
- InvalidateQuotaCache() nach Quota-Änderung für sofortige Wirkung
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,10 @@ type Tenant struct {
|
||||
LDAPURL string `json:"ldap_url,omitempty"`
|
||||
HasLogo bool `json:"has_logo,omitempty"`
|
||||
RetentionDays int `json:"retention_days"` // 0 = use global config
|
||||
// Quota fields (PROJ-29) — nil = unlimited
|
||||
MaxStorageBytes *int64 `json:"max_storage_bytes,omitempty"`
|
||||
MaxUsers *int `json:"max_users,omitempty"`
|
||||
MaxEmails *int64 `json:"max_emails,omitempty"`
|
||||
}
|
||||
|
||||
// TenantDomain is an e-mail domain assigned to a tenant.
|
||||
@@ -77,6 +81,9 @@ ALTER TABLE tenants ADD COLUMN IF NOT EXISTS logo_data BYTEA;
|
||||
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS logo_content_type VARCHAR(100) NOT NULL DEFAULT '';
|
||||
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS imap_mode TEXT NOT NULL DEFAULT 'personal';
|
||||
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS retention_days INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS max_storage_bytes BIGINT;
|
||||
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS max_users INT;
|
||||
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS max_emails BIGINT;
|
||||
`
|
||||
|
||||
// New connects to PostgreSQL and initialises the tenant schema.
|
||||
@@ -128,7 +135,10 @@ func (s *Store) List(ctx context.Context) ([]Tenant, error) {
|
||||
tl.enabled AS ldap_enabled,
|
||||
tl.url AS ldap_url,
|
||||
(t.logo_data IS NOT NULL) AS has_logo,
|
||||
t.retention_days
|
||||
t.retention_days,
|
||||
t.max_storage_bytes,
|
||||
t.max_users,
|
||||
t.max_emails
|
||||
FROM tenants t
|
||||
LEFT JOIN tenant_domains td ON td.tenant_id = t.id
|
||||
LEFT JOIN users u ON u.tenant_id = t.id
|
||||
@@ -144,7 +154,7 @@ func (s *Store) List(ctx context.Context) ([]Tenant, error) {
|
||||
var tenants []Tenant
|
||||
for rows.Next() {
|
||||
var t Tenant
|
||||
if err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Active, &t.CreatedAt, &t.DomainCount, &t.UserCount, &t.LDAPEnabled, &t.LDAPURL, &t.HasLogo, &t.RetentionDays); err != nil {
|
||||
if err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Active, &t.CreatedAt, &t.DomainCount, &t.UserCount, &t.LDAPEnabled, &t.LDAPURL, &t.HasLogo, &t.RetentionDays, &t.MaxStorageBytes, &t.MaxUsers, &t.MaxEmails); err != nil {
|
||||
return nil, fmt.Errorf("tenantstore: scan: %w", err)
|
||||
}
|
||||
tenants = append(tenants, t)
|
||||
|
||||
Reference in New Issue
Block a user