package api import ( "encoding/json" "fmt" "net/http" "strconv" "archivmail/internal/audit" "archivmail/internal/tenantstore" "archivmail/internal/userstore" ) // ── Tenant handlers ────────────────────────────────────────────────────────── func (s *Server) handleListTenants(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } tenants, err := s.tenantStore.List(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list tenants") return } writeJSON(w, http.StatusOK, tenants) } func (s *Server) handleCreateTenant(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } var req struct { Name string `json:"name"` Slug string `json:"slug"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" || req.Slug == "" { writeError(w, http.StatusBadRequest, "name and slug are required") return } tenant, err := s.tenantStore.Create(r.Context(), req.Name, req.Slug) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create tenant") return } // Create default users for the new tenant. type defaultUserCreds struct { Username string `json:"username"` Password string `json:"password"` Role string `json:"role"` } type createTenantResponse struct { *tenantstore.Tenant DefaultUsers []defaultUserCreds `json:"default_users"` } resp := createTenantResponse{Tenant: tenant} for _, spec := range []struct { suffix string role string }{ {suffix: "admin", role: userstore.RoleDomainAdmin}, {suffix: "auditor", role: userstore.RoleAuditor}, } { pw, pwErr := tenantRandomPassword() if pwErr != nil { writeError(w, http.StatusInternalServerError, "failed to generate password") return } username := req.Slug + "-" + spec.suffix email := fmt.Sprintf("%s@%s.local", username, req.Slug) u, uErr := s.users.Create(userstore.CreateUserRequest{ Username: username, Email: email, Password: pw, Role: spec.role, TenantID: &tenant.ID, }) if uErr != nil { writeError(w, http.StatusInternalServerError, "failed to create default user") return } resp.DefaultUsers = append(resp.DefaultUsers, defaultUserCreds{ Username: u.Username, Password: pw, Role: spec.role, }) } sess := sessionFromCtx(r.Context()) s.audlog.Log(audit.Entry{ EventType: "tenant_created", Username: sess.Username, TenantID: sess.TenantID, IPAddress: s.remoteIP(r), Success: true, Detail: "Mandant erstellt: " + req.Name, }) writeJSON(w, http.StatusCreated, resp) } func (s *Server) handleGetTenant(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } id, err := parseTenantID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } // Defense-in-depth tenant scope check (PROJ-63): no-op for global admins. if !tenantAccessAllowed(sessionFromCtx(r.Context()), &id) { writeError(w, http.StatusForbidden, "access denied") return } tenant, err := s.tenantStore.Get(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, "tenant not found") return } writeJSON(w, http.StatusOK, tenant) } func (s *Server) handleUpdateTenant(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } id, err := parseTenantID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } // Defense-in-depth tenant scope check (PROJ-63): no-op for global admins. if !tenantAccessAllowed(sessionFromCtx(r.Context()), &id) { writeError(w, http.StatusForbidden, "access denied") return } var req struct { Name string `json:"name"` Active *bool `json:"active"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } // Load existing to keep unset fields existing, err := s.tenantStore.Get(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, "tenant not found") return } name := existing.Name active := existing.Active if req.Name != "" { name = req.Name } if req.Active != nil { active = *req.Active } tenant, err := s.tenantStore.Update(r.Context(), id, name, active) if err != nil { writeError(w, http.StatusInternalServerError, "failed to update tenant") return } writeJSON(w, http.StatusOK, tenant) } func (s *Server) handleDeleteTenant(w http.ResponseWriter, r *http.Request) { if s.tenantStore == nil { writeError(w, http.StatusServiceUnavailable, "tenant store not available") return } id, err := parseTenantID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid tenant id") return } sess := sessionFromCtx(r.Context()) // Defense-in-depth tenant scope check (PROJ-63): no-op for global admins. if !tenantAccessAllowed(sess, &id) { writeError(w, http.StatusForbidden, "access denied") return } if err := s.tenantStore.Delete(r.Context(), id); err != nil { writeError(w, http.StatusInternalServerError, "failed to delete tenant") return } s.audlog.Log(audit.Entry{ EventType: "tenant_deleted", Username: sess.Username, TenantID: sess.TenantID, IPAddress: s.remoteIP(r), Success: true, Detail: "Mandant gelöscht: " + strconv.FormatInt(id, 10), }) 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 } // Defense-in-depth tenant scope check (PROJ-63): no-op for global admins. if !tenantAccessAllowed(sessionFromCtx(r.Context()), &tenantID) { writeError(w, http.StatusForbidden, "access denied") 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) }