Tenant management: - POST /api/tenants — create tenant (creator becomes owner) - GET /api/tenants — list tenants for authenticated user - GET /api/tenants/:id — tenant details with access check - POST /api/tenants/:id/invite — invite user by email (owner/admin) - DELETE /api/tenants/:id/members/:uid — remove member - GET /api/tenants/:id/members — list members New packages: - internal/services/tenant_service.go — CRUD on tenants + user_tenants - internal/handlers/tenant_handler.go — HTTP handlers with auth checks - internal/auth/tenant_resolver.go — X-Tenant-ID header middleware, defaults to user's first tenant for scoped routes Authorization: owners/admins can invite and remove members. Cannot remove the last owner. Users can remove themselves. TenantResolver applies to resource routes (cases, deadlines, etc.) but not tenant management routes.
244 lines
6.1 KiB
Go
244 lines
6.1 KiB
Go
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 admins 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 != "admin" {
|
|
jsonError(w, "only owners and admins 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 = "member"
|
|
}
|
|
if req.Role != "member" && req.Role != "admin" {
|
|
jsonError(w, "role must be member or admin", http.StatusBadRequest)
|
|
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 admins 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 != "admin" && 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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})
|
|
}
|