From d3a82d165395b2061e6403c297dbabda48edb096 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 13 Apr 2026 10:47:56 +0900 Subject: [PATCH] feat: allow regular users to view their own tenant's org chart Changes the /users endpoint to allow RoleUser access and securely restricts the returned data to only users within their affiliated tenants. Removes the unnecessary back button from the Org Chart view since it's now a top-level nav item. --- .../src/components/layout/AppLayout.tsx | 20 +++++++++++-------- .../tenants/routes/TenantOrgChartPage.tsx | 5 ----- backend/cmd/server/main.go | 19 ++++++++++-------- backend/internal/handler/user_handler.go | 19 +++++++++++++++--- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx index 927f75b0..2635c05d 100644 --- a/adminfront/src/components/layout/AppLayout.tsx +++ b/adminfront/src/components/layout/AppLayout.tsx @@ -125,11 +125,15 @@ function AppLayout() { icon: Building2, }); } - filteredItems.splice(manageableCount <= 1 && profile?.tenantId ? 2 : 2, 0, { - label: "ui.admin.nav.org_chart", - to: "/tenants/org-chart", - icon: Network, - }); + filteredItems.splice( + manageableCount <= 1 && profile?.tenantId ? 2 : 2, + 0, + { + label: "ui.admin.nav.org_chart", + to: "/tenants/org-chart", + icon: Network, + }, + ); } else { // 일반 사용자(Tenant Member)도 조직도 메뉴를 볼 수 있도록 추가합니다. filteredItems.splice(1, 0, { @@ -439,9 +443,9 @@ function AppLayout() { {navItems.map(({ label, to, icon: Icon }) => { const isOrgChart = location.pathname === "/tenants/org-chart"; const isTenantsRoot = to === "/tenants"; - const isCustomActive = isTenantsRoot - ? (location.pathname.startsWith("/tenants") && !isOrgChart) - : to === "/" + const isCustomActive = isTenantsRoot + ? location.pathname.startsWith("/tenants") && !isOrgChart + : to === "/" ? location.pathname === "/" : location.pathname.startsWith(to); diff --git a/adminfront/src/features/tenants/routes/TenantOrgChartPage.tsx b/adminfront/src/features/tenants/routes/TenantOrgChartPage.tsx index e2d44d18..97be1a17 100644 --- a/adminfront/src/features/tenants/routes/TenantOrgChartPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantOrgChartPage.tsx @@ -211,11 +211,6 @@ export function TenantOrgChartPage() {
-

조직도

diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index f7db4c9d..8923aed9 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -599,13 +599,17 @@ func main() { KetoService: ketoService, }) requireAdmin := middleware.RequireRole(middleware.RBACConfig{ - AllowedRoles: []string{domain.RoleSuperAdmin, domain.RoleTenantAdmin}, - AuthHandler: authHandler, - KetoService: ketoService, + AllowedRoles: []string{domain.RoleSuperAdmin, domain.RoleTenantAdmin}, + AuthHandler: authHandler, + KetoService: ketoService, + }) + requireAnyUser := middleware.RequireRole(middleware.RBACConfig{ + AllowedRoles: []string{domain.RoleSuperAdmin, domain.RoleTenantAdmin, domain.RoleRPAdmin, domain.RoleUser}, + AuthHandler: authHandler, + 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) @@ -668,9 +672,8 @@ func main() { relyingPartyHandler.Delete) // Admin User Management - admin.Get("/users", requireAdmin, userHandler.ListUsers) - admin.Get("/users/export", userHandler.ExportUsersCSV) // Removed requireAdmin to handle mock role in query param - admin.Post("/users", requireAdmin, userHandler.CreateUser) + admin.Get("/users", requireAnyUser, userHandler.ListUsers) + admin.Get("/users/export", userHandler.ExportUsersCSV) // Removed requireAdmin to handle mock role in query param admin.Post("/users", requireAdmin, userHandler.CreateUser) admin.Post("/users/bulk", requireAdmin, userHandler.BulkCreateUsers) admin.Put("/users/bulk", requireAdmin, userHandler.BulkUpdateUsers) admin.Delete("/users/bulk", requireAdmin, userHandler.BulkDeleteUsers) diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 31ee8bf7..147e85e5 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -100,13 +100,17 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error { // [New] Manageable Tenants Map for efficient lookup manageableSlugs := make(map[string]bool) - if requesterRole == domain.RoleTenantAdmin { + if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin { profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse) if profile != nil { for _, t := range profile.ManageableTenants { manageableSlugs[strings.ToLower(t.Slug)] = true manageableSlugs[strings.ToLower(t.ID)] = true // Add ID as well } + for _, t := range profile.JoinedTenants { + manageableSlugs[strings.ToLower(t.Slug)] = true + manageableSlugs[strings.ToLower(t.ID)] = true + } // Include primary tenant slug if not already there if profile.CompanyCode != "" { manageableSlugs[strings.ToLower(profile.CompanyCode)] = true @@ -137,8 +141,8 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error { compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode")) tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id")) - // Tenant Admin filtering - if requesterRole == domain.RoleTenantAdmin { + // Tenant Admin & Member filtering + if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin { if !manageableSlugs[compCode] && !manageableSlugs[tID] { continue } @@ -194,6 +198,15 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error { // 2. Fallback to Local DB if Kratos is down slog.Warn("Kratos unavailable, falling back to local DB for user list", "error", err) + // If requester is not Super Admin, we should technically filter by manageable slugs in DB too. + // For simplicity in fallback, if tenantSlug is empty we default to their primary company code. + if (requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin) && tenantSlug == "" { + profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse) + if profile != nil && profile.CompanyCode != "" { + tenantSlug = profile.CompanyCode + } + } + // Fetch from UserRepo users, total, err := h.UserRepo.List(c.Context(), offset, limit, search, tenantSlug) if err != nil {