forked from baron/baron-sso
662 lines
19 KiB
Go
662 lines
19 KiB
Go
package handler
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"baron-sso-backend/internal/repository"
|
|
"baron-sso-backend/internal/service"
|
|
"baron-sso-backend/internal/utils"
|
|
"errors"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type TenantHandler struct {
|
|
DB *gorm.DB
|
|
Service service.TenantService
|
|
UserRepo repository.UserRepository
|
|
Keto service.KetoService
|
|
KetoOutbox repository.KetoOutboxRepository
|
|
KratosAdmin service.KratosAdminService
|
|
}
|
|
|
|
func NewTenantHandler(db *gorm.DB, svc service.TenantService, userRepo repository.UserRepository, keto service.KetoService, outbox repository.KetoOutboxRepository, kratos service.KratosAdminService) *TenantHandler {
|
|
return &TenantHandler{
|
|
DB: db,
|
|
Service: svc,
|
|
UserRepo: userRepo,
|
|
Keto: keto,
|
|
KetoOutbox: outbox,
|
|
KratosAdmin: kratos,
|
|
}
|
|
}
|
|
|
|
type tenantSummary struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
ParentID *string `json:"parentId"`
|
|
Name string `json:"name"`
|
|
Slug string `json:"slug"`
|
|
Description string `json:"description"`
|
|
Status string `json:"status"`
|
|
Domains []string `json:"domains,omitempty"`
|
|
Config domain.JSONMap `json:"config,omitempty"`
|
|
MemberCount int64 `json:"memberCount"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
type tenantListResponse struct {
|
|
Items []tenantSummary `json:"items"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Slug string `json:"slug"`
|
|
Description string `json:"description"`
|
|
Domain string `json:"domain"`
|
|
AdminEmail string `json:"adminEmail"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
|
}
|
|
|
|
// Basic validation
|
|
if req.Name == "" || req.Domain == "" || req.AdminEmail == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "name, domain, and adminEmail are required")
|
|
}
|
|
|
|
tenant, err := h.Service.RequestRegistration(c.Context(), req.Name, req.Slug, req.Description, req.Domain, req.AdminEmail)
|
|
if err != nil {
|
|
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
|
}
|
|
|
|
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{
|
|
"message": "Registration request received and is pending approval.",
|
|
"tenant": mapTenantSummary(*tenant),
|
|
})
|
|
}
|
|
|
|
func (h *TenantHandler) ApproveTenant(c *fiber.Ctx) error {
|
|
tenantID := c.Params("id")
|
|
if tenantID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
|
|
}
|
|
|
|
if err := h.Service.ApproveTenant(c.Context(), tenantID); err != nil {
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"message": "Tenant approved successfully"})
|
|
}
|
|
|
|
func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
|
limit := c.QueryInt("limit", 50)
|
|
offset := c.QueryInt("offset", 0)
|
|
parentId := c.Query("parentId")
|
|
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
|
|
tenants, total, err := h.Service.ListTenants(c.Context(), limit, offset, parentId)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
// Fetch member counts for all tenants in one query using slugs (company codes)
|
|
slugs := make([]string, 0, len(tenants))
|
|
for _, t := range tenants {
|
|
slugs = append(slugs, t.Slug)
|
|
}
|
|
memberCounts, err := h.UserRepo.CountByCompanyCodes(c.Context(), slugs)
|
|
if err != nil {
|
|
slog.Warn("failed to count members for tenants", "error", err)
|
|
memberCounts = make(map[string]int64)
|
|
}
|
|
|
|
items := make([]tenantSummary, 0, len(tenants))
|
|
for _, t := range tenants {
|
|
summary := mapTenantSummary(t)
|
|
// Ensure robust matching by trimming and lowercasing the slug key
|
|
key := strings.ToLower(strings.TrimSpace(t.Slug))
|
|
summary.MemberCount = memberCounts[key]
|
|
items = append(items, summary)
|
|
}
|
|
|
|
return c.JSON(tenantListResponse{Items: items, Limit: limit, Offset: offset, Total: total})
|
|
}
|
|
|
|
func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
|
|
if h.DB == nil {
|
|
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
|
|
}
|
|
|
|
tenantID := strings.TrimSpace(c.Params("id"))
|
|
if tenantID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
|
|
}
|
|
|
|
var tenant domain.Tenant
|
|
if err := h.DB.Preload("Domains").First(&tenant, "id = ?", tenantID).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errorJSON(c, fiber.StatusNotFound, "tenant not found")
|
|
}
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
memberCounts, err := h.UserRepo.CountByCompanyCodes(c.Context(), []string{tenant.Slug})
|
|
count := int64(0)
|
|
if err == nil {
|
|
count = memberCounts[strings.ToLower(tenant.Slug)]
|
|
}
|
|
summary := mapTenantSummary(tenant)
|
|
summary.MemberCount = count
|
|
|
|
return c.JSON(summary)
|
|
}
|
|
|
|
func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
|
|
if h.DB == nil {
|
|
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
|
|
}
|
|
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Slug string `json:"slug"`
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
Status string `json:"status"`
|
|
Domains []string `json:"domains"`
|
|
ParentID *string `json:"parentId"`
|
|
Config map[string]any `json:"config"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
|
}
|
|
|
|
name := strings.TrimSpace(req.Name)
|
|
if name == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "name is required")
|
|
}
|
|
|
|
tenantType := normalizeTenantType(req.Type)
|
|
if tenantType == "" {
|
|
tenantType = domain.TenantTypeCompany // Default to COMPANY
|
|
}
|
|
|
|
slug := req.Slug
|
|
if slug == "" {
|
|
slug = utils.GenerateUniqueSlug(name, func(s string) bool {
|
|
var count int64
|
|
h.DB.Unscoped().Model(&domain.Tenant{}).Where("slug = ?", s).Count(&count)
|
|
return count > 0
|
|
})
|
|
} else {
|
|
slug = utils.GenerateSlug(slug)
|
|
}
|
|
if slug == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "slug is required")
|
|
}
|
|
|
|
status := normalizeTenantStatus(req.Status)
|
|
if status == "" {
|
|
status = "active"
|
|
}
|
|
|
|
// Use Service
|
|
var parentID *string
|
|
if req.ParentID != nil && strings.TrimSpace(*req.ParentID) != "" {
|
|
pid := strings.TrimSpace(*req.ParentID)
|
|
parentID = &pid
|
|
}
|
|
|
|
// Extract creator ID if present
|
|
creatorID := ""
|
|
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok {
|
|
creatorID = profile.ID
|
|
}
|
|
|
|
tenant, err := h.Service.RegisterTenant(c.Context(), name, slug, tenantType, req.Description, req.Domains, parentID, creatorID)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "already exists") {
|
|
return errorJSON(c, fiber.StatusConflict, err.Error())
|
|
}
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
summary := mapTenantSummary(*tenant)
|
|
summary.MemberCount = 0
|
|
|
|
if req.Config != nil {
|
|
tenant.Config = req.Config
|
|
h.DB.Save(tenant)
|
|
summary.Config = tenant.Config
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(summary)
|
|
}
|
|
|
|
func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
|
|
if h.DB == nil {
|
|
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
|
|
}
|
|
|
|
tenantID := strings.TrimSpace(c.Params("id"))
|
|
if tenantID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
|
|
}
|
|
|
|
var tenant domain.Tenant
|
|
if err := h.DB.First(&tenant, "id = ?", tenantID).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errorJSON(c, fiber.StatusNotFound, "tenant not found")
|
|
}
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
var req struct {
|
|
Name *string `json:"name"`
|
|
Type *string `json:"type"`
|
|
Slug *string `json:"slug"`
|
|
Description *string `json:"description"`
|
|
Status *string `json:"status"`
|
|
ParentID *string `json:"parentId"`
|
|
Domains []string `json:"domains"`
|
|
Config map[string]any `json:"config"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
|
}
|
|
|
|
if req.Name != nil {
|
|
name := strings.TrimSpace(*req.Name)
|
|
if name == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "name cannot be empty")
|
|
}
|
|
tenant.Name = name
|
|
}
|
|
if req.Type != nil {
|
|
tenantType := normalizeTenantType(*req.Type)
|
|
if tenantType == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid tenant type"})
|
|
}
|
|
tenant.Type = tenantType
|
|
}
|
|
if req.Slug != nil {
|
|
slug := utils.GenerateSlug(*req.Slug)
|
|
if slug == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "slug cannot be empty")
|
|
}
|
|
if slug != tenant.Slug {
|
|
var exists domain.Tenant
|
|
if err := h.DB.Unscoped().Where("slug = ?", slug).First(&exists).Error; err == nil {
|
|
return errorJSON(c, fiber.StatusConflict, "slug already exists")
|
|
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
tenant.Slug = slug
|
|
}
|
|
}
|
|
if req.Description != nil {
|
|
tenant.Description = strings.TrimSpace(*req.Description)
|
|
}
|
|
if req.Status != nil {
|
|
status := normalizeTenantStatus(*req.Status)
|
|
if status == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
|
|
}
|
|
tenant.Status = status
|
|
}
|
|
if req.ParentID != nil {
|
|
pid := strings.TrimSpace(*req.ParentID)
|
|
if pid == "" {
|
|
tenant.ParentID = nil
|
|
} else {
|
|
tenant.ParentID = &pid
|
|
}
|
|
|
|
// [Keto] Sync hierarchy via Outbox
|
|
if h.KetoOutbox != nil {
|
|
if tenant.ParentID != nil {
|
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
|
Namespace: "Tenant",
|
|
Object: tenant.ID,
|
|
Relation: "parents",
|
|
Subject: "Tenant:" + *tenant.ParentID,
|
|
Action: domain.KetoOutboxActionCreate,
|
|
})
|
|
} else {
|
|
// We don't have enough info here to delete specific parent if we don't know the old one,
|
|
// but for now we focus on adding.
|
|
}
|
|
}
|
|
}
|
|
if req.Config != nil {
|
|
tenant.Config = req.Config
|
|
}
|
|
|
|
if err := h.DB.Save(&tenant).Error; err != nil {
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
// Update domains if provided
|
|
if req.Domains != nil {
|
|
// Simple approach: Delete existing and recreate
|
|
if err := h.DB.Delete(&domain.TenantDomain{}, "tenant_id = ?", tenant.ID).Error; err != nil {
|
|
return errorJSON(c, fiber.StatusInternalServerError, "failed to clear old domains")
|
|
}
|
|
for _, d := range req.Domains {
|
|
if strings.TrimSpace(d) == "" {
|
|
continue
|
|
}
|
|
// Use repository for consistency
|
|
if err := repository.NewTenantRepository(h.DB).AddDomain(c.Context(), tenant.ID, strings.TrimSpace(d), true); err != nil {
|
|
return errorJSON(c, fiber.StatusInternalServerError, "failed to add domain: "+d)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Refetch to get updated relations
|
|
h.DB.Preload("Domains").First(&tenant, "id = ?", tenant.ID)
|
|
|
|
return c.JSON(mapTenantSummary(tenant))
|
|
}
|
|
|
|
func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error {
|
|
if h.DB == nil {
|
|
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
|
|
}
|
|
|
|
tenantID := strings.TrimSpace(c.Params("id"))
|
|
if tenantID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
|
|
}
|
|
|
|
var tenant domain.Tenant
|
|
if err := h.DB.First(&tenant, "id = ?", tenantID).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errorJSON(c, fiber.StatusNotFound, "tenant not found")
|
|
}
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
// Rename slug to release it for reuse before soft delete
|
|
deletedSlug := tenant.Slug + "-deleted-" + time.Now().Format("20060102150405")
|
|
if err := h.DB.Model(&tenant).Update("slug", deletedSlug).Error; err != nil {
|
|
return errorJSON(c, fiber.StatusInternalServerError, "failed to release slug")
|
|
}
|
|
|
|
if err := h.DB.Delete(&tenant).Error; err != nil {
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return c.SendStatus(fiber.StatusNoContent)
|
|
}
|
|
|
|
func (h *TenantHandler) ListAdmins(c *fiber.Ctx) error {
|
|
tenantID := c.Params("id")
|
|
if tenantID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
|
|
}
|
|
|
|
// Fetch admins from Keto
|
|
relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "admins", "")
|
|
if err != nil {
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
type adminInfo struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
}
|
|
admins := []adminInfo{}
|
|
|
|
for _, rel := range relations {
|
|
if !strings.HasPrefix(rel.SubjectID, "User:") {
|
|
continue
|
|
}
|
|
userID := strings.TrimPrefix(rel.SubjectID, "User:")
|
|
|
|
// Fetch user details - Try Kratos first, then local DB
|
|
name := "Unknown"
|
|
email := "Unknown"
|
|
|
|
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
|
if err == nil && identity != nil {
|
|
if n, ok := identity.Traits["name"].(string); ok {
|
|
name = n
|
|
}
|
|
if e, ok := identity.Traits["email"].(string); ok {
|
|
email = e
|
|
}
|
|
} else if h.UserRepo != nil {
|
|
// Fallback to local DB (useful for Mock users or users not yet synced/migrated to Kratos)
|
|
user, err := h.UserRepo.FindByID(c.Context(), userID)
|
|
if err == nil && user != nil {
|
|
name = user.Name
|
|
email = user.Email
|
|
} else if userID == "00000000-0000-0000-0000-000000000000" {
|
|
name = "Dev Mock User"
|
|
email = "mock@hmac.kr"
|
|
}
|
|
}
|
|
|
|
admins = append(admins, adminInfo{
|
|
ID: userID,
|
|
Name: name,
|
|
Email: email,
|
|
})
|
|
}
|
|
|
|
return c.JSON(admins)
|
|
}
|
|
|
|
func (h *TenantHandler) AddAdmin(c *fiber.Ctx) error {
|
|
tenantID := c.Params("id")
|
|
userID := c.Params("userId")
|
|
if tenantID == "" || userID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
|
|
}
|
|
|
|
if h.KetoOutbox != nil {
|
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
|
Namespace: "Tenant",
|
|
Object: tenantID,
|
|
Relation: "admins",
|
|
Subject: "User:" + userID,
|
|
Action: domain.KetoOutboxActionCreate,
|
|
})
|
|
// Also add as member for UI visibility/ReBAC logic
|
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
|
Namespace: "Tenant",
|
|
Object: tenantID,
|
|
Relation: "members",
|
|
Subject: "User:" + userID,
|
|
Action: domain.KetoOutboxActionCreate,
|
|
})
|
|
}
|
|
|
|
return c.SendStatus(fiber.StatusOK)
|
|
}
|
|
|
|
func (h *TenantHandler) RemoveAdmin(c *fiber.Ctx) error {
|
|
tenantID := c.Params("id")
|
|
userID := c.Params("userId")
|
|
if tenantID == "" || userID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
|
|
}
|
|
|
|
if h.KetoOutbox != nil {
|
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
|
Namespace: "Tenant",
|
|
Object: tenantID,
|
|
Relation: "admins",
|
|
Subject: "User:" + userID,
|
|
Action: domain.KetoOutboxActionDelete,
|
|
})
|
|
}
|
|
|
|
return c.SendStatus(fiber.StatusNoContent)
|
|
}
|
|
|
|
func (h *TenantHandler) ListOwners(c *fiber.Ctx) error {
|
|
tenantID := c.Params("id")
|
|
if tenantID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
|
|
}
|
|
|
|
// Fetch owners from Keto
|
|
relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "owners", "")
|
|
if err != nil {
|
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
type ownerInfo struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
}
|
|
owners := []ownerInfo{}
|
|
|
|
for _, rel := range relations {
|
|
if !strings.HasPrefix(rel.SubjectID, "User:") {
|
|
continue
|
|
}
|
|
userID := strings.TrimPrefix(rel.SubjectID, "User:")
|
|
|
|
// Fetch user details - Try Kratos first, then local DB
|
|
name := "Unknown"
|
|
email := "Unknown"
|
|
|
|
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
|
if err == nil && identity != nil {
|
|
if n, ok := identity.Traits["name"].(string); ok {
|
|
name = n
|
|
}
|
|
if e, ok := identity.Traits["email"].(string); ok {
|
|
email = e
|
|
}
|
|
} else if h.UserRepo != nil {
|
|
// Fallback to local DB
|
|
user, err := h.UserRepo.FindByID(c.Context(), userID)
|
|
if err == nil && user != nil {
|
|
name = user.Name
|
|
email = user.Email
|
|
} else if userID == "00000000-0000-0000-0000-000000000000" {
|
|
name = "Dev Mock User"
|
|
email = "mock@hmac.kr"
|
|
}
|
|
}
|
|
|
|
owners = append(owners, ownerInfo{
|
|
ID: userID,
|
|
Name: name,
|
|
Email: email,
|
|
})
|
|
}
|
|
|
|
return c.JSON(owners)
|
|
}
|
|
|
|
func (h *TenantHandler) AddOwner(c *fiber.Ctx) error {
|
|
tenantID := c.Params("id")
|
|
userID := c.Params("userId")
|
|
if tenantID == "" || userID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
|
|
}
|
|
|
|
if h.KetoOutbox != nil {
|
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
|
Namespace: "Tenant",
|
|
Object: tenantID,
|
|
Relation: "owners",
|
|
Subject: "User:" + userID,
|
|
Action: domain.KetoOutboxActionCreate,
|
|
})
|
|
// Also add as member for UI visibility/ReBAC logic
|
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
|
Namespace: "Tenant",
|
|
Object: tenantID,
|
|
Relation: "members",
|
|
Subject: "User:" + userID,
|
|
Action: domain.KetoOutboxActionCreate,
|
|
})
|
|
}
|
|
|
|
return c.SendStatus(fiber.StatusOK)
|
|
}
|
|
|
|
func (h *TenantHandler) RemoveOwner(c *fiber.Ctx) error {
|
|
tenantID := c.Params("id")
|
|
userID := c.Params("userId")
|
|
if tenantID == "" || userID == "" {
|
|
return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
|
|
}
|
|
|
|
if h.KetoOutbox != nil {
|
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
|
Namespace: "Tenant",
|
|
Object: tenantID,
|
|
Relation: "owners",
|
|
Subject: "User:" + userID,
|
|
Action: domain.KetoOutboxActionDelete,
|
|
})
|
|
}
|
|
|
|
return c.SendStatus(fiber.StatusNoContent)
|
|
}
|
|
|
|
func mapTenantSummary(t domain.Tenant) tenantSummary {
|
|
domains := make([]string, 0, len(t.Domains))
|
|
for _, d := range t.Domains {
|
|
domains = append(domains, d.Domain)
|
|
}
|
|
|
|
return tenantSummary{
|
|
ID: t.ID,
|
|
Type: t.Type,
|
|
ParentID: t.ParentID,
|
|
Name: t.Name,
|
|
Slug: t.Slug,
|
|
Description: t.Description,
|
|
Status: t.Status,
|
|
Domains: domains,
|
|
Config: t.Config,
|
|
CreatedAt: t.CreatedAt.Format(time.RFC3339),
|
|
UpdatedAt: t.UpdatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func normalizeTenantStatus(value string) string {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
if value != "active" && value != "inactive" {
|
|
return ""
|
|
}
|
|
return value
|
|
}
|
|
|
|
func normalizeTenantType(value string) string {
|
|
value = strings.ToUpper(strings.TrimSpace(value))
|
|
switch value {
|
|
case domain.TenantTypePersonal, domain.TenantTypeCompany, domain.TenantTypeCompanyGroup, domain.TenantTypeUserGroup:
|
|
return value
|
|
default:
|
|
return ""
|
|
}
|
|
}
|