1
0
forked from baron/baron-sso

offline 스코프 제거, rp_claims 값 표준화

This commit is contained in:
2026-06-11 14:50:26 +09:00
parent f60b15a17b
commit c495e9119b
26 changed files with 1034 additions and 300 deletions

View File

@@ -13,7 +13,7 @@ import {
Upload,
X,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAuth } from "react-oidc-context";
import { Link, useNavigate, useParams } from "react-router-dom";
import { PageHeader } from "../../../../common/core/components/page";
@@ -58,6 +58,12 @@ import { fetchMe, type UserProfile } from "../auth/authApi";
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
import { ClientDetailTabs } from "./ClientDetailTabs";
import { AllowedTenantBadge } from "./components/AllowedTenantBadge";
import {
claimDateTimeValueToInputString,
dateTimeInputToUnixSeconds,
getBrowserTimeZone,
getSupportedTimeZones,
} from "./rpClaimDateTime";
interface ScopeItem {
id: string;
@@ -84,6 +90,7 @@ interface IdTokenClaimItem {
namespace: ClaimNamespace;
key: string;
value: string;
timeZone: string;
valueType: ClaimValueType;
nullable: boolean;
readPermission: CustomClaimPermission;
@@ -171,6 +178,7 @@ function createIdTokenClaimItem(id: string): IdTokenClaimItem {
namespace: "rp_claims",
key: "",
value: "",
timeZone: getBrowserTimeZone(),
valueType: "text",
nullable: false,
readPermission: "admin_only",
@@ -215,23 +223,32 @@ function readIdTokenClaimsMetadata(
}
const keyValue = typeof record.key === "string" ? record.key : "";
const rawValue = record.value;
const valueValue =
typeof rawValue === "string"
? rawValue
: rawValue == null
? ""
: JSON.stringify(rawValue);
const valueTypeValue =
typeof record.valueType === "string" &&
isClaimValueType(record.valueType)
? record.valueType
: "text";
const timeZoneValue = getBrowserTimeZone();
const valueValue =
valueTypeValue === "date" || valueTypeValue === "datetime"
? claimDateTimeValueToInputString(
rawValue,
"",
valueTypeValue,
timeZoneValue,
)
: typeof rawValue === "string"
? rawValue
: rawValue == null
? ""
: JSON.stringify(rawValue);
return normalizeIdTokenClaimPermissions({
id: `claim-${index + 1}`,
namespace: namespaceValue,
key: keyValue,
value: valueValue,
timeZone: timeZoneValue,
valueType: valueTypeValue,
nullable: record.nullable === true,
readPermission: isCustomClaimPermission(record.readPermission)
@@ -249,11 +266,21 @@ function normalizeClaimPreviewValue(
value: string,
valueType: ClaimValueType,
nullable: boolean,
timeZone: string,
): unknown {
const trimmed = value.trim();
if (nullable && trimmed === "") {
return null;
}
if (valueType === "date" || valueType === "datetime") {
if (trimmed === "") return "";
const unixSeconds = dateTimeInputToUnixSeconds(
trimmed,
valueType,
timeZone,
);
return unixSeconds ?? trimmed;
}
if (valueType === "number" || valueType === "float") {
if (trimmed === "") return "";
const parsed = Number(trimmed);
@@ -320,6 +347,19 @@ function isValidDateTimeInputValue(value: string) {
return !Number.isNaN(date.getTime());
}
function normalizedClaimValue(claim: IdTokenClaimItem): string | number {
const value = claim.value.trim();
if (claim.valueType !== "date" && claim.valueType !== "datetime") {
return value;
}
if (value === "") {
return value;
}
return (
dateTimeInputToUnixSeconds(value, claim.valueType, claim.timeZone) ?? value
);
}
function claimDefaultValueValidationError(claim: IdTokenClaimItem) {
const value = claim.value.trim();
if (value === "") {
@@ -440,6 +480,7 @@ function buildIdTokenClaimsPreview(
item.value,
item.valueType,
item.nullable,
item.timeZone,
);
}
@@ -612,6 +653,11 @@ function ClientGeneralPage() {
},
]);
const [idTokenClaims, setIdTokenClaims] = useState<IdTokenClaimItem[]>([]);
const browserTimeZone = useMemo(() => getBrowserTimeZone(), []);
const timeZoneOptions = useMemo(
() => getSupportedTimeZones(browserTimeZone),
[browserTimeZone],
);
const tenantScopeDescription = t(
"msg.dev.clients.scopes.tenant",
@@ -985,13 +1031,20 @@ function ClientGeneralPage() {
"허용 알고리즘: {{algorithms}}",
{ algorithms: HEADLESS_LOGIN_ALLOWED_ALGORITHMS.join(", ") },
);
const normalizedIdTokenClaims = idTokenClaims.map((claim) =>
const normalizedIdTokenClaimItems = idTokenClaims.map((claim) =>
normalizeIdTokenClaimPermissions({
...claim,
key: claim.key.trim(),
value: claim.value.trim(),
}),
);
const normalizedIdTokenClaims = normalizedIdTokenClaimItems.map((claim) => {
const { timeZone: _timeZone, value: _value, ...persisted } = claim;
return {
...persisted,
value: normalizedClaimValue(claim),
};
});
if (headlessLoginEnabled) {
if (!trimmedJwksUri) {
@@ -1048,7 +1101,7 @@ function ClientGeneralPage() {
const claimValidationErrors: string[] = [];
const seenClaimKeys = new Set<string>();
for (const claim of normalizedIdTokenClaims) {
for (const claim of normalizedIdTokenClaimItems) {
if (!claim.key) {
claimValidationErrors.push(
t(
@@ -1087,7 +1140,7 @@ function ClientGeneralPage() {
const hasValidationErrors = validationErrors.length > 0;
const idTokenClaimPreview = buildIdTokenClaimsPreview(
normalizedIdTokenClaims,
normalizedIdTokenClaimItems,
);
const idTokenClaimPreviewJson = JSON.stringify(idTokenClaimPreview, null, 2);
const normalizedTenantSearch = tenantSearch.trim().toLowerCase();
@@ -2529,32 +2582,59 @@ function ClientGeneralPage() {
<option value="false">false</option>
</select>
) : (
<Input
type={claimDefaultInputType(claim.valueType)}
inputMode={claimDefaultInputMode(
claim.valueType,
<div className="flex flex-col gap-2">
<Input
type={claimDefaultInputType(claim.valueType)}
inputMode={claimDefaultInputMode(
claim.valueType,
)}
pattern={claimDefaultInputPattern(
claim.valueType,
)}
value={claim.value}
onChange={(e) =>
updateIdTokenClaim(
claim.id,
"value",
e.target.value,
)
}
className="h-9 font-mono text-xs"
placeholder={t(
"ui.dev.clients.general.id_token_claims.value_placeholder",
"Enter the default value",
)}
disabled={isGeneralSettingsReadOnly}
aria-invalid={
defaultValueError ? true : undefined
}
/>
{(claim.valueType === "date" ||
claim.valueType === "datetime") && (
<select
value={claim.timeZone}
onChange={(event) =>
updateIdTokenClaim(
claim.id,
"timeZone",
event.target.value,
)
}
className="h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-xs shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
disabled={isGeneralSettingsReadOnly}
aria-label={t(
"ui.dev.clients.general.id_token_claims.timezone_label",
"Claim 기본값 시간대",
)}
>
{timeZoneOptions.map((timeZone) => (
<option key={timeZone} value={timeZone}>
{timeZone}
</option>
))}
</select>
)}
pattern={claimDefaultInputPattern(
claim.valueType,
)}
value={claim.value}
onChange={(e) =>
updateIdTokenClaim(
claim.id,
"value",
e.target.value,
)
}
className="h-9 font-mono text-xs"
placeholder={t(
"ui.dev.clients.general.id_token_claims.value_placeholder",
"Enter the default value",
)}
disabled={isGeneralSettingsReadOnly}
aria-invalid={
defaultValueError ? true : undefined
}
/>
</div>
)}
{defaultValueError && (
<p className="mt-1 text-xs text-destructive">