forked from baron/baron-sso
관리자 권한 판별 로직 보완으로 메뉴/접근 제어 일치화
This commit is contained in:
@@ -10,6 +10,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
|
import { resolveProfileRole } from "../../lib/role";
|
||||||
import LanguageSelector from "../common/LanguageSelector";
|
import LanguageSelector from "../common/LanguageSelector";
|
||||||
import { Toaster } from "../ui/toaster";
|
import { Toaster } from "../ui/toaster";
|
||||||
|
|
||||||
@@ -96,6 +97,14 @@ function AppLayout() {
|
|||||||
auth.user?.profile?.email?.toString().trim() ||
|
auth.user?.profile?.email?.toString().trim() ||
|
||||||
t("ui.dev.profile.unknown_email", "unknown@example.com");
|
t("ui.dev.profile.unknown_email", "unknown@example.com");
|
||||||
const profileInitial = profileName.charAt(0).toUpperCase();
|
const profileInitial = profileName.charAt(0).toUpperCase();
|
||||||
|
const currentRole = resolveProfileRole(
|
||||||
|
auth.user?.profile as Record<string, unknown> | undefined,
|
||||||
|
);
|
||||||
|
const isDevConsoleAllowed = [
|
||||||
|
"super_admin",
|
||||||
|
"tenant_admin",
|
||||||
|
"rp_admin",
|
||||||
|
].includes(currentRole);
|
||||||
const expiresAtSec = auth.user?.expires_at;
|
const expiresAtSec = auth.user?.expires_at;
|
||||||
const remainingMs =
|
const remainingMs =
|
||||||
typeof expiresAtSec === "number" ? expiresAtSec * 1000 - nowMs : null;
|
typeof expiresAtSec === "number" ? expiresAtSec * 1000 - nowMs : null;
|
||||||
@@ -191,23 +200,24 @@ function AppLayout() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => (
|
{isDevConsoleAllowed &&
|
||||||
<NavLink
|
navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => (
|
||||||
key={to}
|
<NavLink
|
||||||
to={to}
|
key={to}
|
||||||
className={({ isActive }) =>
|
to={to}
|
||||||
[
|
className={({ isActive }) =>
|
||||||
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
|
[
|
||||||
isActive
|
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
|
||||||
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
|
isActive
|
||||||
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
|
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
|
||||||
].join(" ")
|
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
|
||||||
}
|
].join(" ")
|
||||||
>
|
}
|
||||||
<Icon size={18} />
|
>
|
||||||
<span>{t(labelKey, labelFallback)}</span>
|
<Icon size={18} />
|
||||||
</NavLink>
|
<span>{t(labelKey, labelFallback)}</span>
|
||||||
))}
|
</NavLink>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { Navigate, Outlet } from "react-router-dom";
|
import { Navigate, Outlet } from "react-router-dom";
|
||||||
|
import { t } from "../../lib/i18n";
|
||||||
|
import { resolveProfileRole } from "../../lib/role";
|
||||||
|
|
||||||
export default function AuthGuard() {
|
export default function AuthGuard() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
@@ -16,5 +18,39 @@ export default function AuthGuard() {
|
|||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedRole = resolveProfileRole(
|
||||||
|
auth.user?.profile as Record<string, unknown> | undefined,
|
||||||
|
);
|
||||||
|
const isTenantMember =
|
||||||
|
normalizedRole === "user" || normalizedRole === "tenant_member";
|
||||||
|
|
||||||
|
if (isTenantMember) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen grid place-items-center bg-background text-foreground p-6">
|
||||||
|
<div className="max-w-lg w-full rounded-xl border border-border bg-card p-6 space-y-4">
|
||||||
|
<h1 className="text-xl font-semibold">
|
||||||
|
{t("msg.dev.auth.access_denied_title", "접근 권한이 없습니다.")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"msg.dev.auth.access_denied_description",
|
||||||
|
"DevFront는 관리자 전용 화면입니다. 권한이 필요하면 관리자에게 요청해 주세요.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
|
||||||
|
onClick={() => {
|
||||||
|
auth.removeUser();
|
||||||
|
window.location.href = "/login";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("ui.common.back_to_login", "로그인으로 돌아가기")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
}
|
}
|
||||||
|
|||||||
25
devfront/src/lib/role.ts
Normal file
25
devfront/src/lib/role.ts
Normal file
@@ -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<string, unknown> | 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 "";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user