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>
269 lines
7.6 KiB
Go
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,
|
|
})
|
|
}
|