From 9db433c4c1b1c068a450cf70a4ad395e2da59af8 Mon Sep 17 00:00:00 2001 From: sysops Date: Fri, 20 Mar 2026 02:01:12 +0100 Subject: [PATCH] fix: Mandantenverwaltung LDAP-Status und Nutzer-Listing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tenantstore.List(): LEFT JOIN tenant_ldap hinzugefügt — ldap_enabled + ldap_url werden jetzt im GET /api/tenants Response mitgeliefert - Tenant-Struct: Felder LDAPEnabled *bool + LDAPURL string ergänzt - Neuer Endpunkt GET /api/tenants/{id}/users → listet Nutzer eines Mandanten - api.ts: getTenantUsers() Funktion + tenant_id Feld im User Interface - Admin-Panel: "Nutzer"-Button im Mandanten-Tab öffnet Dialog mit Nutzerliste Co-Authored-By: Claude Sonnet 4.6 --- internal/api/ldap_tenants.go | 18 ++++++++++ internal/tenantstore/store.go | 17 ++++++---- src/app/admin/page.tsx | 62 +++++++++++++++++++++++++++++++++++ src/lib/api.ts | 5 +++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/internal/api/ldap_tenants.go b/internal/api/ldap_tenants.go index ad8e167..6e38967 100644 --- a/internal/api/ldap_tenants.go +++ b/internal/api/ldap_tenants.go @@ -40,6 +40,7 @@ func (s *Server) SetTenants(store *tenantstore.Store) { s.mux.HandleFunc("GET /api/tenants/{id}/domains", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleListTenantDomains))) s.mux.HandleFunc("POST /api/tenants/{id}/domains", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleAddTenantDomain))) s.mux.HandleFunc("DELETE /api/tenants/{id}/domains/{did}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleRemoveTenantDomain))) + s.mux.HandleFunc("GET /api/tenants/{id}/users", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleListTenantUsers))) } // ── LDAP handlers ──────────────────────────────────────────────────────────── @@ -385,6 +386,23 @@ func (s *Server) handleRemoveTenantDomain(w http.ResponseWriter, r *http.Request w.WriteHeader(http.StatusNoContent) } +func (s *Server) handleListTenantUsers(w http.ResponseWriter, r *http.Request) { + tenantID, err := parseTenantID(r) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid tenant id") + return + } + users, err := s.users.ListByTenant(r.Context(), tenantID) + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to list tenant users") + return + } + if users == nil { + users = []*userstore.User{} + } + writeJSON(w, http.StatusOK, users) +} + // ── PROJ-23: Per-Tenant LDAP handlers (Phase B) ───────────────────────────── // SetTenantLDAP wires the per-tenant LDAP config store into the API server and diff --git a/internal/tenantstore/store.go b/internal/tenantstore/store.go index 7a3249b..75c67a4 100644 --- a/internal/tenantstore/store.go +++ b/internal/tenantstore/store.go @@ -20,8 +20,10 @@ type Tenant struct { Active bool `json:"active"` CreatedAt time.Time `json:"created_at"` // Computed fields populated by List. - DomainCount int `json:"domain_count,omitempty"` - UserCount int `json:"user_count,omitempty"` + DomainCount int `json:"domain_count,omitempty"` + UserCount int `json:"user_count,omitempty"` + LDAPEnabled *bool `json:"ldap_enabled,omitempty"` + LDAPURL string `json:"ldap_url,omitempty"` } // TenantDomain is an e-mail domain assigned to a tenant. @@ -111,16 +113,19 @@ func (s *Store) Create(ctx context.Context, name, slug string) (*Tenant, error) return s.Get(ctx, id) } -// List returns all tenants with computed domain_count and user_count. +// List returns all tenants with computed domain_count, user_count, and LDAP status. func (s *Store) List(ctx context.Context) ([]Tenant, error) { rows, err := s.pool.Query(ctx, ` SELECT t.id, t.name, t.slug, t.active, t.created_at, COUNT(DISTINCT td.id) AS domain_count, - COUNT(DISTINCT u.id) AS user_count + COUNT(DISTINCT u.id) AS user_count, + tl.enabled AS ldap_enabled, + tl.url AS ldap_url FROM tenants t LEFT JOIN tenant_domains td ON td.tenant_id = t.id LEFT JOIN users u ON u.tenant_id = t.id - GROUP BY t.id + LEFT JOIN tenant_ldap tl ON tl.tenant_id = t.id + GROUP BY t.id, tl.enabled, tl.url ORDER BY t.id `) if err != nil { @@ -131,7 +136,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); err != nil { + if err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Active, &t.CreatedAt, &t.DomainCount, &t.UserCount, &t.LDAPEnabled, &t.LDAPURL); err != nil { return nil, fmt.Errorf("tenantstore: scan: %w", err) } tenants = append(tenants, t) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 3a4be3a..2fd3f6b 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -24,6 +24,7 @@ import { deleteLDAPConfig, testLDAPConfig, getTenants, + getTenantUsers, createTenant, updateTenant, deleteTenant, @@ -238,6 +239,12 @@ export default function AdminPage() { // Superadmin: tenant LDAP dialog const [tenantLdapDialogId, setTenantLdapDialogId] = useState(null); + // Tenant users dialog + const [tenantUsersDialogId, setTenantUsersDialogId] = useState(null); + const [tenantUsersDialogName, setTenantUsersDialogName] = useState(""); + const [tenantUsers, setTenantUsers] = useState([]); + const [tenantUsersLoading, setTenantUsersLoading] = useState(false); + // Labels state const [adminLabels, setAdminLabels] = useState([]); const [adminLabelsLoading, setAdminLabelsLoading] = useState(false); @@ -747,6 +754,18 @@ export default function AdminPage() { finally { setDomainsLoading(false); } } + async function openUsersDialog(t: Tenant) { + setTenantUsersDialogId(t.id); + setTenantUsersDialogName(t.name); + setTenantUsersLoading(true); + setTenantUsers([]); + try { + const users = await getTenantUsers(t.id); + setTenantUsers(users || []); + } catch { /* ignore */ } + finally { setTenantUsersLoading(false); } + } + async function handleAddDomain() { if (!domainDialogTenant || !newDomain) return; setAddDomainLoading(true); @@ -2477,6 +2496,9 @@ export default function AdminPage() { + @@ -2564,6 +2586,46 @@ export default function AdminPage() { + {/* Tenant users dialog */} + { if (!open) setTenantUsersDialogId(null); }}> + + + Nutzer: {tenantUsersDialogName} + Dem Mandanten zugewiesene Benutzerkonten. + + {tenantUsersLoading ? ( + + ) : tenantUsers.length === 0 ? ( +

Keine Benutzer diesem Mandanten zugewiesen.

+ ) : ( + + + + Benutzername + E-Mail + Rolle + Status + + + + {tenantUsers.map((u) => ( + + {u.username} + {u.email} + {u.role} + + + {u.active ? "Aktiv" : "Inaktiv"} + + + + ))} + +
+ )} +
+
+ {/* Tenant LDAP dialog (superadmin) */} {tenantLdapDialogId !== null && ( { return request("/api/tenants"); } +export async function getTenantUsers(tenantId: number): Promise { + return request(`/api/tenants/${tenantId}/users`); +} + export async function createTenant(name: string, slug: string): Promise { return request("/api/tenants", { method: "POST",