forked from baron/baron-sso
offline 스코프 제거, rp_claims 값 표준화
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user