forked from baron/baron-sso
커스텀 클레임 ui/ux 추가
This commit is contained in:
@@ -59,6 +59,17 @@ interface ScopeItem {
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
type ClaimNamespace = "top_level" | "rp_claims";
|
||||
type ClaimValueType = "text" | "number" | "boolean" | "array" | "object";
|
||||
|
||||
interface IdTokenClaimItem {
|
||||
id: string;
|
||||
namespace: ClaimNamespace;
|
||||
key: string;
|
||||
value: string;
|
||||
valueType: ClaimValueType;
|
||||
}
|
||||
|
||||
type SecurityProfile = "private" | "pkce";
|
||||
type TokenEndpointAuthMethod =
|
||||
| "none"
|
||||
@@ -111,6 +122,142 @@ function readMetadataString(
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function isClaimNamespace(value: string): value is ClaimNamespace {
|
||||
return value === "top_level" || value === "rp_claims";
|
||||
}
|
||||
|
||||
function isClaimValueType(value: string): value is ClaimValueType {
|
||||
return (
|
||||
value === "text" ||
|
||||
value === "number" ||
|
||||
value === "boolean" ||
|
||||
value === "array" ||
|
||||
value === "object"
|
||||
);
|
||||
}
|
||||
|
||||
function createIdTokenClaimItem(id: string): IdTokenClaimItem {
|
||||
return {
|
||||
id,
|
||||
namespace: "top_level",
|
||||
key: "",
|
||||
value: "",
|
||||
valueType: "text",
|
||||
};
|
||||
}
|
||||
|
||||
function readIdTokenClaimsMetadata(
|
||||
metadata: Record<string, unknown>,
|
||||
): IdTokenClaimItem[] {
|
||||
const rawClaims = metadata.id_token_claims;
|
||||
if (!Array.isArray(rawClaims)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rawClaims
|
||||
.map((item, index) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const record = item as Record<string, unknown>;
|
||||
const namespaceValue =
|
||||
typeof record.namespace === "string" &&
|
||||
isClaimNamespace(record.namespace)
|
||||
? record.namespace
|
||||
: "top_level";
|
||||
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";
|
||||
|
||||
return {
|
||||
id: `claim-${index + 1}`,
|
||||
namespace: namespaceValue,
|
||||
key: keyValue,
|
||||
value: valueValue,
|
||||
valueType: valueTypeValue,
|
||||
};
|
||||
})
|
||||
.filter((item): item is IdTokenClaimItem => item !== null);
|
||||
}
|
||||
|
||||
function normalizeClaimPreviewValue(
|
||||
value: string,
|
||||
valueType: ClaimValueType,
|
||||
): unknown {
|
||||
const trimmed = value.trim();
|
||||
if (valueType === "number") {
|
||||
if (trimmed === "") return "";
|
||||
const parsed = Number(trimmed);
|
||||
return Number.isFinite(parsed) ? parsed : trimmed;
|
||||
}
|
||||
|
||||
if (valueType === "boolean") {
|
||||
return ["true", "1", "yes", "on"].includes(trimmed.toLowerCase());
|
||||
}
|
||||
|
||||
if (valueType === "array") {
|
||||
if (trimmed === "") return [];
|
||||
try {
|
||||
if (trimmed.startsWith("[")) {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
return Array.isArray(parsed) ? parsed : [parsed];
|
||||
}
|
||||
} catch {
|
||||
// Fall through to comma-separated parsing.
|
||||
}
|
||||
return trimmed
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (valueType === "object") {
|
||||
if (trimmed === "") return {};
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
return parsed;
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function buildIdTokenClaimsPreview(
|
||||
items: IdTokenClaimItem[],
|
||||
): Record<string, unknown> {
|
||||
const preview: Record<string, unknown> = {};
|
||||
const rpClaims: Record<string, unknown> = {};
|
||||
|
||||
for (const item of items) {
|
||||
const key = item.key.trim();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = item.namespace === "rp_claims" ? rpClaims : preview;
|
||||
target[key] = normalizeClaimPreviewValue(item.value, item.valueType);
|
||||
}
|
||||
|
||||
if (Object.keys(rpClaims).length > 0) {
|
||||
preview.rp_claims = rpClaims;
|
||||
}
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
function isValidUrl(value: string): boolean {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
@@ -192,6 +339,7 @@ function ClientGeneralPage() {
|
||||
mandatory: false,
|
||||
},
|
||||
]);
|
||||
const [idTokenClaims, setIdTokenClaims] = useState<IdTokenClaimItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
@@ -287,6 +435,7 @@ function ClientGeneralPage() {
|
||||
),
|
||||
);
|
||||
}
|
||||
setIdTokenClaims(readIdTokenClaimsMetadata(metadata));
|
||||
}, [data]);
|
||||
|
||||
const securityProfile: SecurityProfile =
|
||||
@@ -436,6 +585,32 @@ function ClientGeneralPage() {
|
||||
);
|
||||
};
|
||||
|
||||
const addIdTokenClaim = () => {
|
||||
setIdTokenClaims((current) => [
|
||||
...current,
|
||||
createIdTokenClaimItem(`claim-${Date.now()}`),
|
||||
]);
|
||||
};
|
||||
|
||||
const updateIdTokenClaim = <K extends keyof IdTokenClaimItem>(
|
||||
id: string,
|
||||
field: K,
|
||||
value: IdTokenClaimItem[K],
|
||||
) => {
|
||||
setIdTokenClaims((current) =>
|
||||
current.map((claim) => {
|
||||
if (claim.id !== id) {
|
||||
return claim;
|
||||
}
|
||||
return { ...claim, [field]: value };
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const removeIdTokenClaim = (id: string) => {
|
||||
setIdTokenClaims((current) => current.filter((claim) => claim.id !== id));
|
||||
};
|
||||
|
||||
const handleStatusChange = (nextStatus: ClientStatus) => {
|
||||
setStatus(nextStatus);
|
||||
const statusLabel =
|
||||
@@ -487,6 +662,11 @@ function ClientGeneralPage() {
|
||||
"허용 알고리즘: {{algorithms}}",
|
||||
{ algorithms: HEADLESS_LOGIN_ALLOWED_ALGORITHMS.join(", ") },
|
||||
);
|
||||
const normalizedIdTokenClaims = idTokenClaims.map((claim) => ({
|
||||
...claim,
|
||||
key: claim.key.trim(),
|
||||
value: claim.value.trim(),
|
||||
}));
|
||||
|
||||
if (headlessLoginEnabled) {
|
||||
if (!trimmedJwksUri) {
|
||||
@@ -541,7 +721,61 @@ function ClientGeneralPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const claimValidationErrors: string[] = [];
|
||||
const seenClaimKeys = new Set<string>();
|
||||
for (const claim of normalizedIdTokenClaims) {
|
||||
if (!claim.key) {
|
||||
claimValidationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.id_token_claims.key_required",
|
||||
"Claim key를 입력해야 합니다.",
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (claim.key === "rp_claims" && claim.namespace === "top_level") {
|
||||
claimValidationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.id_token_claims.reserved_key",
|
||||
"`rp_claims`는 예약된 namespace 키입니다.",
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const keySignature = `${claim.namespace}:${claim.key}`;
|
||||
if (seenClaimKeys.has(keySignature)) {
|
||||
claimValidationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.id_token_claims.duplicate_key",
|
||||
"중복된 claim key가 있습니다: {{namespace}}.{{key}}",
|
||||
{
|
||||
namespace:
|
||||
claim.namespace === "rp_claims"
|
||||
? t(
|
||||
"ui.dev.clients.general.id_token_claims.namespace_rp_claims",
|
||||
"rp_claims",
|
||||
)
|
||||
: t(
|
||||
"ui.dev.clients.general.id_token_claims.namespace_top_level",
|
||||
"top-level",
|
||||
),
|
||||
key: claim.key,
|
||||
},
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
seenClaimKeys.add(keySignature);
|
||||
}
|
||||
validationErrors.push(...claimValidationErrors);
|
||||
|
||||
const hasValidationErrors = validationErrors.length > 0;
|
||||
const idTokenClaimPreview = buildIdTokenClaimsPreview(
|
||||
normalizedIdTokenClaims,
|
||||
);
|
||||
const idTokenClaimPreviewJson = JSON.stringify(idTokenClaimPreview, null, 2);
|
||||
const normalizedTenantSearch = tenantSearch.trim().toLowerCase();
|
||||
const tenantOptions: Array<TenantSummary | MyTenantSummary> =
|
||||
tenantData ?? [];
|
||||
@@ -676,6 +910,7 @@ function ClientGeneralPage() {
|
||||
auto_login_supported: autoLoginSupported,
|
||||
auto_login_url: autoLoginSupported ? trimmedAutoLoginUrl : undefined,
|
||||
structured_scopes: normalizedScopes,
|
||||
id_token_claims: normalizedIdTokenClaims,
|
||||
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
|
||||
headless_login_enabled: headlessLoginEnabled,
|
||||
headless_token_endpoint_auth_method:
|
||||
@@ -1534,6 +1769,249 @@ function ClientGeneralPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-xl font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.title",
|
||||
"ID Token Claims",
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t(
|
||||
"msg.dev.clients.general.id_token_claims.subtitle",
|
||||
"공통 claim과 RP 전용 확장 claim을 구분해서 관리합니다.",
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button onClick={addIdTokenClaim} className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("ui.dev.clients.general.id_token_claims.add", "Claim 추가")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-4 xl:grid-cols-[1.3fr_0.7fr]">
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-md border border-border overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50 border-b border-border text-xs uppercase tracking-wider text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.table.key",
|
||||
"Claim Key",
|
||||
)}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.table.namespace",
|
||||
"Namespace",
|
||||
)}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.table.value_type",
|
||||
"Value Type",
|
||||
)}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.table.value",
|
||||
"Value",
|
||||
)}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.table.delete",
|
||||
"Delete",
|
||||
)}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{idTokenClaims.map((claim) => (
|
||||
<tr key={claim.id} className="hover:bg-muted/20">
|
||||
<td className="px-4 py-3 align-top">
|
||||
<Input
|
||||
value={claim.key}
|
||||
onChange={(e) =>
|
||||
updateIdTokenClaim(
|
||||
claim.id,
|
||||
"key",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
className="h-9 font-mono text-xs"
|
||||
placeholder={t(
|
||||
"ui.dev.clients.general.id_token_claims.key_placeholder",
|
||||
"e.g. locale",
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 align-top">
|
||||
<select
|
||||
value={claim.namespace}
|
||||
onChange={(e) =>
|
||||
updateIdTokenClaim(
|
||||
claim.id,
|
||||
"namespace",
|
||||
e.target.value as ClaimNamespace,
|
||||
)
|
||||
}
|
||||
aria-label={t(
|
||||
"ui.dev.clients.general.id_token_claims.namespace_label",
|
||||
"Claim namespace",
|
||||
)}
|
||||
className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<option value="top_level">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.namespace_top_level",
|
||||
"top-level",
|
||||
)}
|
||||
</option>
|
||||
<option value="rp_claims">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.namespace_rp_claims",
|
||||
"rp_claims",
|
||||
)}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-4 py-3 align-top">
|
||||
<select
|
||||
value={claim.valueType}
|
||||
onChange={(e) =>
|
||||
updateIdTokenClaim(
|
||||
claim.id,
|
||||
"valueType",
|
||||
e.target.value as ClaimValueType,
|
||||
)
|
||||
}
|
||||
aria-label={t(
|
||||
"ui.dev.clients.general.id_token_claims.value_type_label",
|
||||
"Claim value type",
|
||||
)}
|
||||
className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<option value="text">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.value_type_text",
|
||||
"Text",
|
||||
)}
|
||||
</option>
|
||||
<option value="number">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.value_type_number",
|
||||
"Number",
|
||||
)}
|
||||
</option>
|
||||
<option value="boolean">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.value_type_boolean",
|
||||
"Boolean",
|
||||
)}
|
||||
</option>
|
||||
<option value="array">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.value_type_array",
|
||||
"Array",
|
||||
)}
|
||||
</option>
|
||||
<option value="object">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.value_type_object",
|
||||
"Object",
|
||||
)}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-4 py-3 align-top">
|
||||
<Input
|
||||
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 claim value",
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right align-top">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeIdTokenClaim(claim.id)}
|
||||
className="h-9 w-9 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{idTokenClaims.length === 0 && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={5}
|
||||
className="px-4 py-8 text-center text-muted-foreground"
|
||||
>
|
||||
{t(
|
||||
"msg.dev.clients.general.id_token_claims.empty",
|
||||
"아직 추가된 ID Token claim이 없습니다.",
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-xs leading-6 text-muted-foreground">
|
||||
{t(
|
||||
"msg.dev.clients.general.id_token_claims.hint",
|
||||
"top-level은 일반 claim에, rp_claims는 RP 전용 확장 claim에 사용합니다. 배열은 JSON 또는 콤마 구분 문자열, 객체는 JSON을 입력하면 됩니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-xl border border-border bg-muted/20 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="h-4 w-4 text-primary" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.preview_title",
|
||||
"Saved JSON Preview",
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"msg.dev.clients.general.id_token_claims.preview_hint",
|
||||
"저장될 metadata.id_token_claims 구조를 미리 확인할 수 있습니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Textarea
|
||||
readOnly
|
||||
value={idTokenClaimPreviewJson}
|
||||
className="mt-4 min-h-72 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 3. Security Settings */}
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-3">
|
||||
|
||||
@@ -421,6 +421,15 @@ empty = "No scopes registered."
|
||||
subtitle = "Define the permission scopes this application can request."
|
||||
tenant = "Tenant access claim"
|
||||
|
||||
[msg.dev.clients.general.id_token_claims]
|
||||
subtitle = "Separate shared claims from RP-specific extension claims."
|
||||
empty = "No ID Token claims have been added yet."
|
||||
hint = "Use top-level for shared claims and rp_claims for RP-specific extension claims. Arrays accept JSON or comma-separated values, and objects accept JSON."
|
||||
preview_hint = "Preview the metadata.id_token_claims structure that will be saved."
|
||||
key_required = "Enter a claim key."
|
||||
reserved_key = "`rp_claims` is a reserved namespace key."
|
||||
duplicate_key = "Duplicate claim key: {{namespace}}.{{key}}"
|
||||
|
||||
[msg.dev.clients.general.security]
|
||||
private_help = "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers."
|
||||
pkce_help = "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory."
|
||||
@@ -1452,6 +1461,22 @@ hint = "Turning this on adds the tenant scope automatically and requires at leas
|
||||
autocomplete_hint = "Type a tenant name to see autocomplete suggestions. Click one to add it to the allowed list."
|
||||
validation_required = "Select at least one allowed tenant when tenant access restriction is enabled."
|
||||
|
||||
[ui.dev.clients.general.id_token_claims]
|
||||
title = "ID Token Claims"
|
||||
add = "Add Claim"
|
||||
preview_title = "Saved JSON Preview"
|
||||
namespace_label = "Claim namespace"
|
||||
namespace_top_level = "top-level"
|
||||
namespace_rp_claims = "rp_claims"
|
||||
value_type_label = "Claim value type"
|
||||
value_type_text = "Text"
|
||||
value_type_number = "Number"
|
||||
value_type_boolean = "Boolean"
|
||||
value_type_array = "Array"
|
||||
value_type_object = "Object"
|
||||
key_placeholder = "e.g. locale"
|
||||
value_placeholder = "Enter the claim value"
|
||||
|
||||
[ui.dev.clients.general.security]
|
||||
private = "Server Side App"
|
||||
pkce = "PKCE"
|
||||
|
||||
@@ -421,6 +421,15 @@ empty = "등록된 스코프가 없습니다."
|
||||
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
|
||||
tenant = "소속 테넌트 정보 접근"
|
||||
|
||||
[msg.dev.clients.general.id_token_claims]
|
||||
subtitle = "공통 claim과 RP 전용 확장 claim을 구분해서 관리합니다."
|
||||
empty = "아직 추가된 ID Token claim이 없습니다."
|
||||
hint = "top-level은 공통 claim에, rp_claims는 RP 전용 확장 claim에 사용합니다. 배열은 JSON 또는 콤마 구분 문자열, 객체는 JSON을 입력하면 됩니다."
|
||||
preview_hint = "저장될 metadata.id_token_claims 구조를 미리 확인할 수 있습니다."
|
||||
key_required = "Claim key를 입력해야 합니다."
|
||||
reserved_key = "`rp_claims`는 예약된 namespace 키입니다."
|
||||
duplicate_key = "중복된 claim key가 있습니다: {{namespace}}.{{key}}"
|
||||
|
||||
[msg.dev.clients.general.security]
|
||||
pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다."
|
||||
private_help = "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다."
|
||||
@@ -1451,6 +1460,22 @@ hint = "제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용
|
||||
autocomplete_hint = "테넌트 이름을 입력하면 자동 완성 후보가 나타납니다. 클릭하면 허용 목록에 추가됩니다."
|
||||
validation_required = "테넌트 접근 제한을 사용할 경우 허용 테넌트를 하나 이상 선택해야 합니다."
|
||||
|
||||
[ui.dev.clients.general.id_token_claims]
|
||||
title = "ID Token Claims"
|
||||
add = "Claim 추가"
|
||||
preview_title = "저장 JSON 미리보기"
|
||||
namespace_label = "Claim 네임스페이스"
|
||||
namespace_top_level = "top-level"
|
||||
namespace_rp_claims = "rp_claims"
|
||||
value_type_label = "Claim 값 타입"
|
||||
value_type_text = "텍스트"
|
||||
value_type_number = "숫자"
|
||||
value_type_boolean = "불리언"
|
||||
value_type_array = "배열"
|
||||
value_type_object = "객체"
|
||||
key_placeholder = "예: locale"
|
||||
value_placeholder = "Claim 값을 입력하세요"
|
||||
|
||||
[ui.dev.clients.general.security]
|
||||
private = "Server side App"
|
||||
pkce = "PKCE"
|
||||
|
||||
@@ -426,6 +426,15 @@ save_error = ""
|
||||
save_forbidden = ""
|
||||
status_changed = ""
|
||||
|
||||
[msg.dev.clients.general.id_token_claims]
|
||||
subtitle = ""
|
||||
empty = ""
|
||||
hint = ""
|
||||
preview_hint = ""
|
||||
key_required = ""
|
||||
reserved_key = ""
|
||||
duplicate_key = ""
|
||||
|
||||
[msg.dev.clients.relationships]
|
||||
subtitle = ""
|
||||
add_description = ""
|
||||
@@ -1534,6 +1543,22 @@ hint = ""
|
||||
autocomplete_hint = ""
|
||||
validation_required = ""
|
||||
|
||||
[ui.dev.clients.general.id_token_claims]
|
||||
title = ""
|
||||
add = ""
|
||||
preview_title = ""
|
||||
namespace_label = ""
|
||||
namespace_top_level = ""
|
||||
namespace_rp_claims = ""
|
||||
value_type_label = ""
|
||||
value_type_text = ""
|
||||
value_type_number = ""
|
||||
value_type_boolean = ""
|
||||
value_type_array = ""
|
||||
value_type_object = ""
|
||||
key_placeholder = ""
|
||||
value_placeholder = ""
|
||||
|
||||
[ui.dev.clients.general.security]
|
||||
private = ""
|
||||
pkce = ""
|
||||
|
||||
Reference in New Issue
Block a user