forked from baron/baron-sso
chore: consolidate local integration changes
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
Check,
|
||||
ExternalLink,
|
||||
Info,
|
||||
Plus,
|
||||
@@ -56,6 +55,7 @@ import { resolveProfileRole } from "../../lib/role";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { fetchMe, type UserProfile } from "../auth/authApi";
|
||||
import { ClientDetailTabs } from "./ClientDetailTabs";
|
||||
import { AllowedTenantBadge } from "./components/AllowedTenantBadge";
|
||||
|
||||
interface ScopeItem {
|
||||
id: string;
|
||||
@@ -65,8 +65,16 @@ interface ScopeItem {
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
type ClaimNamespace = "top_level" | "rp_claims";
|
||||
type ClaimValueType = "text" | "number" | "boolean" | "array" | "object";
|
||||
type ClaimNamespace = "rp_claims";
|
||||
type ClaimValueType =
|
||||
| "text"
|
||||
| "number"
|
||||
| "boolean"
|
||||
| "array"
|
||||
| "object"
|
||||
| "date"
|
||||
| "datetime";
|
||||
type CustomClaimPermission = "admin_only" | "user_and_admin";
|
||||
|
||||
interface IdTokenClaimItem {
|
||||
id: string;
|
||||
@@ -74,6 +82,8 @@ interface IdTokenClaimItem {
|
||||
key: string;
|
||||
value: string;
|
||||
valueType: ClaimValueType;
|
||||
readPermission: CustomClaimPermission;
|
||||
writePermission: CustomClaimPermission;
|
||||
}
|
||||
|
||||
type SecurityProfile = "private" | "pkce";
|
||||
@@ -129,7 +139,7 @@ function readMetadataString(
|
||||
}
|
||||
|
||||
function isClaimNamespace(value: string): value is ClaimNamespace {
|
||||
return value === "top_level" || value === "rp_claims";
|
||||
return value === "rp_claims";
|
||||
}
|
||||
|
||||
function isClaimValueType(value: string): value is ClaimValueType {
|
||||
@@ -138,17 +148,27 @@ function isClaimValueType(value: string): value is ClaimValueType {
|
||||
value === "number" ||
|
||||
value === "boolean" ||
|
||||
value === "array" ||
|
||||
value === "object"
|
||||
value === "object" ||
|
||||
value === "date" ||
|
||||
value === "datetime"
|
||||
);
|
||||
}
|
||||
|
||||
function isCustomClaimPermission(
|
||||
value: unknown,
|
||||
): value is CustomClaimPermission {
|
||||
return value === "admin_only" || value === "user_and_admin";
|
||||
}
|
||||
|
||||
function createIdTokenClaimItem(id: string): IdTokenClaimItem {
|
||||
return {
|
||||
id,
|
||||
namespace: "top_level",
|
||||
namespace: "rp_claims",
|
||||
key: "",
|
||||
value: "",
|
||||
valueType: "text",
|
||||
readPermission: "admin_only",
|
||||
writePermission: "admin_only",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,7 +191,10 @@ function readIdTokenClaimsMetadata(
|
||||
typeof record.namespace === "string" &&
|
||||
isClaimNamespace(record.namespace)
|
||||
? record.namespace
|
||||
: "top_level";
|
||||
: null;
|
||||
if (namespaceValue === null) {
|
||||
return null;
|
||||
}
|
||||
const keyValue = typeof record.key === "string" ? record.key : "";
|
||||
const rawValue = record.value;
|
||||
const valueValue =
|
||||
@@ -192,6 +215,12 @@ function readIdTokenClaimsMetadata(
|
||||
key: keyValue,
|
||||
value: valueValue,
|
||||
valueType: valueTypeValue,
|
||||
readPermission: isCustomClaimPermission(record.readPermission)
|
||||
? record.readPermission
|
||||
: "admin_only",
|
||||
writePermission: isCustomClaimPermission(record.writePermission)
|
||||
? record.writePermission
|
||||
: "admin_only",
|
||||
};
|
||||
})
|
||||
.filter((item): item is IdTokenClaimItem => item !== null);
|
||||
@@ -253,8 +282,7 @@ function buildIdTokenClaimsPreview(
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = item.namespace === "rp_claims" ? rpClaims : preview;
|
||||
target[key] = normalizeClaimPreviewValue(item.value, item.valueType);
|
||||
rpClaims[key] = normalizeClaimPreviewValue(item.value, item.valueType);
|
||||
}
|
||||
|
||||
if (Object.keys(rpClaims).length > 0) {
|
||||
@@ -841,16 +869,6 @@ function ClientGeneralPage() {
|
||||
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(
|
||||
@@ -858,16 +876,10 @@ function ClientGeneralPage() {
|
||||
"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",
|
||||
),
|
||||
namespace: t(
|
||||
"ui.dev.clients.general.id_token_claims.namespace_rp_claims",
|
||||
"rp_claims",
|
||||
),
|
||||
key: claim.key,
|
||||
},
|
||||
),
|
||||
@@ -1951,26 +1963,12 @@ function ClientGeneralPage() {
|
||||
{tenantAccessRestricted && allowedTenantIds.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedAllowedTenants.map((tenant) => (
|
||||
<Badge
|
||||
<AllowedTenantBadge
|
||||
key={tenant.id}
|
||||
variant="secondary"
|
||||
className="gap-2 px-3 py-1.5"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
<span className="max-w-44 truncate">{tenant.name}</span>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{tenant.slug}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("ui.common.delete", "삭제")}
|
||||
onClick={() => toggleAllowedTenant(tenant.id)}
|
||||
className="text-muted-foreground transition hover:text-destructive"
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Badge>
|
||||
tenant={tenant}
|
||||
onRemove={() => toggleAllowedTenant(tenant.id)}
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
/>
|
||||
))}
|
||||
{allowedTenantIds
|
||||
.filter(
|
||||
@@ -1980,23 +1978,12 @@ function ClientGeneralPage() {
|
||||
),
|
||||
)
|
||||
.map((tenantId) => (
|
||||
<Badge
|
||||
<AllowedTenantBadge
|
||||
key={tenantId}
|
||||
variant="secondary"
|
||||
className="gap-2 px-3 py-1.5"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
<span className="max-w-44 truncate">{tenantId}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("ui.common.delete", "삭제")}
|
||||
onClick={() => toggleAllowedTenant(tenantId)}
|
||||
className="text-muted-foreground transition hover:text-destructive"
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Badge>
|
||||
tenant={{ id: tenantId, name: tenantId }}
|
||||
onRemove={() => toggleAllowedTenant(tenantId)}
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -2070,6 +2057,18 @@ function ClientGeneralPage() {
|
||||
"Value Type",
|
||||
)}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.table.read_permission",
|
||||
"Read",
|
||||
)}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.table.write_permission",
|
||||
"Write",
|
||||
)}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.table.value",
|
||||
@@ -2106,35 +2105,15 @@ function ClientGeneralPage() {
|
||||
/>
|
||||
</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"
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
<Badge
|
||||
variant="muted"
|
||||
className="h-9 rounded-md border bg-muted/40 px-3 py-2 font-mono text-xs"
|
||||
>
|
||||
<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>
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.namespace_rp_claims",
|
||||
"rp_claims",
|
||||
)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 align-top">
|
||||
<select
|
||||
@@ -2183,6 +2162,80 @@ function ClientGeneralPage() {
|
||||
"Object",
|
||||
)}
|
||||
</option>
|
||||
<option value="date">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.value_type_date",
|
||||
"Date",
|
||||
)}
|
||||
</option>
|
||||
<option value="datetime">
|
||||
{t(
|
||||
"ui.dev.clients.general.id_token_claims.value_type_datetime",
|
||||
"Datetime",
|
||||
)}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-4 py-3 align-top">
|
||||
<select
|
||||
value={claim.readPermission}
|
||||
onChange={(e) =>
|
||||
updateIdTokenClaim(
|
||||
claim.id,
|
||||
"readPermission",
|
||||
e.target.value as CustomClaimPermission,
|
||||
)
|
||||
}
|
||||
aria-label={t(
|
||||
"ui.dev.clients.general.id_token_claims.read_permission_label",
|
||||
"읽기 권한",
|
||||
)}
|
||||
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"
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
>
|
||||
<option value="admin_only">
|
||||
{t(
|
||||
"ui.common.custom_claim_permission.admin_only",
|
||||
"관리자만 가능",
|
||||
)}
|
||||
</option>
|
||||
<option value="user_and_admin">
|
||||
{t(
|
||||
"ui.common.custom_claim_permission.user_and_admin",
|
||||
"사용자 및 관리자 가능",
|
||||
)}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-4 py-3 align-top">
|
||||
<select
|
||||
value={claim.writePermission}
|
||||
onChange={(e) =>
|
||||
updateIdTokenClaim(
|
||||
claim.id,
|
||||
"writePermission",
|
||||
e.target.value as CustomClaimPermission,
|
||||
)
|
||||
}
|
||||
aria-label={t(
|
||||
"ui.dev.clients.general.id_token_claims.write_permission_label",
|
||||
"쓰기 권한",
|
||||
)}
|
||||
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"
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
>
|
||||
<option value="admin_only">
|
||||
{t(
|
||||
"ui.common.custom_claim_permission.admin_only",
|
||||
"관리자만 가능",
|
||||
)}
|
||||
</option>
|
||||
<option value="user_and_admin">
|
||||
{t(
|
||||
"ui.common.custom_claim_permission.user_and_admin",
|
||||
"사용자 및 관리자 가능",
|
||||
)}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-4 py-3 align-top">
|
||||
@@ -2219,7 +2272,7 @@ function ClientGeneralPage() {
|
||||
{idTokenClaims.length === 0 && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={5}
|
||||
colSpan={7}
|
||||
className="px-4 py-8 text-center text-muted-foreground"
|
||||
>
|
||||
{t(
|
||||
@@ -2235,7 +2288,7 @@ function ClientGeneralPage() {
|
||||
<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을 입력하면 됩니다.",
|
||||
"RP 전용 확장 claim만 관리합니다. 배열은 JSON 또는 콤마 구분 문자열, 객체는 JSON을 입력하면 됩니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user