1
0
forked from baron/baron-sso

Devfront back-channel logout 설정 UI 및 i18n 추가

This commit is contained in:
2026-05-04 11:00:02 +09:00
parent 068d0adbd4
commit 0664640c6f
5 changed files with 214 additions and 0 deletions

View File

@@ -271,6 +271,29 @@ function isValidUrl(value: string): boolean {
}
}
function isValidBackchannelLogoutUrl(value: string): boolean {
const trimmed = value.trim();
if (!trimmed) {
return true;
}
try {
const url = new URL(trimmed);
if (url.hash) {
return false;
}
if (url.protocol === "https:") {
return true;
}
if (url.protocol !== "http:") {
return false;
}
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
} catch {
return false;
}
}
function formatDateTime(value?: string) {
if (!value) return "-";
const date = new Date(value);
@@ -315,6 +338,11 @@ function ClientGeneralPage() {
const [status, setStatus] = useState<ClientStatus>("active");
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
const [redirectUris, setRedirectUris] = useState("");
const [backchannelLogoutUri, setBackchannelLogoutUri] = useState("");
const [backchannelLogoutSessionRequired, setBackchannelLogoutSessionRequired] =
useState(false);
const [isBackchannelSessionRequiredInfoOpen, setIsBackchannelSessionRequiredInfoOpen] =
useState(false);
const [tenantAccessRestricted, setTenantAccessRestricted] = useState(false);
const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]);
const [tenantSearch, setTenantSearch] = useState("");
@@ -368,6 +396,14 @@ function ClientGeneralPage() {
if (typeof metadata.description === "string")
setDescription(metadata.description);
if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url);
setBackchannelLogoutUri(
client.backchannelLogoutUri ||
readMetadataString(metadata, "backchannel_logout_uri"),
);
setBackchannelLogoutSessionRequired(
client.backchannelLogoutSessionRequired === true ||
metadata.backchannel_logout_session_required === true,
);
setAutoLoginSupported(metadata.auto_login_supported === true);
if (typeof metadata.auto_login_url === "string")
setAutoLoginUrl(metadata.auto_login_url);
@@ -468,6 +504,11 @@ function ClientGeneralPage() {
const trimmedAutoLoginUrl = autoLoginUrl.trim();
const hasLogoUrl = trimmedLogoUrl.length > 0;
const hasValidLogoUrl = !hasLogoUrl || isValidUrl(trimmedLogoUrl);
const trimmedBackchannelLogoutUri = backchannelLogoutUri.trim();
const hasBackchannelLogoutUri = trimmedBackchannelLogoutUri.length > 0;
const hasValidBackchannelLogoutUri =
!hasBackchannelLogoutUri ||
isValidBackchannelLogoutUrl(trimmedBackchannelLogoutUri);
const hasValidAutoLoginUrl =
!autoLoginSupported ||
(trimmedAutoLoginUrl.length > 0 && isValidUrl(trimmedAutoLoginUrl));
@@ -893,6 +934,14 @@ function ClientGeneralPage() {
),
);
}
if (hasBackchannelLogoutUri && !hasValidBackchannelLogoutUri) {
throw new Error(
t(
"msg.dev.clients.general.backchannel_logout.invalid",
"Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https, 로컬 개발 환경은 localhost/127.0.0.1의 http만 허용됩니다.",
),
);
}
if (isGeneralSettingsReadOnly) {
throw new Error(
t(
@@ -936,6 +985,11 @@ function ClientGeneralPage() {
trimmedJwksUri
? trimmedJwksUri
: undefined,
backchannelLogoutUri: trimmedBackchannelLogoutUri || undefined,
backchannelLogoutSessionRequired:
trimmedBackchannelLogoutUri !== ""
? backchannelLogoutSessionRequired
: false,
metadata: {
description,
logo_url: trimmedLogoUrl,
@@ -957,6 +1011,11 @@ function ClientGeneralPage() {
allowed_tenants: tenantAccessRestricted
? normalizedAllowedTenantIds
: [],
backchannel_logout_uri: trimmedBackchannelLogoutUri || undefined,
backchannel_logout_session_required:
trimmedBackchannelLogoutUri !== ""
? backchannelLogoutSessionRequired
: undefined,
},
};
@@ -1493,6 +1552,118 @@ function ClientGeneralPage() {
</div>
)}
<div className="space-y-4 border-b border-border pb-6 mb-6">
<div className="space-y-2">
<Label
className="text-sm font-semibold"
htmlFor="backchannel-logout-uri"
>
{t(
"ui.dev.clients.general.backchannel_logout.uri",
"Back-Channel Logout URI",
)}
</Label>
<Input
id="backchannel-logout-uri"
value={backchannelLogoutUri}
onChange={(e) => setBackchannelLogoutUri(e.target.value)}
placeholder={t(
"ui.dev.clients.general.backchannel_logout.uri_placeholder",
"https://rp.example.com/oidc/backchannel-logout",
)}
className="font-mono text-sm"
disabled={isGeneralSettingsReadOnly}
/>
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.backchannel_logout.uri_help",
"Baron이 세션 종료 이벤트를 서버 간 POST로 전달할 RP endpoint입니다.",
)}
</p>
{hasBackchannelLogoutUri && !hasValidBackchannelLogoutUri ? (
<p className="text-xs text-destructive">
{t(
"msg.dev.clients.general.backchannel_logout.invalid",
"Back-Channel Logout URI 형식이 올바르지 않습니다. 운영 환경은 https, 로컬 개발 환경은 localhost/127.0.0.1의 http만 허용됩니다.",
)}
</p>
) : null}
</div>
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/20 px-4 py-3">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Label
className="text-sm font-semibold"
htmlFor="backchannel-logout-session-required"
>
{t(
"ui.dev.clients.general.backchannel_logout.session_required",
"SID Claim Required",
)}
</Label>
<button
type="button"
className={`rounded-full p-0.5 transition-colors ${
isBackchannelSessionRequiredInfoOpen
? "text-primary"
: "text-muted-foreground/60 hover:text-primary"
}`}
onClick={() =>
setIsBackchannelSessionRequiredInfoOpen(
(prev) => !prev,
)
}
aria-label={t(
"ui.dev.clients.general.backchannel_logout.session_required_info",
"SID Claim Required 설명 보기",
)}
>
{isBackchannelSessionRequiredInfoOpen ? (
<X className="h-3.5 w-3.5" />
) : (
<Info className="h-3.5 w-3.5" />
)}
</button>
</div>
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.backchannel_logout.session_required_help",
"RP가 logout_token에 sid claim이 포함된 경우에만 처리하도록 요구할 때 사용합니다.",
)}
</p>
{isBackchannelSessionRequiredInfoOpen ? (
<div className="mt-2 animate-in fade-in slide-in-from-top-1 rounded-lg border border-primary/20 bg-primary/5 p-3 text-xs leading-relaxed text-foreground shadow-sm">
<div className="flex items-center gap-1.5 font-bold text-primary mb-1">
<Info className="h-3 w-3" />
{t("ui.common.info", "상세 안내")}
</div>
<div>
{t(
"msg.dev.clients.general.backchannel_logout.session_required_on",
"켜면: logout_token 안에 sid가 있을 때만 로그아웃 처리",
)}
</div>
<div>
{t(
"msg.dev.clients.general.backchannel_logout.session_required_off",
"끄면: sid가 없어도 sub만으로 로그아웃 처리 가능",
)}
</div>
</div>
) : null}
</div>
<Switch
id="backchannel-logout-session-required"
checked={backchannelLogoutSessionRequired}
onCheckedChange={setBackchannelLogoutSessionRequired}
disabled={
isGeneralSettingsReadOnly || !hasBackchannelLogoutUri
}
/>
</div>
</div>
<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">