1
0
forked from baron/baron-sso

feat: simplify RBAC roles and remove dev role switcher

- Simplified RBAC system to two roles: super_admin and user.
- Removed tenant_admin and rp_admin roles across backend and frontend.
- Removed Dev Role Switcher feature from adminfront.
- Updated all handlers, middlewares, and navigation to reflect the new role model.
- Fixed backend build errors and updated tests.
This commit is contained in:
2026-06-02 18:29:18 +09:00
parent 57f05e2694
commit 802bf3e91d
32 changed files with 487 additions and 938 deletions

View File

@@ -42,10 +42,8 @@ import {
shouldAttemptUnlimitedSessionRenew,
} from "../../lib/sessionSliding";
import LanguageSelector from "../common/LanguageSelector";
import RoleSwitcher from "./RoleSwitcher";
const LOCALE_CHANGED_EVENT = "baron_locale_changed";
const DEV_ROLE_CHANGED_EVENT = "baron_dev_role_changed";
const staticNavItems: ShellSidebarNavItem[] = [
{
@@ -132,19 +130,8 @@ function AppLayout() {
const lastRenewAttemptAtRef = useRef(0);
const lastVisitedRouteRef = useRef<string | null>(null);
const isDevelopmentRuntime = import.meta.env.MODE === "development";
const isDevRoleOverrideEnabled =
import.meta.env.MODE === "development" ||
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
._IS_TEST_MODE === true;
const isMockRoleEnabled =
isDevRoleOverrideEnabled &&
window.localStorage.getItem("X-Mock-Role-Enabled") === "true";
const mockRoleOverride = isMockRoleEnabled
? window.localStorage.getItem("X-Mock-Role")
: null;
const [theme, setTheme] = useState<"light" | "dark">(readShellTheme);
const [isProfileOpen, setIsProfileOpen] = useState(false);
const [, setDevelopmentRenderRevision] = useState(0);
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() =>
readShellSessionExpiryEnabled(!isDevelopmentRuntime),
);
@@ -173,10 +160,9 @@ function AppLayout() {
const isTest =
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
._IS_TEST_MODE === true;
const effectiveRole = mockRoleOverride || profile?.role;
const effectiveRole = profile?.role;
const isSuperAdmin = isTest || isSuperAdminRole(effectiveRole);
const isTenantAdmin = effectiveRole === "tenant_admin";
const manageableCount = profile?.manageableTenants?.length ?? 0;
const orgfrontUrl = buildAuthenticatedOrgChartUrl(
import.meta.env.ORGFRONT_URL || "http://localhost:5175",
@@ -215,7 +201,8 @@ function AppLayout() {
to: "/system/data-integrity",
icon: ShieldCheck,
});
} else if (isTenantAdmin || manageableCount > 0) {
} else {
// Non-superadmins
if (manageableCount <= 1 && profile?.tenantId) {
filteredItems.splice(1, 0, {
labelKey: "ui.admin.nav.my_tenant",
@@ -231,20 +218,7 @@ function AppLayout() {
icon: Building2,
});
}
filteredItems.splice(
manageableCount <= 1 && profile?.tenantId ? 2 : 2,
0,
{
labelKey: "ui.admin.nav.org_chart",
labelFallback: "Org Chart",
to: orgfrontUrl,
icon: Network,
isExternal: true,
},
);
} else {
// 일반 사용자(Tenant Member)도 조직도 메뉴를 볼 수 있도록 추가합니다.
filteredItems.splice(1, 0, {
filteredItems.splice(filteredItems.findIndex(i => i.to === "/users") + 1, 0, {
labelKey: "ui.admin.nav.org_chart",
labelFallback: "Org Chart",
to: orgfrontUrl,
@@ -254,7 +228,7 @@ function AppLayout() {
}
return filteredItems;
}, [mockRoleOverride, profile]);
}, [profile]);
const handleLogout = () => {
if (
@@ -299,21 +273,16 @@ function AppLayout() {
}
const rerenderDevelopmentShell = () => {
setDevelopmentRenderRevision((value) => value + 1);
// Re-render when locale changes
};
window.addEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
window.addEventListener(DEV_ROLE_CHANGED_EVENT, rerenderDevelopmentShell);
return () => {
window.removeEventListener(
LOCALE_CHANGED_EVENT,
rerenderDevelopmentShell,
);
window.removeEventListener(
DEV_ROLE_CHANGED_EVENT,
rerenderDevelopmentShell,
);
};
}, []);
@@ -494,7 +463,7 @@ function AppLayout() {
fallbackName: t("ui.shell.profile.unknown_name", "Unknown User"),
fallbackEmail: t("ui.shell.profile.unknown_email", "unknown@example.com"),
});
const profileRoleKey = mockRoleOverride || profile?.role || "user";
const profileRoleKey = profile?.role || "user";
const handleSessionExpiryToggle = () => {
setIsSessionExpiryEnabled((prev) => {
const next = !prev;
@@ -781,7 +750,6 @@ function AppLayout() {
<main className={shellLayoutClasses.mainMinWidth}>
<Outlet />
</main>
<RoleSwitcher />
</div>
</div>
);

View File

@@ -1,177 +0,0 @@
import { ChevronDown, ChevronUp, Wrench } from "lucide-react";
import type { FC } from "react";
import { useEffect, useState } from "react";
import { t } from "../../lib/i18n";
const DEV_ROLE_CHANGED_EVENT = "baron_dev_role_changed";
const RoleSwitcher: FC = () => {
const [currentRole, setCurrentRole] = useState<string>("");
const [isOverrideEnabled, setIsOverrideEnabled] = useState<boolean>(false);
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
return window.localStorage.getItem("RoleSwitcher-Collapsed") === "true";
});
useEffect(() => {
const savedRole = window.localStorage.getItem("X-Mock-Role");
const savedEnabled =
window.localStorage.getItem("X-Mock-Role-Enabled") === "true";
setIsOverrideEnabled(savedEnabled);
if (savedRole) {
setCurrentRole(savedRole);
}
}, []);
const toggleCollapse = () => {
const nextState = !isCollapsed;
setIsCollapsed(nextState);
window.localStorage.setItem("RoleSwitcher-Collapsed", String(nextState));
};
const switchRole = (role: string) => {
window.localStorage.setItem("X-Mock-Role", role);
window.localStorage.setItem("X-Mock-Role-Enabled", "true");
setCurrentRole(role);
setIsOverrideEnabled(true);
window.dispatchEvent(new Event(DEV_ROLE_CHANGED_EVENT));
};
const clearRoleOverride = () => {
window.localStorage.removeItem("X-Mock-Role-Enabled");
setIsOverrideEnabled(false);
window.dispatchEvent(new Event(DEV_ROLE_CHANGED_EVENT));
};
if (import.meta.env.MODE === "production") return null;
const roleLabels: Record<string, string> = {
super_admin: t("ui.admin.role.super_admin", "SUPER ADMIN"),
tenant_admin: t("ui.admin.role.tenant_admin", "TENANT ADMIN"),
rp_admin: t("ui.admin.role.rp_admin", "RP ADMIN"),
user: t("ui.admin.role.user", "TENANT MEMBER"),
};
return (
<div
style={{
position: "fixed",
bottom: "20px",
right: "20px",
zIndex: 9999,
background: "#1A1F2C",
color: "white",
padding: "8px 12px",
borderRadius: "8px",
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
display: "flex",
flexDirection: "column",
gap: isCollapsed ? "0" : "8px",
fontSize: "12px",
transition: "all 0.3s ease",
border: "1px solid #333",
}}
>
<button
type="button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "12px",
cursor: "pointer",
fontWeight: "bold",
paddingBottom: isCollapsed ? "0" : "4px",
borderBottom: isCollapsed ? "none" : "1px solid #444",
background: "transparent",
border: "none",
width: "100%",
color: "inherit",
textAlign: "inherit",
}}
onClick={toggleCollapse}
>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<Wrench size={14} className="text-blue-400" />
{!isCollapsed && (
<span>{t("ui.admin.dev_role_switcher", "DEV Role Switcher")}</span>
)}
{isCollapsed && (
<span style={{ fontSize: "10px", color: "#888" }}>
{isOverrideEnabled && currentRole
? currentRole.toUpperCase()
: "REAL ROLE"}
</span>
)}
</div>
{isCollapsed ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
{!isCollapsed && (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "6px",
marginTop: "4px",
}}
>
<button
type="button"
onClick={clearRoleOverride}
style={{
background: !isOverrideEnabled ? "#3b82f6" : "#333",
color: "white",
border: "none",
padding: "4px 8px",
borderRadius: "4px",
cursor: "pointer",
textAlign: "left",
transition: "background 0.2s",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span>
{t("ui.admin.dev_role_switcher_real", "실제 역할 사용")}
</span>
{!isOverrideEnabled && (
<span style={{ marginLeft: "8px" }}></span>
)}
</button>
{(["super_admin", "tenant_admin", "rp_admin", "user"] as const).map(
(role) => (
<button
key={role}
type="button"
onClick={() => switchRole(role)}
style={{
background: currentRole === role ? "#3b82f6" : "#333",
color: "white",
border: "none",
padding: "4px 8px",
borderRadius: "4px",
cursor: "pointer",
textAlign: "left",
transition: "background 0.2s",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span>
{roleLabels[role] ?? role.toUpperCase().replace("_", " ")}
</span>
{isOverrideEnabled && currentRole === role && (
<span style={{ marginLeft: "8px" }}></span>
)}
</button>
),
)}
</div>
)}
</div>
);
};
export default RoleSwitcher;

View File

@@ -24,8 +24,8 @@ function TenantDetailPage() {
});
const profileRole = normalizeAdminRole(profile?.role);
const canAccessSchema =
profileRole === "super_admin" || profileRole === "tenant_admin";
const canAccessSchema = profileRole === "super_admin";
Broadway
const showWorksmobileEntry = canShowWorksmobileEntry(tenantQuery.data);
const isPermissionsTab = location.pathname.includes("/permissions");

View File

@@ -528,11 +528,7 @@ function TenantListPage() {
return () => window.removeEventListener("message", onMessage);
}, [allTenants, scopePickerOpen]);
if (
profile &&
profileRole !== "super_admin" &&
profileRole !== "tenant_admin"
) {
if (profile && profileRole !== "super_admin") {
return (
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
<h3 className="text-lg font-bold">
@@ -545,13 +541,6 @@ function TenantListPage() {
);
}
if (
profileRole === "tenant_admin" &&
(profile?.manageableTenants?.length ?? 0) <= 1
) {
return null;
}
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedIds(deletableTenants.map((t) => t.id));

View File

@@ -34,8 +34,7 @@ export function TenantSchemaPage() {
});
const profileRole = normalizeAdminRole(profile?.role);
const canAccess =
profileRole === "super_admin" || profileRole === "tenant_admin";
const canAccess = profileRole === "super_admin";
const tenantQuery = useQuery({
queryKey: ["tenant", tenantId],

View File

@@ -819,7 +819,7 @@ function UserCreatePage() {
id="tenantSlug"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
{...register("tenantSlug")}
disabled={profile?.role === "tenant_admin"}
disabled={profileRole !== "super_admin"}
>
{nonHanmacFamilyTenants.map((tenant) => (
<option key={tenant.id} value={tenant.slug}>

View File

@@ -468,8 +468,7 @@ function UserDetailPage() {
});
const profileRole = normalizeAdminRole(profile?.role);
const isAdmin =
profileRole === "super_admin" || profileRole === "tenant_admin";
const isAdmin = profileRole === "super_admin";
const isSelf = Boolean(profile?.id && user?.id && profile.id === user.id);
const watchedStatus = watch("status");

View File

@@ -246,7 +246,7 @@ const UserListSearchControls = React.memo(function UserListSearchControls({
className="flex h-9 w-[160px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
value={selectedCompany}
onChange={(event) => onCompanyChange(event.target.value)}
disabled={profileRole === "tenant_admin"}
disabled={profileRole !== "super_admin"}
>
<option value="">{t("ui.common.all", "전체 테넌트")}</option>
{tenantOptions}

View File

@@ -28,14 +28,6 @@ apiClient.interceptors.request.use(async (config) => {
config.headers["X-Tenant-ID"] = tenantId;
}
// [Development Only] Inject Mock Role from RoleSwitcher
const isMockRoleEnabled =
window.localStorage.getItem("X-Mock-Role-Enabled") === "true";
const mockRole = window.localStorage.getItem("X-Mock-Role");
if (isMockRoleEnabled && mockRole) {
config.headers["X-Test-Role"] = mockRole;
}
return config;
});

View File

@@ -759,7 +759,6 @@ title = "Title"
[ui.admin]
brand = "Brand"
dev_role_switcher = "🛠 DEV Role Switcher"
title = "Admin Control"
[ui.admin.api_keys]

View File

@@ -764,7 +764,6 @@ title = "회원가입 완료"
[ui.admin]
brand = "Baron 로그인"
dev_role_switcher = "🛠 DEV Role Switcher"
title = "Admin Control"
[ui.admin.api_keys]

View File

@@ -772,8 +772,6 @@ title = ""
[ui.admin]
brand = ""
dev_role_switcher = ""
dev_role_switcher_real = ""
title = ""
[ui.admin.api_keys]