diff --git a/adminfront/src/app/routes.test.tsx b/adminfront/src/app/routes.test.tsx
index 08d10367..ec98de66 100644
--- a/adminfront/src/app/routes.test.tsx
+++ b/adminfront/src/app/routes.test.tsx
@@ -22,6 +22,12 @@ describe("admin routes", () => {
expect(matches?.at(-1)?.route.path).toBe("system/projections/users");
});
+ it("registers the super-admin data integrity management route", () => {
+ const matches = matchRoutes(adminRoutes, "/system/data-integrity");
+
+ expect(matches?.at(-1)?.route.path).toBe("system/data-integrity");
+ });
+
it("keeps protected admin pages behind an auth guard before mounting the layout", () => {
const rootRoute = adminRoutes.find((route) => route.path === "/");
const protectedShellRoute = rootRoute?.children?.[0];
diff --git a/adminfront/src/app/routes.tsx b/adminfront/src/app/routes.tsx
index df3df402..7c17cf9e 100644
--- a/adminfront/src/app/routes.tsx
+++ b/adminfront/src/app/routes.tsx
@@ -8,6 +8,7 @@ import AuthCallbackPage from "../features/auth/AuthCallbackPage";
import AuthGuard from "../features/auth/AuthGuard";
import AuthPage from "../features/auth/AuthPage";
import LoginPage from "../features/auth/LoginPage";
+import DataIntegrityPage from "../features/integrity/DataIntegrityPage";
import GlobalOverviewPage from "../features/overview/GlobalOverviewPage";
import UserProjectionPage from "../features/projections/UserProjectionPage";
import { TenantAdminsAndOwnersTab } from "../features/tenants/routes/TenantAdminsAndOwnersTab";
@@ -66,6 +67,7 @@ export const adminRoutes: RouteObject[] = [
{ path: "api-keys", element: },
{ path: "api-keys/new", element: },
{ path: "system/projections/users", element: },
+ { path: "system/data-integrity", element: },
],
},
],
diff --git a/adminfront/src/components/auth/RoleGuard.tsx b/adminfront/src/components/auth/RoleGuard.tsx
index 6fd8e51c..6872747b 100644
--- a/adminfront/src/components/auth/RoleGuard.tsx
+++ b/adminfront/src/components/auth/RoleGuard.tsx
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import type * as React from "react";
import { fetchMe } from "../../lib/adminApi";
+import { normalizeAdminRole } from "../../lib/roles";
interface RoleGuardProps {
children: React.ReactNode;
@@ -29,8 +30,10 @@ export function RoleGuard({
if (isLoading) return null;
- const userRole = profile?.role || "user";
- const hasAccess = roles.includes(userRole);
+ const userRole = normalizeAdminRole(profile?.role);
+ const hasAccess = roles
+ .map((role) => normalizeAdminRole(role))
+ .includes(userRole);
if (!hasAccess) {
return <>{fallback}>;
diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx
index 7f8d894a..5950ddf4 100644
--- a/adminfront/src/components/layout/AppLayout.tsx
+++ b/adminfront/src/components/layout/AppLayout.tsx
@@ -10,6 +10,7 @@ import {
Moon,
Network,
NotebookTabs,
+ ShieldCheck,
ShieldHalf,
Sun,
User as UserIcon,
@@ -32,6 +33,7 @@ import {
import { buildAuthenticatedOrgChartUrl } from "../../features/users/orgChartPicker";
import { fetchMe } from "../../lib/adminApi";
import { t } from "../../lib/i18n";
+import { isSuperAdminRole } from "../../lib/roles";
import {
shouldAttemptSlidingSessionRenew,
shouldAttemptUnlimitedSessionRenew,
@@ -150,7 +152,7 @@ function AppLayout() {
._IS_TEST_MODE === true;
const effectiveRole = mockRoleOverride || profile?.role;
- const isSuperAdmin = isTest || effectiveRole === "super_admin";
+ const isSuperAdmin = isTest || isSuperAdminRole(effectiveRole);
const isTenantAdmin = effectiveRole === "tenant_admin";
const manageableCount = profile?.manageableTenants?.length ?? 0;
const orgfrontUrl = buildAuthenticatedOrgChartUrl(
@@ -180,6 +182,11 @@ function AppLayout() {
to: "/system/projections/users",
icon: Database,
});
+ filteredItems.splice(5, 0, {
+ label: "ui.admin.nav.data_integrity",
+ to: "/system/data-integrity",
+ icon: ShieldCheck,
+ });
} else if (isTenantAdmin || manageableCount > 0) {
if (manageableCount <= 1 && profile?.tenantId) {
filteredItems.splice(1, 0, {
diff --git a/adminfront/src/features/integrity/DataIntegrityPage.test.tsx b/adminfront/src/features/integrity/DataIntegrityPage.test.tsx
new file mode 100644
index 00000000..0b9fd597
--- /dev/null
+++ b/adminfront/src/features/integrity/DataIntegrityPage.test.tsx
@@ -0,0 +1,81 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { fetchDataIntegrityReport, fetchMe } from "../../lib/adminApi";
+import DataIntegrityPage from "./DataIntegrityPage";
+
+let currentRole = "super_admin";
+
+vi.mock("../../lib/adminApi", () => ({
+ fetchMe: vi.fn(async () => ({ role: currentRole })),
+ fetchDataIntegrityReport: vi.fn(async () => ({
+ status: "fail",
+ checkedAt: "2026-05-14T00:00:00Z",
+ summary: {
+ totalChecks: 2,
+ passed: 1,
+ warnings: 0,
+ failures: 1,
+ },
+ sections: [
+ {
+ key: "tenant_integrity",
+ label: "테넌트 정합성",
+ status: "fail",
+ checks: [
+ {
+ key: "duplicate_tenant_slugs",
+ label: "중복 테넌트 slug",
+ description:
+ "active tenant slug의 대소문자 무시 중복을 검사합니다.",
+ status: "fail",
+ severity: "error",
+ count: 1,
+ },
+ ],
+ },
+ ],
+ })),
+}));
+
+function renderPage() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ return render(
+
+
+ ,
+ );
+}
+
+describe("DataIntegrityPage", () => {
+ beforeEach(() => {
+ currentRole = "super_admin";
+ vi.clearAllMocks();
+ });
+
+ it("renders integrity report for super_admin", async () => {
+ renderPage();
+
+ expect(await screen.findByText("데이터 정합성 검증")).toBeInTheDocument();
+ expect(await screen.findByText("테넌트 정합성")).toBeInTheDocument();
+ expect(screen.getByText("중복 테넌트 slug")).toBeInTheDocument();
+ expect(screen.getAllByText("1").length).toBeGreaterThan(0);
+ expect(fetchDataIntegrityReport).toHaveBeenCalledTimes(1);
+ });
+
+ it("blocks non-super admins", async () => {
+ currentRole = "tenant_admin";
+
+ renderPage();
+
+ expect(await screen.findByText("접근 권한이 없습니다")).toBeInTheDocument();
+ expect(fetchMe).toHaveBeenCalled();
+ expect(fetchDataIntegrityReport).not.toHaveBeenCalled();
+ });
+});
diff --git a/adminfront/src/features/integrity/DataIntegrityPage.tsx b/adminfront/src/features/integrity/DataIntegrityPage.tsx
new file mode 100644
index 00000000..502338a2
--- /dev/null
+++ b/adminfront/src/features/integrity/DataIntegrityPage.tsx
@@ -0,0 +1,209 @@
+import { useQuery } from "@tanstack/react-query";
+import {
+ AlertTriangle,
+ CheckCircle2,
+ Database,
+ ShieldAlert,
+} from "lucide-react";
+import { RoleGuard } from "../../components/auth/RoleGuard";
+import { Badge } from "../../components/ui/badge";
+import { Button } from "../../components/ui/button";
+import {
+ type DataIntegrityCheck,
+ type DataIntegrityStatus,
+ fetchDataIntegrityReport,
+} from "../../lib/adminApi";
+
+function statusLabel(status: DataIntegrityStatus) {
+ switch (status) {
+ case "pass":
+ return "정상";
+ case "warning":
+ return "주의";
+ case "fail":
+ return "실패";
+ default:
+ return status;
+ }
+}
+
+function statusBadgeVariant(status: DataIntegrityStatus) {
+ switch (status) {
+ case "pass":
+ return "success";
+ case "warning":
+ return "warning";
+ default:
+ return "warning";
+ }
+}
+
+function formatDateTime(value?: string) {
+ if (!value) return "-";
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) return value;
+ return new Intl.DateTimeFormat("ko-KR", {
+ dateStyle: "medium",
+ timeStyle: "medium",
+ }).format(date);
+}
+
+function CheckIcon({ check }: { check: DataIntegrityCheck }) {
+ if (check.status === "pass") {
+ return ;
+ }
+ if (check.status === "warning") {
+ return ;
+ }
+ return ;
+}
+
+function DataIntegrityContent() {
+ const { data, isLoading, isError, error, refetch, isFetching } = useQuery({
+ queryKey: ["data-integrity-report"],
+ queryFn: fetchDataIntegrityReport,
+ });
+
+ return (
+
+
+
+
System
+
+ 데이터 정합성 검증
+
+
+
+
+
+ {isError ? (
+
+ {(error as Error)?.message || "정합성 리포트를 불러오지 못했습니다."}
+
+ ) : null}
+
+
+
+
+
+
+
+
+
Read model integrity
+
+ Ory SoT를 덮어쓰지 않고 backend DB read model의 이상 징후만
+ 확인합니다.
+
+
+
+ {data ? (
+
+ {statusLabel(data.status)}
+
+ ) : null}
+
+
+ {isLoading ? (
+ 불러오는 중
+ ) : (
+
+
+
- 검사 항목
+ -
+ {data?.summary.totalChecks ?? 0}
+
+
+
+
정상
+
+ {data?.summary.passed ?? 0}
+
+
+
+
실패 건수
+
+ {data?.summary.failures ?? 0}
+
+
+
+
검사 시각
+
+ {formatDateTime(data?.checkedAt)}
+
+
+
+ )}
+
+
+
+ {(data?.sections ?? []).map((section) => (
+
+
+
{section.label}
+
+ {statusLabel(section.status)}
+
+
+
+ {section.checks.map((check) => (
+
+
+
+
+
{check.label}
+
+ {check.description}
+
+
+
+
+
+ {statusLabel(check.status)}
+
+
+ {check.count}
+
+
+
+ ))}
+
+
+ ))}
+
+
+ );
+}
+
+export default function DataIntegrityPage() {
+ return (
+
+
+ 접근 권한이 없습니다
+
+ 이 화면은 super_admin 권한으로만 접근할 수 있습니다.
+
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/adminfront/src/features/overview/GlobalOverviewPage.test.tsx b/adminfront/src/features/overview/GlobalOverviewPage.test.tsx
index 80e33caf..15a2bd36 100644
--- a/adminfront/src/features/overview/GlobalOverviewPage.test.tsx
+++ b/adminfront/src/features/overview/GlobalOverviewPage.test.tsx
@@ -3,14 +3,20 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type React from "react";
import { MemoryRouter } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
-import { fetchAdminRPUsageDaily } from "../../lib/adminApi";
+import {
+ fetchAdminRPUsageDaily,
+ fetchDataIntegrityReport,
+} from "../../lib/adminApi";
import AuthPage from "../auth/AuthPage";
import GlobalOverviewPage from "./GlobalOverviewPage";
+let currentRole = "super_admin";
+
vi.mock("../../lib/adminApi", () => ({
- fetchMe: vi.fn(async () => ({ role: "super_admin" })),
+ fetchMe: vi.fn(async () => ({ role: currentRole })),
fetchAdminOverviewStats: vi.fn(async () => ({
totalTenants: 10,
+ totalUsers: 152,
oidcClients: 3,
auditEvents24h: 18,
})),
@@ -93,6 +99,30 @@ vi.mock("../../lib/adminApi", () => ({
},
],
})),
+ fetchDataIntegrityReport: vi.fn(async () => ({
+ status: "fail",
+ checkedAt: "2026-05-14T00:00:00Z",
+ summary: {
+ totalChecks: 5,
+ passed: 4,
+ warnings: 0,
+ failures: 1,
+ },
+ sections: [
+ {
+ key: "tenant_integrity",
+ label: "테넌트 정합성",
+ status: "pass",
+ checks: [],
+ },
+ {
+ key: "user_integrity",
+ label: "사용자 정합성",
+ status: "fail",
+ checks: [],
+ },
+ ],
+ })),
}));
function renderWithProviders(ui: React.ReactElement) {
@@ -112,6 +142,7 @@ function renderWithProviders(ui: React.ReactElement) {
describe("admin overview and auth guard pages", () => {
beforeEach(() => {
+ currentRole = "super_admin";
vi.clearAllMocks();
});
@@ -132,15 +163,18 @@ describe("admin overview and auth guard pages", () => {
expect(screen.queryByText("ReBAC 권한 검증 도구")).not.toBeInTheDocument();
});
- it("renders overview summary metrics from the admin stats API", async () => {
+ it("renders overview tenant count from the fully fetched tenant list", async () => {
renderWithProviders();
expect(
(await screen.findByText("전체 테넌트 수")).parentElement,
- ).toHaveTextContent("10");
+ ).toHaveTextContent("3");
expect(screen.getByText("OIDC 클라이언트").parentElement).toHaveTextContent(
"3",
);
+ expect(screen.getByText("전체 사용자 수").parentElement).toHaveTextContent(
+ "152",
+ );
expect(screen.getByText("24시간 이벤트").parentElement).toHaveTextContent(
"18",
);
@@ -172,6 +206,26 @@ describe("admin overview and auth guard pages", () => {
expect(await screen.findAllByText("05월")).not.toHaveLength(0);
});
+ it("shows the latest integrity summary at the bottom for super admins only", async () => {
+ renderWithProviders();
+
+ expect(await screen.findByText("정합성 최종 검증")).toBeInTheDocument();
+ expect(screen.getByText("실패 1건")).toBeInTheDocument();
+ expect(screen.getByText("테넌트 정합성")).toBeInTheDocument();
+ expect(screen.getByText("사용자 정합성")).toBeInTheDocument();
+ expect(fetchDataIntegrityReport).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not fetch or show the integrity summary for non-super admins", async () => {
+ currentRole = "tenant_admin";
+
+ renderWithProviders();
+
+ await screen.findByText("회사별 앱별 로그인요청/기타 요청 현황");
+ expect(screen.queryByText("정합성 최종 검증")).not.toBeInTheDocument();
+ expect(fetchDataIntegrityReport).not.toHaveBeenCalled();
+ });
+
it("moves the permission checker to the auth guard page and removes mock guardrails", () => {
renderWithProviders();
diff --git a/adminfront/src/features/overview/GlobalOverviewPage.tsx b/adminfront/src/features/overview/GlobalOverviewPage.tsx
index d3c4dad7..28431105 100644
--- a/adminfront/src/features/overview/GlobalOverviewPage.tsx
+++ b/adminfront/src/features/overview/GlobalOverviewPage.tsx
@@ -1,7 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import {
Activity,
+ AlertTriangle,
BarChart3,
+ CheckCircle2,
Database,
ShieldCheck,
Users,
@@ -9,12 +11,14 @@ import {
import { type ReactNode, useMemo, useState } from "react";
import { RoleGuard } from "../../components/auth/RoleGuard";
import {
+ type DataIntegrityStatus,
type RPUsageDailyMetric,
type RPUsagePeriod,
type TenantSummary,
fetchAdminOverviewStats,
fetchAdminRPUsageDaily,
fetchAllTenants,
+ fetchDataIntegrityReport,
} from "../../lib/adminApi";
import { t } from "../../lib/i18n";
@@ -151,6 +155,102 @@ function OverviewMetric({
);
}
+function formatOverviewDateTime(value?: string) {
+ if (!value) return "-";
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) return value;
+ return new Intl.DateTimeFormat("ko-KR", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(date);
+}
+
+function integrityStatusText(status: DataIntegrityStatus) {
+ switch (status) {
+ case "pass":
+ return "정상";
+ case "warning":
+ return "주의";
+ default:
+ return "실패";
+ }
+}
+
+function integrityStatusClass(status: DataIntegrityStatus) {
+ switch (status) {
+ case "pass":
+ return "text-emerald-700 dark:text-emerald-300";
+ case "warning":
+ return "text-amber-700 dark:text-amber-300";
+ default:
+ return "text-destructive";
+ }
+}
+
+function IntegrityOverviewSummary() {
+ const { data, isError } = useQuery({
+ queryKey: ["admin-overview-integrity"],
+ queryFn: fetchDataIntegrityReport,
+ retry: false,
+ });
+
+ if (isError) {
+ return (
+
+
+
+
정합성 최종 검증 결과를 불러오지 못했습니다.
+
+
+ );
+ }
+
+ if (!data) {
+ return null;
+ }
+
+ return (
+
+
+
+ {data.status === "pass" ? (
+
+ ) : (
+
+ )}
+
정합성 최종 검증
+
+
+
+ {integrityStatusText(data.status)}
+
+ 실패 {data.summary.failures}건
+
+ {formatOverviewDateTime(data.checkedAt)}
+
+
+
+
+ {data.sections.map((section) => (
+
+ {section.label}
+
+ {integrityStatusText(section.status)}
+
+
+ ))}
+
+
+ );
+}
+
function RPUsageMixedChart({
rows,
filters,
@@ -371,6 +471,7 @@ function GlobalOverviewPage() {
retry: false,
});
const stats = statsQuery.data;
+ const visibleTenantCount = tenantsQuery.data?.items.length;
const usageRows = usageQuery.data?.items ?? [];
const metric = (value: number | undefined) =>
value === undefined ? "-" : value.toLocaleString();
@@ -444,7 +545,7 @@ function GlobalOverviewPage() {
"ui.admin.overview.summary.total_tenants",
"전체 테넌트 수",
)}
- value={metric(stats?.totalTenants)}
+ value={metric(visibleTenantCount ?? stats?.totalTenants)}
/>
}
@@ -454,6 +555,11 @@ function GlobalOverviewPage() {
)}
value={metric(stats?.oidcClients)}
/>
+ }
+ label={t("ui.admin.overview.summary.total_users", "전체 사용자 수")}
+ value={metric(stats?.totalUsers)}
+ />
}
@@ -491,6 +597,10 @@ function GlobalOverviewPage() {
period={period}
/>
)}
+
+
+
+
);
}
diff --git a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx
index 87901297..01ce0e0c 100644
--- a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx
@@ -5,6 +5,7 @@ import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import { fetchMe, fetchTenant } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";
+import { normalizeAdminRole } from "../../../lib/roles";
export function canShowWorksmobileEntry(tenant?: {
id?: string;
@@ -30,8 +31,9 @@ function TenantDetailPage() {
queryFn: fetchMe,
});
+ const profileRole = normalizeAdminRole(profile?.role);
const canAccessSchema =
- profile?.role === "super_admin" || profile?.role === "tenant_admin";
+ profileRole === "super_admin" || profileRole === "tenant_admin";
const showWorksmobileEntry = canShowWorksmobileEntry(tenantQuery.data);
const isPermissionsTab = location.pathname.includes("/permissions");
diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx
index 23b6f609..8739e474 100644
--- a/adminfront/src/features/tenants/routes/TenantListPage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx
@@ -74,6 +74,7 @@ import {
importTenantsCSV,
} from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";
+import { normalizeAdminRole } from "../../../lib/roles";
import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree";
import {
filterNonHanmacFamilyTenants,
@@ -262,10 +263,11 @@ function TenantListPage() {
queryKey: ["me"],
queryFn: fetchMe,
});
+ const profileRole = normalizeAdminRole(profile?.role);
// Redirect tenant_admin ONLY if they have one or fewer manageable tenants in the list
React.useEffect(() => {
- if (profile?.role === "tenant_admin") {
+ if (profile && profileRole === "tenant_admin") {
const manageableCount = profile.manageableTenants?.length ?? 0;
if (
(manageableCount === 1 || manageableCount === 0) &&
@@ -274,7 +276,7 @@ function TenantListPage() {
navigate(`/tenants/${profile.tenantId}`, { replace: true });
}
}
- }, [profile, navigate]);
+ }, [profile, profileRole, navigate]);
const query = useInfiniteQuery({
queryKey: ["tenants", "lazy"],
@@ -289,9 +291,9 @@ function TenantListPage() {
getNextPageParam: (lastPage) =>
lastPage.nextCursor || lastPage.next_cursor || undefined,
enabled:
- profile?.role === "super_admin" ||
- (profile?.role === "tenant_admin" &&
- (profile.manageableTenants?.length ?? 0) > 1),
+ profileRole === "super_admin" ||
+ (profileRole === "tenant_admin" &&
+ (profile?.manageableTenants?.length ?? 0) > 1),
});
const deleteMutation = useMutation({
@@ -356,8 +358,8 @@ function TenantListPage() {
if (
profile &&
- profile.role !== "super_admin" &&
- profile.role !== "tenant_admin"
+ profileRole !== "super_admin" &&
+ profileRole !== "tenant_admin"
) {
return (
@@ -372,7 +374,8 @@ function TenantListPage() {
}
if (
- profile?.role === "tenant_admin" &&
+ profile &&
+ profileRole === "tenant_admin" &&
(profile.manageableTenants?.length ?? 0) <= 1
) {
return null;
@@ -396,7 +399,7 @@ function TenantListPage() {
return rawTenants.find((tenant) => tenant.slug === "hanmac-family")?.id;
}, [rawTenants]);
const allTenants = React.useMemo(() => {
- if (profile?.role === "super_admin") {
+ if (profileRole === "super_admin") {
return rawTenants;
}
if (
@@ -406,7 +409,7 @@ function TenantListPage() {
return rawTenants;
}
return filterNonHanmacFamilyTenants(rawTenants, hanmacFamilyTenantId);
- }, [hanmacFamilyTenantId, profile, rawTenants]);
+ }, [hanmacFamilyTenantId, profile, profileRole, rawTenants]);
const importParentOptionGroups =
buildTenantImportParentOptionGroups(allTenants);
const tenantSortResolvers = React.useMemo<
diff --git a/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx
index dca46f15..5c3d6822 100644
--- a/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx
@@ -16,6 +16,7 @@ import { Label } from "../../../components/ui/label";
import { toast } from "../../../components/ui/use-toast";
import { fetchMe, fetchTenant, updateTenant } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";
+import { normalizeAdminRole } from "../../../lib/roles";
export type SchemaFieldType =
| "text"
@@ -101,8 +102,9 @@ export function TenantSchemaPage() {
queryFn: fetchMe,
});
+ const profileRole = normalizeAdminRole(profile?.role);
const canAccess =
- profile?.role === "super_admin" || profile?.role === "tenant_admin";
+ profileRole === "super_admin" || profileRole === "tenant_admin";
const tenantQuery = useQuery({
queryKey: ["tenant", tenantId],
diff --git a/adminfront/src/features/users/UserCreatePage.tsx b/adminfront/src/features/users/UserCreatePage.tsx
index d9e6bb48..70c65dc5 100644
--- a/adminfront/src/features/users/UserCreatePage.tsx
+++ b/adminfront/src/features/users/UserCreatePage.tsx
@@ -48,6 +48,7 @@ import {
fetchTenant,
} from "../../lib/adminApi";
import { t } from "../../lib/i18n";
+import { isSuperAdminRole } from "../../lib/roles";
import {
type OrgChartTenantSelection,
buildAuthenticatedOrgChartTenantPickerUrl,
@@ -642,7 +643,7 @@ function UserCreatePage() {
id="role"
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("role")}
- disabled={profile?.role !== "super_admin"}
+ disabled={!isSuperAdminRole(profile?.role)}
>