package handlers import ( "net/http" "github.com/breakpilot/ai-compliance-sdk/internal/multitenant" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // MultiTenantHandlers handles multi-tenant administration endpoints. type MultiTenantHandlers struct { store *multitenant.Store rbacStore *rbac.Store } // NewMultiTenantHandlers creates new multi-tenant handlers. func NewMultiTenantHandlers(store *multitenant.Store, rbacStore *rbac.Store) *MultiTenantHandlers { return &MultiTenantHandlers{ store: store, rbacStore: rbacStore, } } // GetOverview returns all tenants with compliance scores and module highlights. // GET /sdk/v1/multi-tenant/overview func (h *MultiTenantHandlers) GetOverview(c *gin.Context) { overview, err := h.store.GetOverview(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, overview) } // GetTenantDetail returns detailed compliance info for one tenant. // GET /sdk/v1/multi-tenant/tenants/:id func (h *MultiTenantHandlers) GetTenantDetail(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tenant ID"}) return } detail, err := h.store.GetTenantDetail(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "tenant not found"}) return } c.JSON(http.StatusOK, detail) } // CreateTenant creates a new tenant with default setup. // It creates the tenant via the RBAC store and then creates a default "main" namespace. // POST /sdk/v1/multi-tenant/tenants func (h *MultiTenantHandlers) CreateTenant(c *gin.Context) { var req multitenant.CreateTenantRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Build the tenant from the request tenant := &rbac.Tenant{ Name: req.Name, Slug: req.Slug, MaxUsers: req.MaxUsers, LLMQuotaMonthly: req.LLMQuotaMonthly, } // Create tenant via RBAC store (assigns ID, timestamps, defaults) if err := h.rbacStore.CreateTenant(c.Request.Context(), tenant); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Create default "main" namespace for the new tenant defaultNamespace := &rbac.Namespace{ TenantID: tenant.ID, Name: "Main", Slug: "main", } if err := h.rbacStore.CreateNamespace(c.Request.Context(), defaultNamespace); err != nil { // Tenant was created successfully but namespace creation failed. // Log and continue -- the tenant is still usable. c.JSON(http.StatusCreated, gin.H{ "tenant": tenant, "warning": "tenant created but default namespace creation failed: " + err.Error(), }) return } c.JSON(http.StatusCreated, gin.H{ "tenant": tenant, "namespace": defaultNamespace, }) } // UpdateTenant performs a partial update of tenant settings. // Only non-nil fields in the request body are applied. // PUT /sdk/v1/multi-tenant/tenants/:id func (h *MultiTenantHandlers) UpdateTenant(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tenant ID"}) return } var req multitenant.UpdateTenantRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Fetch the existing tenant so we can apply partial updates tenant, err := h.rbacStore.GetTenant(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "tenant not found"}) return } // Apply only the fields that were provided if req.Name != nil { tenant.Name = *req.Name } if req.MaxUsers != nil { tenant.MaxUsers = *req.MaxUsers } if req.LLMQuotaMonthly != nil { tenant.LLMQuotaMonthly = *req.LLMQuotaMonthly } if req.Status != nil { tenant.Status = rbac.TenantStatus(*req.Status) } if err := h.rbacStore.UpdateTenant(c.Request.Context(), tenant); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, tenant) } // ListNamespaces returns all namespaces for a specific tenant. // GET /sdk/v1/multi-tenant/tenants/:id/namespaces func (h *MultiTenantHandlers) ListNamespaces(c *gin.Context) { tenantID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tenant ID"}) return } namespaces, err := h.rbacStore.ListNamespaces(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "namespaces": namespaces, "total": len(namespaces), }) } // CreateNamespace creates a new namespace within a tenant. // POST /sdk/v1/multi-tenant/tenants/:id/namespaces func (h *MultiTenantHandlers) CreateNamespace(c *gin.Context) { tenantID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tenant ID"}) return } // Verify the tenant exists _, err = h.rbacStore.GetTenant(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "tenant not found"}) return } var req multitenant.CreateNamespaceRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } namespace := &rbac.Namespace{ TenantID: tenantID, Name: req.Name, Slug: req.Slug, } // Apply optional fields if provided if req.IsolationLevel != "" { namespace.IsolationLevel = rbac.IsolationLevel(req.IsolationLevel) } if req.DataClassification != "" { namespace.DataClassification = rbac.DataClassification(req.DataClassification) } if err := h.rbacStore.CreateNamespace(c.Request.Context(), namespace); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, namespace) } // SwitchTenant returns the tenant info needed for the frontend to switch context. // The caller provides a tenant_id and receives back the tenant details needed // to update the frontend's active tenant state. // POST /sdk/v1/multi-tenant/switch func (h *MultiTenantHandlers) SwitchTenant(c *gin.Context) { var req multitenant.SwitchTenantRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tenantID, err := uuid.Parse(req.TenantID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tenant ID"}) return } tenant, err := h.rbacStore.GetTenant(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "tenant not found"}) return } // Verify the tenant is active if tenant.Status != rbac.TenantStatusActive { c.JSON(http.StatusForbidden, gin.H{ "error": "tenant not active", "status": string(tenant.Status), }) return } // Get namespaces for the tenant so the frontend can populate namespace selectors namespaces, err := h.rbacStore.ListNamespaces(c.Request.Context(), tenantID) if err != nil { // Non-fatal: return tenant info without namespaces c.JSON(http.StatusOK, gin.H{ "tenant": multitenant.SwitchTenantResponse{ TenantID: tenant.ID, TenantName: tenant.Name, TenantSlug: tenant.Slug, Status: string(tenant.Status), }, }) return } c.JSON(http.StatusOK, gin.H{ "tenant": multitenant.SwitchTenantResponse{ TenantID: tenant.ID, TenantName: tenant.Name, TenantSlug: tenant.Slug, Status: string(tenant.Status), }, "namespaces": namespaces, }) }