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:
+40
-15
@@ -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]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user