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 {