feat(PROJ-21): Phase 2+3+5+8 Multi-Tenancy + PROJ-2 EML/MBOX Upload

Phase 2a: userstore domain_admin/superadmin Rollen, User.TenantID,
          ListByTenant, UpsertLDAPUser mit tenantID
Phase 2b: storage.Save() mit tenantID *int64, email_refs Tabelle,
          GetTenantForMail, GetAllIDsByTenant, StatsByTenant
Phase 2c: JWT-Claims tenant_id/tenant_slug, Session.TenantID,
          Login Domain-Erkennung via E-Mail-Domain
Phase 3:  tenantMiddleware, Handler-Filterung (Users, Mail, Stats)
Phase 5:  SMTP Domain-Routing via DomainToTenantFunc Callback,
          config smtp.tenant_routing + default_tenant_id
Phase 8:  archivmail migrate-tenants Subkommando
PROJ-2:   Upload-Seite /admin/upload mit DropZone + Progress-Polling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-17 21:03:40 +01:00
parent 5250ffcd52
commit 479c27e5a8
16 changed files with 966 additions and 158 deletions
+40 -15
View File
@@ -16,10 +16,12 @@ import (
// Session holds the claims extracted from a validated JWT.
type Session struct {
UserID int64
Username string
Role string
JTI string // unique JWT ID
UserID int64
Username string
Role string
JTI string // unique JWT ID
TenantID *int64
TenantSlug string
}
// Manager handles login, token issuance, validation, and logout.
@@ -83,7 +85,7 @@ func (m *Manager) Login(username, password string) (string, *userstore.User, err
email = username + "@ldap.local"
}
ldapUser, upsertErr := m.store.UpsertLDAPUser(username, email, role)
ldapUser, upsertErr := m.store.UpsertLDAPUser(username, email, role, nil)
if upsertErr == nil {
return m.issueToken(ldapUser)
}
@@ -98,13 +100,20 @@ func (m *Manager) Login(username, password string) (string, *userstore.User, err
func (m *Manager) issueToken(user *userstore.User) (string, *userstore.User, error) {
jti := generateJTI()
now := time.Now()
var tenantIDVal int64
if user.TenantID != nil {
tenantIDVal = *user.TenantID
}
claims := jwt.MapClaims{
"sub": user.Username,
"role": user.Role,
"uid": user.ID,
"jti": jti,
"iat": now.Unix(),
"exp": now.Add(8 * time.Hour).Unix(),
"sub": user.Username,
"role": user.Role,
"uid": user.ID,
"jti": jti,
"iat": now.Unix(),
"exp": now.Add(8 * time.Hour).Unix(),
"tenant_id": tenantIDVal,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@@ -156,11 +165,25 @@ func (m *Manager) ValidateToken(tokenStr string) (*Session, error) {
userID = v
}
var tenantID *int64
switch v := claims["tenant_id"].(type) {
case float64:
if v != 0 {
id := int64(v)
tenantID = &id
}
case int64:
if v != 0 {
tenantID = &v
}
}
return &Session{
UserID: userID,
Username: username,
Role: role,
JTI: jti,
TenantID: tenantID,
}, nil
}
@@ -196,12 +219,14 @@ func (m *Manager) Logout(tokenStr string) error {
}
// HasRole returns true when userRole satisfies the required role level.
// Hierarchy: admin > auditor > user
// Hierarchy: superadmin > admin > domain_admin > auditor > user
func HasRole(userRole, required string) bool {
levels := map[string]int{
userstore.RoleUser: 1,
userstore.RoleAuditor: 2,
userstore.RoleAdmin: 3,
userstore.RoleUser: 1,
userstore.RoleAuditor: 2,
userstore.RoleDomainAdmin: 3,
userstore.RoleAdmin: 4,
userstore.RoleSuperAdmin: 5,
}
return levels[userRole] >= levels[required]
}