1
0
forked from baron/baron-sso

클레임 토글 및 비활성화 저장 처리 추가

This commit is contained in:
2026-06-17 17:34:36 +09:00
parent 28dad91b1a
commit 7145e703d7
5 changed files with 540 additions and 446 deletions

View File

@@ -334,6 +334,47 @@ describe("ClientGeneralPage RP claims", () => {
); );
}); });
it("clears saved RP claims when custom claims are disabled", async () => {
const { container } = await renderPage();
const claimToggle = Array.from(
container.querySelectorAll<HTMLButtonElement>('[role="switch"]'),
).find((button) =>
(button.getAttribute("aria-label") ?? "").includes("커스텀 클레임 사용"),
);
expect(claimToggle).toBeDefined();
expect(claimToggle?.getAttribute("aria-checked")).toBe("true");
await act(async () => {
claimToggle?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flush();
expect(claimToggle?.getAttribute("aria-checked")).toBe("false");
const saveButton = Array.from(container.querySelectorAll("button")).find(
(button) =>
button.textContent?.includes("저장") ||
button.textContent?.includes("Save"),
);
expect(saveButton).toBeDefined();
await act(async () => {
saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flush();
expect(updateClientMock).toHaveBeenCalledWith(
"client-claims",
expect.objectContaining({
metadata: expect.objectContaining({
id_token_claims_enabled: false,
id_token_claims: [],
}),
}),
);
});
it("preserves tenants scope mandatory state when tenant access restriction is off", async () => { it("preserves tenants scope mandatory state when tenant access restriction is off", async () => {
fetchClientMock.mockResolvedValue( fetchClientMock.mockResolvedValue(
makeClientDetail("old_claim", { makeClientDetail("old_claim", {

View File

@@ -633,6 +633,7 @@ function ClientGeneralPage() {
const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]); const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]);
const [autoLoginSupported, setAutoLoginSupported] = useState(false); const [autoLoginSupported, setAutoLoginSupported] = useState(false);
const [autoLoginUrl, setAutoLoginUrl] = useState(""); const [autoLoginUrl, setAutoLoginUrl] = useState("");
const [idTokenClaimsEnabled, setIdTokenClaimsEnabled] = useState(false);
// Public Key Registration States // Public Key Registration States
const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] = const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] =
@@ -956,7 +957,13 @@ function ClientGeneralPage() {
), ),
); );
} }
setIdTokenClaims(readIdTokenClaimsMetadata(metadata)); const savedIdTokenClaims = readIdTokenClaimsMetadata(metadata);
setIdTokenClaims(savedIdTokenClaims);
setIdTokenClaimsEnabled(
typeof metadata.id_token_claims_enabled === "boolean"
? metadata.id_token_claims_enabled
: savedIdTokenClaims.length > 0,
);
}, [data, normalizeScopesForTenantAccess]); }, [data, normalizeScopesForTenantAccess]);
const securityProfile: SecurityProfile = const securityProfile: SecurityProfile =
@@ -1089,6 +1096,7 @@ function ClientGeneralPage() {
}; };
const addIdTokenClaim = () => { const addIdTokenClaim = () => {
setIdTokenClaimsEnabled(true);
setIdTokenClaims((current) => [ setIdTokenClaims((current) => [
...current, ...current,
createIdTokenClaimItem(`claim-${Date.now()}`), createIdTokenClaimItem(`claim-${Date.now()}`),
@@ -1253,6 +1261,7 @@ function ClientGeneralPage() {
} }
const claimValidationErrors: string[] = []; const claimValidationErrors: string[] = [];
if (idTokenClaimsEnabled) {
const seenClaimKeys = new Set<string>(); const seenClaimKeys = new Set<string>();
for (const claim of normalizedIdTokenClaimItems) { for (const claim of normalizedIdTokenClaimItems) {
if (!claim.key) { if (!claim.key) {
@@ -1289,12 +1298,13 @@ function ClientGeneralPage() {
claimValidationErrors.push(defaultValueError); claimValidationErrors.push(defaultValueError);
} }
} }
}
validationErrors.push(...claimValidationErrors); validationErrors.push(...claimValidationErrors);
const hasValidationErrors = validationErrors.length > 0; const hasValidationErrors = validationErrors.length > 0;
const idTokenClaimPreview = buildIdTokenClaimsPreview( const idTokenClaimPreview = idTokenClaimsEnabled
normalizedIdTokenClaimItems, ? buildIdTokenClaimsPreview(normalizedIdTokenClaimItems)
); : [];
const idTokenClaimPreviewJson = JSON.stringify(idTokenClaimPreview, null, 2); const idTokenClaimPreviewJson = JSON.stringify(idTokenClaimPreview, null, 2);
const tenantOptions: TenantSummary[] = tenantData?.items ?? []; const tenantOptions: TenantSummary[] = tenantData?.items ?? [];
const selectedAllowedTenants = allowedTenantIds const selectedAllowedTenants = allowedTenantIds
@@ -1435,7 +1445,8 @@ function ClientGeneralPage() {
auto_login_supported: autoLoginSupported, auto_login_supported: autoLoginSupported,
auto_login_url: autoLoginSupported ? trimmedAutoLoginUrl : undefined, auto_login_url: autoLoginSupported ? trimmedAutoLoginUrl : undefined,
structured_scopes: normalizedScopes, structured_scopes: normalizedScopes,
id_token_claims: normalizedIdTokenClaims, id_token_claims_enabled: idTokenClaimsEnabled,
id_token_claims: idTokenClaimsEnabled ? normalizedIdTokenClaims : [],
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod, token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
headless_login_enabled: headlessLoginEnabled, headless_login_enabled: headlessLoginEnabled,
headless_token_endpoint_auth_method: headlessLoginEnabled headless_token_endpoint_auth_method: headlessLoginEnabled
@@ -1908,7 +1919,7 @@ function ClientGeneralPage() {
</CardDescription> </CardDescription>
</div> </div>
<Button <Button
variant="outline" variant="default"
size="sm" size="sm"
onClick={addScope} onClick={addScope}
className="gap-2" className="gap-2"
@@ -2315,6 +2326,7 @@ function ClientGeneralPage() {
<Card className="glass-panel"> <Card className="glass-panel">
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between"> <div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div className="space-y-3">
<div className="space-y-1"> <div className="space-y-1">
<CardTitle className="text-xl font-bold"> <CardTitle className="text-xl font-bold">
{t( {t(
@@ -2329,16 +2341,49 @@ function ClientGeneralPage() {
)} )}
</CardDescription> </CardDescription>
</div> </div>
{idTokenClaimsEnabled ? (
<Button <Button
size="sm"
onClick={addIdTokenClaim} onClick={addIdTokenClaim}
className="gap-2" className="gap-2"
disabled={isGeneralSettingsReadOnly} disabled={isGeneralSettingsReadOnly}
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
{t("ui.dev.clients.general.id_token_claims.add", "Claim 추가")} {t(
"ui.dev.clients.general.id_token_claims.add",
"Claim 추가",
)}
</Button> </Button>
) : null}
</div>
<div className="flex shrink-0 items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="space-y-0.5 text-right">
<p className="text-sm font-semibold">
{idTokenClaimsEnabled
? t("ui.common.enabled", "사용")
: t("ui.common.disabled", "사용 안 함")}
</p>
<p className="text-xs text-muted-foreground">
{t(
"ui.dev.clients.general.id_token_claims.enabled",
"커스텀 클레임 사용",
)}
</p>
</div>
<Switch
checked={idTokenClaimsEnabled}
onCheckedChange={setIdTokenClaimsEnabled}
id="custom-claims-enabled"
aria-label={t(
"ui.dev.clients.general.id_token_claims.enabled",
"커스텀 클레임 사용",
)}
disabled={isGeneralSettingsReadOnly}
/>
</div>
</div> </div>
</CardHeader> </CardHeader>
{idTokenClaimsEnabled ? (
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.45fr)_minmax(360px,0.75fr)]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,1.45fr)_minmax(360px,0.75fr)]">
<div className="space-y-3"> <div className="space-y-3">
@@ -2652,7 +2697,10 @@ function ClientGeneralPage() {
)} )}
> >
{timeZoneOptions.map((timeZone) => ( {timeZoneOptions.map((timeZone) => (
<option key={timeZone} value={timeZone}> <option
key={timeZone}
value={timeZone}
>
{timeZone} {timeZone}
</option> </option>
))} ))}
@@ -2727,6 +2775,7 @@ function ClientGeneralPage() {
</div> </div>
</div> </div>
</CardContent> </CardContent>
) : null}
</Card> </Card>
{/* 4. Tenant Access Restriction */} {/* 4. Tenant Access Restriction */}
@@ -2994,6 +3043,7 @@ function ClientGeneralPage() {
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
{autoLoginSupported ? (
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="auto-login-url" className="text-sm font-semibold"> <Label htmlFor="auto-login-url" className="text-sm font-semibold">
@@ -3006,7 +3056,6 @@ function ClientGeneralPage() {
id="auto-login-url" id="auto-login-url"
value={autoLoginUrl} value={autoLoginUrl}
onChange={(event) => setAutoLoginUrl(event.target.value)} onChange={(event) => setAutoLoginUrl(event.target.value)}
disabled={!autoLoginSupported}
aria-invalid={!hasValidAutoLoginUrl} aria-invalid={!hasValidAutoLoginUrl}
className={!hasValidAutoLoginUrl ? "border-destructive" : ""} className={!hasValidAutoLoginUrl ? "border-destructive" : ""}
placeholder={t( placeholder={t(
@@ -3030,6 +3079,7 @@ function ClientGeneralPage() {
) : null} ) : null}
</div> </div>
</CardContent> </CardContent>
) : null}
</Card> </Card>
{/* 6. Security Settings */} {/* 6. Security Settings */}

View File

@@ -1649,6 +1649,7 @@ picker_hint_with_count = "{{count}} tenants selected."
[ui.dev.clients.general.id_token_claims] [ui.dev.clients.general.id_token_claims]
title = "Custom Claims" title = "Custom Claims"
add = "Add Claim" add = "Add Claim"
enabled = "Custom Claims Enabled"
preview_title = "Saved JSON Preview" preview_title = "Saved JSON Preview"
namespace_label = "Claim namespace" namespace_label = "Claim namespace"
namespace_top_level = "top-level" namespace_top_level = "top-level"

View File

@@ -1648,6 +1648,7 @@ picker_hint_with_count = "현재 {{count}}개가 선택되어 있습니다."
[ui.dev.clients.general.id_token_claims] [ui.dev.clients.general.id_token_claims]
title = "커스텀 클레임" title = "커스텀 클레임"
add = "Claim 추가" add = "Claim 추가"
enabled = "커스텀 클레임 사용"
preview_title = "저장 JSON 미리보기" preview_title = "저장 JSON 미리보기"
namespace_label = "Claim 네임스페이스" namespace_label = "Claim 네임스페이스"
namespace_top_level = "top-level" namespace_top_level = "top-level"

View File

@@ -1698,6 +1698,7 @@ picker_hint_with_count = ""
[ui.dev.clients.general.id_token_claims] [ui.dev.clients.general.id_token_claims]
title = "" title = ""
add = "" add = ""
enabled = ""
preview_title = "" preview_title = ""
namespace_label = "" namespace_label = ""
namespace_top_level = "" namespace_top_level = ""