1
0
forked from baron/baron-sso

테넌트 목록 및 조직 계층 구조 개선

This commit is contained in:
2026-02-27 10:29:15 +09:00
parent 600961f33d
commit ca45a14bae
27 changed files with 1906 additions and 806 deletions

View File

@@ -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 ""
}
}