forked from baron/baron-sso
337 lines
12 KiB
TypeScript
337 lines
12 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { Key, Plus, Save, Trash2, Users } from "lucide-react";
|
|
import * as React from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { PageHeader } from "../../../../common/core/components/page";
|
|
import { Button } from "../../components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "../../components/ui/card";
|
|
import { Input } from "../../components/ui/input";
|
|
import { toast } from "../../components/ui/use-toast";
|
|
import {
|
|
fetchGlobalCustomClaimDefinitions,
|
|
type GlobalCustomClaimDefinition,
|
|
type GlobalCustomClaimPermission,
|
|
updateGlobalCustomClaimDefinitions,
|
|
} from "../../lib/adminApi";
|
|
import { t } from "../../lib/i18n";
|
|
|
|
type ClaimDraft = GlobalCustomClaimDefinition & { id: string };
|
|
|
|
const valueTypes: GlobalCustomClaimDefinition["valueType"][] = [
|
|
"text",
|
|
"number",
|
|
"boolean",
|
|
"array",
|
|
"object",
|
|
"date",
|
|
"datetime",
|
|
];
|
|
|
|
const permissions: GlobalCustomClaimPermission[] = [
|
|
"admin_only",
|
|
"user_and_admin",
|
|
];
|
|
|
|
function toDrafts(items: GlobalCustomClaimDefinition[]): ClaimDraft[] {
|
|
return items.map((item, index) => ({
|
|
id: `${item.key || "claim"}-${index}`,
|
|
key: item.key,
|
|
label: item.label,
|
|
valueType: item.valueType || "text",
|
|
readPermission: item.readPermission || "admin_only",
|
|
writePermission: item.writePermission || "admin_only",
|
|
description: item.description || "",
|
|
}));
|
|
}
|
|
|
|
function toDefinitions(drafts: ClaimDraft[]): GlobalCustomClaimDefinition[] {
|
|
return drafts
|
|
.map((draft) => normalizeClaimDraftPermissions(draft))
|
|
.map((draft) => ({
|
|
key: draft.key.trim(),
|
|
label: draft.label.trim(),
|
|
valueType: draft.valueType,
|
|
readPermission: draft.readPermission,
|
|
writePermission: draft.writePermission,
|
|
description: draft.description?.trim(),
|
|
}))
|
|
.filter((draft) => draft.key.length > 0);
|
|
}
|
|
|
|
function normalizeClaimDraftPermissions(draft: ClaimDraft): ClaimDraft {
|
|
if (draft.writePermission !== "user_and_admin") {
|
|
return draft;
|
|
}
|
|
return {
|
|
...draft,
|
|
readPermission: "user_and_admin",
|
|
};
|
|
}
|
|
|
|
function permissionLabel(permission: GlobalCustomClaimPermission) {
|
|
return permission === "user_and_admin"
|
|
? t(
|
|
"ui.common.custom_claim_permission.user_and_admin",
|
|
"사용자 및 관리자 가능",
|
|
)
|
|
: t("ui.common.custom_claim_permission.admin_only", "관리자만 가능");
|
|
}
|
|
|
|
export default function GlobalCustomClaimsPage() {
|
|
const queryClient = useQueryClient();
|
|
const [drafts, setDrafts] = React.useState<ClaimDraft[]>([]);
|
|
|
|
const query = useQuery({
|
|
queryKey: ["global-custom-claim-definitions"],
|
|
queryFn: fetchGlobalCustomClaimDefinitions,
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
if (query.data) {
|
|
setDrafts(toDrafts(query.data.items));
|
|
}
|
|
}, [query.data]);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: updateGlobalCustomClaimDefinitions,
|
|
onSuccess: (data) => {
|
|
queryClient.setQueryData(["global-custom-claim-definitions"], data);
|
|
toast.success(t("msg.info.saved_success", "저장되었습니다."));
|
|
},
|
|
onError: () => {
|
|
toast.error(t("err.common.unknown", "오류가 발생했습니다."));
|
|
},
|
|
});
|
|
|
|
const addClaim = () => {
|
|
setDrafts((current) => [
|
|
...current,
|
|
{
|
|
id: `global-claim-${Date.now()}`,
|
|
key: "",
|
|
label: "",
|
|
valueType: "text",
|
|
readPermission: "admin_only",
|
|
writePermission: "admin_only",
|
|
description: "",
|
|
},
|
|
]);
|
|
};
|
|
|
|
const updateClaim = (id: string, patch: Partial<ClaimDraft>) => {
|
|
setDrafts((current) =>
|
|
current.map((draft) =>
|
|
draft.id === id
|
|
? normalizeClaimDraftPermissions({ ...draft, ...patch })
|
|
: draft,
|
|
),
|
|
);
|
|
};
|
|
|
|
const removeClaim = (id: string) => {
|
|
setDrafts((current) => current.filter((draft) => draft.id !== id));
|
|
};
|
|
|
|
const saveClaims = () => {
|
|
mutation.mutate({ items: toDefinitions(drafts) });
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
titleAs="h2"
|
|
icon={<Key size={20} />}
|
|
title={t(
|
|
"ui.admin.users.global_custom_claims.title",
|
|
"전역 Claim 설정",
|
|
)}
|
|
description={t(
|
|
"msg.admin.users.global_custom_claims.description",
|
|
"모든 RP에 공통 적용할 사용자 claim 정의와 사용자의 읽기/쓰기 권한 기본값을 관리합니다. 쓰기 허용 시 읽기도 자동으로 허용됩니다.",
|
|
)}
|
|
actions={
|
|
<>
|
|
<Button asChild variant="outline" size="sm" className="h-9">
|
|
<Link to="/users">
|
|
<Users size={16} />
|
|
{t("ui.admin.users.list.title", "사용자 관리")}
|
|
</Link>
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-9 gap-2"
|
|
onClick={addClaim}
|
|
>
|
|
<Plus size={16} />
|
|
{t("ui.common.add", "추가")}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
className="h-9 gap-2"
|
|
disabled={mutation.isPending}
|
|
onClick={saveClaims}
|
|
>
|
|
<Save size={16} />
|
|
{t("ui.common.save", "저장")}
|
|
</Button>
|
|
</>
|
|
}
|
|
/>
|
|
|
|
<Card className="bg-[var(--color-panel)]">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">
|
|
{t(
|
|
"ui.admin.users.global_custom_claims.registry",
|
|
"Global Claim Registry",
|
|
)}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{t(
|
|
"msg.admin.users.global_custom_claims.registry",
|
|
"정의된 claim key만 사용자 상세의 전역 claim 값 관리 대상이 됩니다. 읽기/쓰기는 관리자 권한이 아니라 사용자가 본인 claim 값을 조회하거나 수정할 수 있는지에 대한 설정입니다.",
|
|
)}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{query.isLoading ? (
|
|
<div className="py-12 text-center text-sm text-muted-foreground">
|
|
{t("ui.common.loading", "로딩 중...")}
|
|
</div>
|
|
) : drafts.length === 0 ? (
|
|
<div className="rounded-lg border border-dashed py-12 text-center text-sm text-muted-foreground">
|
|
{t(
|
|
"msg.admin.users.global_custom_claims.empty",
|
|
"정의된 전역 claim이 없습니다.",
|
|
)}
|
|
</div>
|
|
) : (
|
|
drafts.map((claim) => (
|
|
<div
|
|
key={claim.id}
|
|
className="grid gap-3 rounded-md border bg-background p-3 lg:grid-cols-[minmax(160px,0.8fr)_minmax(160px,0.8fr)_130px_160px_160px_minmax(220px,1fr)_40px]"
|
|
>
|
|
<Input
|
|
value={claim.key}
|
|
name={`global-claim-definition-key-${claim.id}`}
|
|
className="font-mono text-xs"
|
|
placeholder="claim_key"
|
|
data-testid={`global-claim-definition-key-${claim.key || claim.id}`}
|
|
onChange={(event) =>
|
|
updateClaim(claim.id, { key: event.target.value })
|
|
}
|
|
/>
|
|
<Input
|
|
value={claim.label}
|
|
name={`global-claim-definition-label-${claim.id}`}
|
|
placeholder={t(
|
|
"ui.admin.users.global_custom_claims.label_placeholder",
|
|
"표시 이름",
|
|
)}
|
|
data-testid={`global-claim-definition-label-${claim.key || claim.id}`}
|
|
onChange={(event) =>
|
|
updateClaim(claim.id, { label: event.target.value })
|
|
}
|
|
/>
|
|
<select
|
|
aria-label={t(
|
|
"ui.admin.users.global_custom_claims.value_type",
|
|
"Claim 타입",
|
|
)}
|
|
value={claim.valueType}
|
|
name={`global-claim-definition-value-type-${claim.id}`}
|
|
className="h-10 rounded-md border border-input bg-background px-3 text-sm"
|
|
onChange={(event) =>
|
|
updateClaim(claim.id, {
|
|
valueType: event.target
|
|
.value as GlobalCustomClaimDefinition["valueType"],
|
|
})
|
|
}
|
|
>
|
|
{valueTypes.map((valueType) => (
|
|
<option key={valueType} value={valueType}>
|
|
{valueType}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
aria-label={t(
|
|
"ui.admin.users.global_custom_claims.read_permission",
|
|
"읽기 권한",
|
|
)}
|
|
value={claim.readPermission}
|
|
name={`global-claim-definition-read-permission-${claim.id}`}
|
|
className="h-10 rounded-md border border-input bg-background px-3 text-sm"
|
|
data-testid={`global-claim-definition-read-permission-${claim.key || claim.id}`}
|
|
onChange={(event) =>
|
|
updateClaim(claim.id, {
|
|
readPermission: event.target
|
|
.value as GlobalCustomClaimPermission,
|
|
})
|
|
}
|
|
>
|
|
{permissions.map((permission) => (
|
|
<option key={permission} value={permission}>
|
|
{permissionLabel(permission)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
aria-label={t(
|
|
"ui.admin.users.global_custom_claims.write_permission",
|
|
"쓰기 권한",
|
|
)}
|
|
value={claim.writePermission}
|
|
name={`global-claim-definition-write-permission-${claim.id}`}
|
|
className="h-10 rounded-md border border-input bg-background px-3 text-sm"
|
|
data-testid={`global-claim-definition-write-permission-${claim.key || claim.id}`}
|
|
onChange={(event) =>
|
|
updateClaim(claim.id, {
|
|
writePermission: event.target
|
|
.value as GlobalCustomClaimPermission,
|
|
})
|
|
}
|
|
>
|
|
{permissions.map((permission) => (
|
|
<option key={permission} value={permission}>
|
|
{permissionLabel(permission)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<Input
|
|
value={claim.description || ""}
|
|
name={`global-claim-definition-description-${claim.id}`}
|
|
placeholder={t(
|
|
"ui.admin.users.global_custom_claims.description_placeholder",
|
|
"설명",
|
|
)}
|
|
onChange={(event) =>
|
|
updateClaim(claim.id, { description: event.target.value })
|
|
}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => removeClaim(claim.id)}
|
|
>
|
|
<Trash2 size={16} />
|
|
</Button>
|
|
</div>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|