diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx index 66b8d175..92b458a8 100644 --- a/devfront/src/components/layout/AppLayout.tsx +++ b/devfront/src/components/layout/AppLayout.tsx @@ -10,6 +10,7 @@ import { useEffect, useRef, useState } from "react"; import { useAuth } from "react-oidc-context"; import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { t } from "../../lib/i18n"; +import { resolveProfileRole } from "../../lib/role"; import LanguageSelector from "../common/LanguageSelector"; import { Toaster } from "../ui/toaster"; @@ -96,6 +97,14 @@ function AppLayout() { auth.user?.profile?.email?.toString().trim() || t("ui.dev.profile.unknown_email", "unknown@example.com"); const profileInitial = profileName.charAt(0).toUpperCase(); + const currentRole = resolveProfileRole( + auth.user?.profile as Record | undefined, + ); + const isDevConsoleAllowed = [ + "super_admin", + "tenant_admin", + "rp_admin", + ].includes(currentRole); const expiresAtSec = auth.user?.expires_at; const remainingMs = typeof expiresAtSec === "number" ? expiresAtSec * 1000 - nowMs : null; @@ -191,23 +200,24 @@ function AppLayout() {
- {navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => ( - - [ - "flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition", - isActive - ? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]" - : "text-muted-foreground hover:bg-muted/10 hover:text-foreground", - ].join(" ") - } - > - - {t(labelKey, labelFallback)} - - ))} + {isDevConsoleAllowed && + navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => ( + + [ + "flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition", + isActive + ? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]" + : "text-muted-foreground hover:bg-muted/10 hover:text-foreground", + ].join(" ") + } + > + + {t(labelKey, labelFallback)} + + ))}
diff --git a/devfront/src/features/auth/AuthGuard.tsx b/devfront/src/features/auth/AuthGuard.tsx index 26069583..0ab7c9fd 100644 --- a/devfront/src/features/auth/AuthGuard.tsx +++ b/devfront/src/features/auth/AuthGuard.tsx @@ -1,5 +1,7 @@ import { useAuth } from "react-oidc-context"; import { Navigate, Outlet } from "react-router-dom"; +import { t } from "../../lib/i18n"; +import { resolveProfileRole } from "../../lib/role"; export default function AuthGuard() { const auth = useAuth(); @@ -16,5 +18,39 @@ export default function AuthGuard() { return ; } + const normalizedRole = resolveProfileRole( + auth.user?.profile as Record | undefined, + ); + const isTenantMember = + normalizedRole === "user" || normalizedRole === "tenant_member"; + + if (isTenantMember) { + return ( +
+
+

+ {t("msg.dev.auth.access_denied_title", "접근 권한이 없습니다.")} +

+

+ {t( + "msg.dev.auth.access_denied_description", + "DevFront는 관리자 전용 화면입니다. 권한이 필요하면 관리자에게 요청해 주세요.", + )} +

+ +
+
+ ); + } + return ; } diff --git a/devfront/src/lib/role.ts b/devfront/src/lib/role.ts new file mode 100644 index 00000000..f4400c72 --- /dev/null +++ b/devfront/src/lib/role.ts @@ -0,0 +1,25 @@ +export function normalizeRole(rawRole: unknown): string { + if (typeof rawRole !== "string") return ""; + const role = rawRole.trim().toLowerCase(); + if (role === "tenant_member") return "user"; + if (role === "admin") return "tenant_admin"; + if (role === "superadmin") return "super_admin"; + if (role === "tenantadmin") return "tenant_admin"; + if (role === "rpadmin") return "rp_admin"; + return role; +} + +export function resolveProfileRole(profile: Record | undefined) { + if (!profile) return ""; + const candidates = [ + profile.role, + profile.grade, + profile["custom:role"], + profile["custom:grade"], + ]; + for (const candidate of candidates) { + const normalized = normalizeRole(candidate); + if (normalized) return normalized; + } + return ""; +}