forked from baron/baron-sso
Devfront back-channel logout 설정 UI 및 i18n 추가
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user