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>
This commit is contained in:
268
ai-compliance-sdk/internal/api/handlers/multitenant_handlers.go
Normal file
268
ai-compliance-sdk/internal/api/handlers/multitenant_handlers.go
Normal file
@@ -0,0 +1,268 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user