package handlers import ( "encoding/json" "net/http" "github.com/google/uuid" "mgit.msbls.de/m/KanzlAI-mGMT/internal/auth" "mgit.msbls.de/m/KanzlAI-mGMT/internal/services" ) type TenantHandler struct { svc *services.TenantService } func NewTenantHandler(svc *services.TenantService) *TenantHandler { return &TenantHandler{svc: svc} } // CreateTenant handles POST /api/tenants func (h *TenantHandler) CreateTenant(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var req struct { Name string `json:"name"` Slug string `json:"slug"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonError(w, "invalid request body", http.StatusBadRequest) return } if req.Name == "" || req.Slug == "" { jsonError(w, "name and slug are required", http.StatusBadRequest) return } tenant, err := h.svc.Create(r.Context(), userID, req.Name, req.Slug) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, tenant, http.StatusCreated) } // ListTenants handles GET /api/tenants func (h *TenantHandler) ListTenants(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } tenants, err := h.svc.ListForUser(r.Context(), userID) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, tenants, http.StatusOK) } // GetTenant handles GET /api/tenants/{id} func (h *TenantHandler) GetTenant(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } tenantID, err := uuid.Parse(r.PathValue("id")) if err != nil { jsonError(w, "invalid tenant ID", http.StatusBadRequest) return } // Verify user has access to this tenant role, err := h.svc.GetUserRole(r.Context(), userID, tenantID) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } if role == "" { jsonError(w, "not found", http.StatusNotFound) return } tenant, err := h.svc.GetByID(r.Context(), tenantID) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } if tenant == nil { jsonError(w, "not found", http.StatusNotFound) return } jsonResponse(w, tenant, http.StatusOK) } // InviteUser handles POST /api/tenants/{id}/invite func (h *TenantHandler) InviteUser(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } tenantID, err := uuid.Parse(r.PathValue("id")) if err != nil { jsonError(w, "invalid tenant ID", http.StatusBadRequest) return } // Only owners and partners can invite role, err := h.svc.GetUserRole(r.Context(), userID, tenantID) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } if role != "owner" && role != "partner" { jsonError(w, "only owners and partners can invite users", http.StatusForbidden) return } var req struct { Email string `json:"email"` Role string `json:"role"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonError(w, "invalid request body", http.StatusBadRequest) return } if req.Email == "" { jsonError(w, "email is required", http.StatusBadRequest) return } if req.Role == "" { req.Role = "associate" } if !auth.IsValidRole(req.Role) { jsonError(w, "invalid role", http.StatusBadRequest) return } // Non-owners cannot invite as owner if role != "owner" && req.Role == "owner" { jsonError(w, "only owners can invite as owner", http.StatusForbidden) return } ut, err := h.svc.InviteByEmail(r.Context(), tenantID, req.Email, req.Role) if err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } jsonResponse(w, ut, http.StatusCreated) } // RemoveMember handles DELETE /api/tenants/{id}/members/{uid} func (h *TenantHandler) RemoveMember(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } tenantID, err := uuid.Parse(r.PathValue("id")) if err != nil { jsonError(w, "invalid tenant ID", http.StatusBadRequest) return } memberID, err := uuid.Parse(r.PathValue("uid")) if err != nil { jsonError(w, "invalid member ID", http.StatusBadRequest) return } // Only owners and partners can remove members (or user removing themselves) role, err := h.svc.GetUserRole(r.Context(), userID, tenantID) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } if role != "owner" && role != "partner" && userID != memberID { jsonError(w, "insufficient permissions", http.StatusForbidden) return } if err := h.svc.RemoveMember(r.Context(), tenantID, memberID); err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } jsonResponse(w, map[string]string{"status": "removed"}, http.StatusOK) } // UpdateSettings handles PUT /api/tenants/{id}/settings func (h *TenantHandler) UpdateSettings(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } tenantID, err := uuid.Parse(r.PathValue("id")) if err != nil { jsonError(w, "invalid tenant ID", http.StatusBadRequest) return } // Only owners and partners can update settings role, err := h.svc.GetUserRole(r.Context(), userID, tenantID) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } if role != "owner" && role != "partner" { jsonError(w, "only owners and partners can update settings", http.StatusForbidden) return } var settings json.RawMessage if err := json.NewDecoder(r.Body).Decode(&settings); err != nil { jsonError(w, "invalid request body", http.StatusBadRequest) return } tenant, err := h.svc.UpdateSettings(r.Context(), tenantID, settings) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, tenant, http.StatusOK) } // ListMembers handles GET /api/tenants/{id}/members func (h *TenantHandler) ListMembers(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } tenantID, err := uuid.Parse(r.PathValue("id")) if err != nil { jsonError(w, "invalid tenant ID", http.StatusBadRequest) return } // Verify user has access role, err := h.svc.GetUserRole(r.Context(), userID, tenantID) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } if role == "" { jsonError(w, "not found", http.StatusNotFound) return } members, err := h.svc.ListMembers(r.Context(), tenantID) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonResponse(w, members, http.StatusOK) } // UpdateMemberRole handles PUT /api/tenants/{id}/members/{uid}/role func (h *TenantHandler) UpdateMemberRole(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } tenantID, err := uuid.Parse(r.PathValue("id")) if err != nil { jsonError(w, "invalid tenant ID", http.StatusBadRequest) return } memberID, err := uuid.Parse(r.PathValue("uid")) if err != nil { jsonError(w, "invalid member ID", http.StatusBadRequest) return } // Only owners and partners can change roles role, err := h.svc.GetUserRole(r.Context(), userID, tenantID) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } if role != "owner" && role != "partner" { jsonError(w, "only owners and partners can change roles", http.StatusForbidden) return } var req struct { Role string `json:"role"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonError(w, "invalid request body", http.StatusBadRequest) return } if !auth.IsValidRole(req.Role) { jsonError(w, "invalid role", http.StatusBadRequest) return } // Non-owners cannot promote to owner if role != "owner" && req.Role == "owner" { jsonError(w, "only owners can promote to owner", http.StatusForbidden) return } if err := h.svc.UpdateMemberRole(r.Context(), tenantID, memberID, req.Role); err != nil { jsonError(w, err.Error(), http.StatusBadRequest) return } jsonResponse(w, map[string]string{"status": "updated"}, http.StatusOK) } // GetMe handles GET /api/me — returns the current user's ID and role in the active tenant. func (h *TenantHandler) GetMe(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } role := auth.UserRoleFromContext(r.Context()) tenantID, _ := auth.TenantFromContext(r.Context()) // Get user's permissions for frontend UI perms := auth.GetRolePermissions(role) jsonResponse(w, map[string]any{ "user_id": userID, "tenant_id": tenantID, "role": role, "permissions": perms, }, http.StatusOK) } func jsonResponse(w http.ResponseWriter, data interface{}, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } func jsonError(w http.ResponseWriter, msg string, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]string{"error": msg}) }