forked from baron/baron-sso
클레임 토글 및 비활성화 저장 처리 추가
This commit is contained in:
@@ -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", {
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user