From 0f8b19a9b1c3ee6c31436a67982ab33397b7ec71 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 4 Mar 2026 13:15:28 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=ED=8C=90=EB=B3=84=20=EB=A1=9C=EC=A7=81=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84=EC=9C=BC=EB=A1=9C=20=EB=A9=94=EB=89=B4/=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=20=EC=A0=9C=EC=96=B4=20=EC=9D=BC=EC=B9=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/components/layout/AppLayout.tsx | 44 ++++++++++++-------- devfront/src/features/auth/AuthGuard.tsx | 36 ++++++++++++++++ devfront/src/lib/role.ts | 25 +++++++++++ 3 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 devfront/src/lib/role.ts 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 ""; +}