Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/multitenant_handlers.go
Benjamin Boenisch 504dd3591b feat: Add Academy, Whistleblower, Incidents, Vendor, DSB, SSO, Reporting, Multi-Tenant and Industry backends
Go handlers, models, stores and migrations for all SDK modules.
Updates developer portal navigation and BYOEH page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 21:11:27 +01:00

269 lines
7.6 KiB
Go

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,
})
}