forked from baron/baron-sso
테넌트 목록 및 조직 계층 구조 개선
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"baron-sso-backend/internal/service"
|
||||
"baron-sso-backend/internal/utils"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -16,15 +17,17 @@ import (
|
||||
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, keto service.KetoService, outbox repository.KetoOutboxRepository, kratos service.KratosAdminService) *TenantHandler {
|
||||
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,
|
||||
@@ -33,12 +36,15 @@ func NewTenantHandler(db *gorm.DB, svc service.TenantService, keto service.KetoS
|
||||
|
||||
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"`
|
||||
}
|
||||
@@ -98,6 +104,8 @@ 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
|
||||
}
|
||||
@@ -105,19 +113,45 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
// Use separate queries for count and find to avoid GORM statement contamination
|
||||
countQuery := h.DB.Model(&domain.Tenant{})
|
||||
if parentId != "" {
|
||||
countQuery = countQuery.Where("parent_id = ?", parentId)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := h.DB.Model(&domain.Tenant{}).Count(&total).Error; err != nil {
|
||||
if err := countQuery.Count(&total).Error; err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
findQuery := h.DB.Model(&domain.Tenant{})
|
||||
if parentId != "" {
|
||||
findQuery = findQuery.Where("parent_id = ?", parentId)
|
||||
}
|
||||
|
||||
var tenants []domain.Tenant
|
||||
if err := h.DB.Order("created_at desc").Limit(limit).Offset(offset).Preload("Domains").Find(&tenants).Error; err != nil {
|
||||
if err := findQuery.Order("created_at desc").Limit(limit).Offset(offset).Preload("Domains").Find(&tenants).Error; 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 {
|
||||
items = append(items, mapTenantSummary(t))
|
||||
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})
|
||||
@@ -141,7 +175,15 @@ func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(mapTenantSummary(tenant))
|
||||
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 {
|
||||
@@ -152,6 +194,7 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
|
||||
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"`
|
||||
@@ -167,6 +210,11 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "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 {
|
||||
@@ -193,7 +241,7 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
|
||||
parentID = &pid
|
||||
}
|
||||
|
||||
tenant, err := h.Service.RegisterTenant(c.Context(), name, slug, req.Description, req.Domains, parentID)
|
||||
tenant, err := h.Service.RegisterTenant(c.Context(), name, slug, tenantType, req.Description, req.Domains, parentID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -201,12 +249,16 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": 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(mapTenantSummary(*tenant))
|
||||
return c.Status(fiber.StatusCreated).JSON(summary)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
|
||||
@@ -229,9 +281,11 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) 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"`
|
||||
}
|
||||
@@ -246,6 +300,13 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
|
||||
}
|
||||
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 == "" {
|
||||
@@ -271,6 +332,30 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -432,6 +517,8 @@ func mapTenantSummary(t domain.Tenant) tenantSummary {
|
||||
|
||||
return tenantSummary{
|
||||
ID: t.ID,
|
||||
Type: t.Type,
|
||||
ParentID: t.ParentID,
|
||||
Name: t.Name,
|
||||
Slug: t.Slug,
|
||||
Description: t.Description,
|
||||
@@ -453,3 +540,13 @@ func normalizeTenantStatus(value string) string {
|
||||
}
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user