1
0
forked from baron/baron-sso

chore: consolidate local integration changes

This commit is contained in:
2026-06-09 21:03:05 +09:00
parent aa2848c3b6
commit 1341f07ef9
158 changed files with 10995 additions and 1490 deletions

View File

@@ -60,10 +60,14 @@ import {
TabsTrigger,
} from "../../components/ui/tabs";
import { toast } from "../../components/ui/use-toast";
import type { PasswordPolicyResponse } from "../../lib/adminApi";
import type {
GlobalCustomClaimDefinition,
PasswordPolicyResponse,
} from "../../lib/adminApi";
import {
deleteUser,
fetchAllTenants,
fetchGlobalCustomClaimDefinitions,
fetchMe,
fetchPasswordPolicy,
fetchTenant,
@@ -111,6 +115,25 @@ type PickerTarget = { kind: "appointment"; index: number };
type AppointmentDraft = UserAppointment & {
draftId: string;
};
type GlobalCustomClaimType =
| "text"
| "number"
| "boolean"
| "array"
| "object"
| "date"
| "datetime";
type CustomClaimPermission = "admin_only" | "user_and_admin";
type GlobalCustomClaimRow = {
id: string;
key: string;
label: string;
value: string;
valueType: GlobalCustomClaimType;
readPermission: CustomClaimPermission;
writePermission: CustomClaimPermission;
description?: string;
};
const PASSWORD_RESET_MIN_LENGTH = 12;
@@ -179,6 +202,74 @@ function createDraftId() {
return globalThis.crypto?.randomUUID?.() ?? `appointment-${Date.now()}`;
}
function createGlobalCustomClaimRows(
metadata: Record<string, unknown>,
definitions: GlobalCustomClaimDefinition[],
): GlobalCustomClaimRow[] {
const rawClaims = isMetadataRecord(metadata.global_custom_claims)
? metadata.global_custom_claims
: {};
return definitions.map((definition, index) => {
const value = rawClaims[definition.key];
return {
id: `${definition.key}-${index}`,
key: definition.key,
label: definition.label,
description: definition.description,
value:
typeof value === "string"
? value
: value == null
? ""
: JSON.stringify(value),
valueType: definition.valueType,
readPermission: definition.readPermission,
writePermission: definition.writePermission,
};
});
}
function globalCustomClaimInputType(valueType: GlobalCustomClaimType) {
if (valueType === "date") {
return "date";
}
if (valueType === "datetime") {
return "datetime-local";
}
if (valueType === "number") {
return "number";
}
return "text";
}
function globalCustomClaimRowsToMetadata(rows: GlobalCustomClaimRow[]) {
const claims: Record<string, unknown> = {};
const types: Record<string, GlobalCustomClaimType> = {};
const permissions: Record<
string,
{
readPermission: CustomClaimPermission;
writePermission: CustomClaimPermission;
}
> = {};
for (const row of rows) {
const key = row.key.trim();
if (!key) {
continue;
}
claims[key] = row.value.trim();
types[key] = row.valueType;
permissions[key] = {
readPermission: row.readPermission,
writePermission: row.writePermission,
};
}
return { claims, types, permissions };
}
async function resolveTenantSelection(
selection: OrgChartTenantSelection,
tenants: TenantSummary[],
@@ -408,6 +499,9 @@ function UserDetailPage() {
const [additionalAppointments, setAdditionalAppointments] = React.useState<
AppointmentDraft[]
>([]);
const [globalCustomClaimRows, setGlobalCustomClaimRows] = React.useState<
GlobalCustomClaimRow[]
>([]);
const [pickerTarget, setPickerTarget] = React.useState<PickerTarget | null>(
null,
);
@@ -449,6 +543,14 @@ function UserDetailPage() {
queryKey: ["password-policy"],
queryFn: fetchPasswordPolicy,
});
const { data: globalCustomClaimDefinitionsData } = useQuery({
queryKey: ["global-custom-claim-definitions"],
queryFn: fetchGlobalCustomClaimDefinitions,
});
const globalCustomClaimDefinitions = React.useMemo(
() => globalCustomClaimDefinitionsData?.items ?? [],
[globalCustomClaimDefinitionsData?.items],
);
const {
register,
@@ -757,6 +859,9 @@ function UserDetailPage() {
? "hanmac"
: "external";
setUserCategory(resolvedUserCategory);
setGlobalCustomClaimRows(
createGlobalCustomClaimRows(metadata, globalCustomClaimDefinitions),
);
const familyFallbackTenants = [
...(user.joinedTenants ?? []),
...(user.tenant ? [user.tenant] : []),
@@ -814,7 +919,14 @@ function UserDetailPage() {
: [],
);
}
}, [hanmacFamilyTenantId, personalTenant, tenants, user, reset]);
}, [
globalCustomClaimDefinitions,
hanmacFamilyTenantId,
personalTenant,
tenants,
user,
reset,
]);
const mutation = useMutation({
mutationFn: (data: UserUpdateRequest) => updateUser(userId, data),
@@ -963,6 +1075,29 @@ function UserDetailPage() {
}
};
const updateGlobalCustomClaimRow = (
id: string,
patch: Partial<GlobalCustomClaimRow>,
) => {
setGlobalCustomClaimRows((current) =>
current.map((row) => (row.id === id ? { ...row, ...patch } : row)),
);
};
const saveGlobalCustomClaims = () => {
const { claims, types, permissions } = globalCustomClaimRowsToMetadata(
globalCustomClaimRows,
);
mutation.mutate({
metadata: {
...((user?.metadata as Record<string, unknown> | undefined) ?? {}),
global_custom_claims: claims,
global_custom_claim_types: types,
global_custom_claim_permissions: permissions,
},
});
};
const userAffiliatedTenants = React.useMemo(() => {
const joined = user?.joinedTenants || [];
const primary = user?.tenant;
@@ -1121,6 +1256,17 @@ function UserDetailPage() {
<Building2 size={16} className="mr-2" />
{t("ui.admin.users.detail.tabs.tenants", "테넌트 프로필")}
</TabsTrigger>
<TabsTrigger
value="customClaims"
className="px-6 py-2 rounded-lg data-[state=active]:shadow-sm"
data-testid="global-custom-claim-tab"
>
<Key size={16} className="mr-2" />
{t(
"ui.admin.users.detail.tabs.custom_claims",
"전역 Custom Claims",
)}
</TabsTrigger>
<TabsTrigger
value="security"
className="px-6 py-2 rounded-lg data-[state=active]:shadow-sm"
@@ -1793,6 +1939,135 @@ function UserDetailPage() {
</Button>
</div>
</TabsContent>
<TabsContent
value="customClaims"
className="space-y-6 mt-0 animate-in fade-in slide-in-from-bottom-2"
>
<Card className="border-none shadow-sm bg-[var(--color-panel)] rounded-2xl">
<CardHeader className="pb-4">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<CardTitle className="text-lg flex items-center gap-2">
<Key size={18} className="text-primary" />
{t(
"ui.admin.users.detail.custom_claims.title",
"사용자별 Custom Claim 값",
)}
</CardTitle>
<CardDescription>
{t(
"msg.admin.users.detail.custom_claims.description",
"전역으로 정의된 custom claim의 이 사용자 값을 관리합니다. Claim 정의 추가와 타입 변경은 전역 설정 화면에서만 가능합니다.",
)}
</CardDescription>
</div>
<Button
type="button"
variant="outline"
className="gap-2"
onClick={() => navigate("/users/custom-claims")}
>
<Key className="h-4 w-4" />
{t(
"ui.admin.users.global_custom_claims.manage_definitions",
"전역 정의 관리",
)}
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4 p-8">
{globalCustomClaimRows.length === 0 ? (
<div className="rounded-2xl border-2 border-dashed bg-muted/5 py-12 text-center text-sm text-muted-foreground">
{t(
"msg.admin.users.detail.custom_claims.empty",
"전역으로 정의된 custom claim이 없습니다.",
)}
</div>
) : (
<div className="space-y-3">
{globalCustomClaimRows.map((claim) => (
<div
key={claim.id}
className="grid gap-3 lg:grid-cols-[minmax(180px,0.8fr)_130px_150px_160px_minmax(220px,1fr)]"
>
<div className="flex h-10 items-center rounded-md border bg-muted/30 px-3 font-mono text-xs">
{claim.key}
</div>
<Badge
variant="muted"
className="h-10 justify-center rounded-md px-3 font-mono text-xs"
>
{claim.valueType}
</Badge>
<Badge
variant="muted"
className="h-10 justify-center rounded-md px-3 text-xs"
>
{claim.readPermission === "user_and_admin"
? t(
"ui.common.custom_claim_permission.user_and_admin",
"사용자 및 관리자 가능",
)
: t(
"ui.common.custom_claim_permission.admin_only",
"관리자만 가능",
)}
</Badge>
<Badge
variant="muted"
className="h-10 justify-center rounded-md px-3 text-xs"
>
{claim.writePermission === "user_and_admin"
? t(
"ui.common.custom_claim_permission.user_and_admin",
"사용자 및 관리자 가능",
)
: t(
"ui.common.custom_claim_permission.admin_only",
"관리자만 가능",
)}
</Badge>
<Input
type={globalCustomClaimInputType(claim.valueType)}
value={claim.value}
onChange={(event) =>
updateGlobalCustomClaimRow(claim.id, {
value: event.target.value,
})
}
className="font-mono text-xs"
data-testid={`global-custom-claim-value-${claim.key || claim.id}`}
placeholder="claim value"
/>
</div>
))}
</div>
)}
</CardContent>
</Card>
<div className="flex justify-end pt-4">
<Button
type="button"
disabled={mutation.isPending}
onClick={saveGlobalCustomClaims}
className="px-12 h-12 rounded-xl shadow-lg transition-all hover:scale-105"
>
{mutation.isPending ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<Save className="mr-2 h-5 w-5" />
)}
<span className="text-base font-bold">
{t(
"ui.admin.users.detail.custom_claims.save",
"사용자 Claim 값 저장",
)}
</span>
</Button>
</div>
</TabsContent>
</form>
<TabsContent