forked from baron/baron-sso
adminfront: 글로벌 사이드바에 독립적인 '권한 부여' 메뉴 및 전용 대시보드 페이지 추가 완료
This commit is contained in:
@@ -18,6 +18,7 @@ import TenantListPage from "../features/tenants/routes/TenantListPage";
|
|||||||
import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage";
|
import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage";
|
||||||
import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage";
|
import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage";
|
||||||
import { TenantFineGrainedPermissionsTab } from "../features/tenants/routes/TenantFineGrainedPermissionsTab";
|
import { TenantFineGrainedPermissionsTab } from "../features/tenants/routes/TenantFineGrainedPermissionsTab";
|
||||||
|
import { TenantFineGrainedPermissionsPage } from "../features/tenants/routes/TenantFineGrainedPermissionsPage";
|
||||||
import { TenantWorksmobilePage } from "../features/tenants/routes/TenantWorksmobilePage";
|
import { TenantWorksmobilePage } from "../features/tenants/routes/TenantWorksmobilePage";
|
||||||
import TenantUserGroupsTab from "../features/user-groups/routes/TenantUserGroupsTab";
|
import TenantUserGroupsTab from "../features/user-groups/routes/TenantUserGroupsTab";
|
||||||
import GlobalCustomClaimsPage from "../features/users/GlobalCustomClaimsPage";
|
import GlobalCustomClaimsPage from "../features/users/GlobalCustomClaimsPage";
|
||||||
@@ -52,6 +53,7 @@ export const adminRoutes: RouteObject[] = [
|
|||||||
{ path: "tenants", element: <TenantListPage /> },
|
{ path: "tenants", element: <TenantListPage /> },
|
||||||
{ path: "tenants/new", element: <TenantCreatePage /> },
|
{ path: "tenants/new", element: <TenantCreatePage /> },
|
||||||
{ path: "worksmobile", element: <TenantWorksmobilePage /> },
|
{ path: "worksmobile", element: <TenantWorksmobilePage /> },
|
||||||
|
{ path: "permissions-direct", element: <TenantFineGrainedPermissionsPage /> },
|
||||||
{
|
{
|
||||||
path: "tenants/:tenantId",
|
path: "tenants/:tenantId",
|
||||||
element: <TenantDetailPage />,
|
element: <TenantDetailPage />,
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ const staticNavItems: ShellSidebarNavItem[] = [
|
|||||||
to: "/users",
|
to: "/users",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
labelKey: "ui.admin.nav.permissions_direct",
|
||||||
|
labelFallback: "Direct Permissions",
|
||||||
|
to: "/permissions-direct",
|
||||||
|
icon: ShieldCheck,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
labelKey: "ui.admin.nav.auth_guard",
|
labelKey: "ui.admin.nav.auth_guard",
|
||||||
labelFallback: "Auth Guard",
|
labelFallback: "Auth Guard",
|
||||||
@@ -208,6 +214,7 @@ function AppLayout() {
|
|||||||
});
|
});
|
||||||
const filteredItems = items.filter((item) => {
|
const filteredItems = items.filter((item) => {
|
||||||
if (item.to === "/api-keys") return isSuperAdmin;
|
if (item.to === "/api-keys") return isSuperAdmin;
|
||||||
|
if (item.to === "/permissions-direct") return isSuperAdmin || _manageableCount > 0;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { fetchAllTenants, fetchMe } from "../../../lib/adminApi";
|
||||||
|
import { t } from "../../../lib/i18n";
|
||||||
|
import { TenantFineGrainedPermissionsTab } from "./TenantFineGrainedPermissionsTab";
|
||||||
|
import { ShieldCheck } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "../../../components/ui/card";
|
||||||
|
|
||||||
|
export function TenantFineGrainedPermissionsPage() {
|
||||||
|
const [selectedTenantId, setSelectedTenantId] = useState("");
|
||||||
|
|
||||||
|
const { data: profile } = useQuery({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: fetchMe,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSuperAdmin = profile?.role === "super_admin";
|
||||||
|
|
||||||
|
const tenantsQuery = useQuery({
|
||||||
|
queryKey: ["tenants", "list-all"],
|
||||||
|
queryFn: () => fetchAllTenants(),
|
||||||
|
enabled: isSuperAdmin,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tenants = isSuperAdmin
|
||||||
|
? (tenantsQuery.data?.items ?? [])
|
||||||
|
: (profile?.manageableTenants ?? []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-2">
|
||||||
|
<ShieldCheck className="h-8 w-8 text-primary" />
|
||||||
|
{t("ui.admin.nav.permissions_direct", "권한 부여")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"msg.admin.permissions_direct.description",
|
||||||
|
"선택한 테넌트의 각 기능별 세부 조회 및 수정 권한을 지정하고 부여합니다.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="border-none shadow-sm bg-[var(--color-panel)]">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle className="text-lg font-semibold">
|
||||||
|
{t("ui.admin.permissions_direct.select_tenant", "대상 테넌트 선택")}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t(
|
||||||
|
"msg.admin.permissions_direct.select_tenant_desc",
|
||||||
|
"권한을 부여할 대상 테넌트를 리스트에서 선택해 주세요.",
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<select
|
||||||
|
value={selectedTenantId}
|
||||||
|
onChange={(e) => setSelectedTenantId(e.target.value)}
|
||||||
|
className="flex h-10 w-full max-w-[360px] rounded-md border border-input bg-background 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"
|
||||||
|
>
|
||||||
|
<option value="">{t("ui.admin.permissions_direct.placeholder", "-- 테넌트 선택 --")}</option>
|
||||||
|
{tenants.map((tenant) => (
|
||||||
|
<option key={tenant.id} value={tenant.id}>
|
||||||
|
{tenant.name} ({tenant.slug})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{selectedTenantId ? (
|
||||||
|
<TenantFineGrainedPermissionsTab tenantIdProp={selectedTenantId} />
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-dashed border-border p-12 text-center text-muted-foreground">
|
||||||
|
{t("msg.admin.permissions_direct.select_prompt", "상단에서 테넌트를 선택하면 세부 권한 격리 설정 격자가 노출됩니다.")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -45,9 +45,13 @@ import {
|
|||||||
import { t } from "../../../lib/i18n";
|
import { t } from "../../../lib/i18n";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
|
|
||||||
export function TenantFineGrainedPermissionsTab() {
|
interface TenantFineGrainedPermissionsTabProps {
|
||||||
|
tenantIdProp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrainedPermissionsTabProps = {}) {
|
||||||
const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
|
const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
|
||||||
const tenantId = tenantIdParam ?? "";
|
const tenantId = tenantIdProp || tenantIdParam || "";
|
||||||
const { hasPermission } = useTenantPermission(tenantId);
|
const { hasPermission } = useTenantPermission(tenantId);
|
||||||
const isWritable = hasPermission("manage_admins");
|
const isWritable = hasPermission("manage_admins");
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
Reference in New Issue
Block a user