From 010719eee9a18a4b37898052840d8490e2c85b6b Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 13 Apr 2026 11:56:35 +0900 Subject: [PATCH] feat(backend): allow regular users and tenant admins to list their full tenant trees Changes the /v1/admin/tenants endpoint to be accessible by all authenticated users (requireAnyUser). In the handler, it dynamically resolves the user's affiliations and filters the response to return the complete hierarchical tree (root, parent, child, sibling nodes) for any tenant they belong to. --- backend/cmd/server/main.go | 5 +- backend/internal/handler/tenant_handler.go | 73 +++++++++++++++++++--- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 8923aed9..3be0125b 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -609,10 +609,11 @@ func main() { KetoService: ketoService, }) - admin.Get("/check", adminHandler.CheckAuth) // 기본 Admin 체크는 requireAdmin 없이 ApiKeyAuth로만 보호될 수 있음 (또는 추가 가능) admin.Get("/stats", requireSuperAdmin, adminHandler.GetSystemStats) + admin.Get("/check", adminHandler.CheckAuth) // 기본 Admin 체크는 requireAdmin 없이 ApiKeyAuth로만 보호될 수 있음 (또는 추가 가능) + admin.Get("/stats", requireSuperAdmin, adminHandler.GetSystemStats) // Tenant Management (Mixed roles, handler filters results) - admin.Get("/tenants", requireAdmin, tenantHandler.ListTenants) + admin.Get("/tenants", requireAnyUser, tenantHandler.ListTenants) admin.Post("/tenants", requireSuperAdmin, tenantHandler.CreateTenant) admin.Delete("/tenants/bulk", requireSuperAdmin, tenantHandler.DeleteTenantsBulk) admin.Post("/tenants/:id/approve", requireSuperAdmin, tenantHandler.ApproveTenant) diff --git a/backend/internal/handler/tenant_handler.go b/backend/internal/handler/tenant_handler.go index 73bafe8e..03b4fb17 100644 --- a/backend/internal/handler/tenant_handler.go +++ b/backend/internal/handler/tenant_handler.go @@ -6,7 +6,6 @@ import ( "baron-sso-backend/internal/service" "baron-sso-backend/internal/utils" "errors" - "log/slog" "strings" "time" @@ -114,16 +113,72 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error { var err error profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse) + role := "" + if profile != nil { + role = domain.NormalizeRole(profile.Role) + } - // If Tenant Admin, only list manageable tenants - if profile != nil && domain.NormalizeRole(profile.Role) == domain.RoleTenantAdmin { - slog.Info("Listing manageable tenants for tenant admin", "userID", profile.ID) - tenants, err = h.Service.ListManageableTenants(c.Context(), profile.ID) + if role != domain.RoleSuperAdmin { + // Not a super admin: Only return the entire tree(s) of the tenants they belong to + allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "") if err != nil { - return errorJSON(c, fiber.StatusInternalServerError, "failed to list manageable tenants: "+err.Error()) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } + + if profile != nil { + baseTenantIDs := []string{} + for _, t := range profile.ManageableTenants { + baseTenantIDs = append(baseTenantIDs, t.ID) + } + for _, t := range profile.JoinedTenants { + baseTenantIDs = append(baseTenantIDs, t.ID) + } + if profile.TenantID != nil { + baseTenantIDs = append(baseTenantIDs, *profile.TenantID) + } + + // Try to find by companyCode if needed + if profile.CompanyCode != "" { + for _, t := range allTenants { + if strings.EqualFold(t.Slug, profile.CompanyCode) { + baseTenantIDs = append(baseTenantIDs, t.ID) + } + } + } + + parentMap := make(map[string]string) + for _, t := range allTenants { + if t.ParentID != nil { + parentMap[t.ID] = *t.ParentID + } + } + + findRoot := func(id string) string { + curr := id + for { + p, exists := parentMap[curr] + if !exists || p == "" { + break + } + curr = p + } + return curr + } + + roots := make(map[string]bool) + for _, id := range baseTenantIDs { + roots[findRoot(id)] = true + } + + // Filter tenants that belong to the same tree family + for _, t := range allTenants { + if roots[findRoot(t.ID)] { + tenants = append(tenants, t) + } + } + } + total = int64(len(tenants)) - // Apply basic pagination if needed (optional for usually small number of manageable tenants) if offset < len(tenants) { end := offset + limit if end > len(tenants) { @@ -751,8 +806,8 @@ func (h *TenantHandler) DeleteTenantsBulk(c *fiber.Ctx) error { return errorJSON(c, fiber.StatusBadRequest, "no IDs provided") } - // Permission check: Super Admin can delete anything. - // Tenant Admin should theoretically only delete manageable sub-tenants, + // Permission check: Super Admin can delete anything. + // Tenant Admin should theoretically only delete manageable sub-tenants, // but currently bulk delete is intended for Super Admin. profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse) if profile == nil || domain.NormalizeRole(profile.Role) != domain.RoleSuperAdmin {