forked from baron/baron-sso
앱 로고 URL 검증 및 미리보기 상태 표시
This commit is contained in:
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ExternalLink,
|
||||
Info,
|
||||
Plus,
|
||||
Save,
|
||||
@@ -133,6 +134,9 @@ function ClientGeneralPage() {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [logoUrl, setLogoUrl] = useState("");
|
||||
const [logoPreviewStatus, setLogoPreviewStatus] = useState<
|
||||
"idle" | "loading" | "loaded" | "error"
|
||||
>("idle");
|
||||
const [clientType, setClientType] = useState<ClientType>("private");
|
||||
const [status, setStatus] = useState<ClientStatus>("active");
|
||||
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
|
||||
@@ -240,6 +244,21 @@ function ClientGeneralPage() {
|
||||
|
||||
const securityProfile: SecurityProfile =
|
||||
clientType === "pkce" ? "pkce" : "private";
|
||||
const trimmedLogoUrl = logoUrl.trim();
|
||||
const hasLogoUrl = trimmedLogoUrl.length > 0;
|
||||
const hasValidLogoUrl = !hasLogoUrl || isValidUrl(trimmedLogoUrl);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasLogoUrl) {
|
||||
setLogoPreviewStatus("idle");
|
||||
return;
|
||||
}
|
||||
if (!hasValidLogoUrl) {
|
||||
setLogoPreviewStatus("error");
|
||||
return;
|
||||
}
|
||||
setLogoPreviewStatus("loading");
|
||||
}, [hasLogoUrl, hasValidLogoUrl, trimmedLogoUrl]);
|
||||
|
||||
const handleSecurityProfileChange = (profile: SecurityProfile) => {
|
||||
setClientType(profile);
|
||||
@@ -438,6 +457,15 @@ function ClientGeneralPage() {
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (hasLogoUrl && !hasValidLogoUrl) {
|
||||
throw new Error(
|
||||
t(
|
||||
"msg.dev.clients.general.identity.logo_invalid",
|
||||
"앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
|
||||
|
||||
const effectiveTokenEndpointAuthMethod =
|
||||
@@ -457,7 +485,7 @@ function ClientGeneralPage() {
|
||||
: undefined,
|
||||
metadata: {
|
||||
description,
|
||||
logo_url: logoUrl,
|
||||
logo_url: trimmedLogoUrl,
|
||||
structured_scopes: scopes,
|
||||
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
|
||||
headless_login_enabled: headlessLoginEnabled,
|
||||
@@ -722,6 +750,8 @@ function ClientGeneralPage() {
|
||||
<Input
|
||||
value={logoUrl}
|
||||
onChange={(e) => setLogoUrl(e.target.value)}
|
||||
aria-invalid={!hasValidLogoUrl}
|
||||
className={!hasValidLogoUrl ? "border-destructive" : ""}
|
||||
placeholder={t(
|
||||
"ui.dev.clients.general.identity.logo_placeholder",
|
||||
"https://example.com/logo.png",
|
||||
@@ -733,19 +763,100 @@ function ClientGeneralPage() {
|
||||
"인증 화면에 표시될 PNG/SVG URL입니다.",
|
||||
)}
|
||||
</p>
|
||||
{!hasValidLogoUrl ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{t(
|
||||
"msg.dev.clients.general.identity.logo_invalid",
|
||||
"앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
{hasLogoUrl && hasValidLogoUrl ? (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className={cn("text-muted-foreground", {
|
||||
"text-foreground": logoPreviewStatus === "loaded",
|
||||
"text-destructive": logoPreviewStatus === "error",
|
||||
})}
|
||||
>
|
||||
{logoPreviewStatus === "loading"
|
||||
? t(
|
||||
"msg.dev.clients.general.identity.logo_preview_loading",
|
||||
"로고 미리보기를 불러오는 중입니다.",
|
||||
)
|
||||
: logoPreviewStatus === "loaded"
|
||||
? t(
|
||||
"msg.dev.clients.general.identity.logo_preview_ready",
|
||||
"로고 미리보기를 확인했습니다.",
|
||||
)
|
||||
: logoPreviewStatus === "error"
|
||||
? t(
|
||||
"msg.dev.clients.general.identity.logo_preview_failed",
|
||||
"로고 미리보기를 불러오지 못했습니다. URL 또는 이미지 접근 권한을 확인하세요.",
|
||||
)
|
||||
: null}
|
||||
</span>
|
||||
<a
|
||||
href={trimmedLogoUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
{t(
|
||||
"ui.dev.clients.general.identity.logo_open",
|
||||
"새 탭에서 열기",
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/40 shrink-0">
|
||||
{logoUrl ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-20 w-20 shrink-0 items-center justify-center rounded-lg border-2 border-dashed",
|
||||
hasLogoUrl && hasValidLogoUrl && logoPreviewStatus !== "error"
|
||||
? "bg-white"
|
||||
: "bg-muted/40",
|
||||
logoPreviewStatus === "error"
|
||||
? "border-destructive/60"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
{hasLogoUrl && hasValidLogoUrl ? (
|
||||
<img
|
||||
src={logoUrl}
|
||||
key={trimmedLogoUrl}
|
||||
src={trimmedLogoUrl}
|
||||
alt={t(
|
||||
"ui.dev.clients.general.identity.logo_preview",
|
||||
"Logo Preview",
|
||||
)}
|
||||
className="h-full w-full object-contain"
|
||||
onLoad={() => setLogoPreviewStatus("loaded")}
|
||||
onError={() => setLogoPreviewStatus("error")}
|
||||
/>
|
||||
) : (
|
||||
<Upload className="h-5 w-5 text-muted-foreground" />
|
||||
<div className="flex flex-col items-center justify-center gap-1 px-2 text-center">
|
||||
<Upload
|
||||
className={cn("h-5 w-5 text-muted-foreground", {
|
||||
"text-destructive": logoPreviewStatus === "error",
|
||||
})}
|
||||
/>
|
||||
{logoPreviewStatus === "error" ? (
|
||||
<span className="text-[10px] leading-tight text-destructive">
|
||||
{t(
|
||||
"ui.dev.clients.general.identity.logo_preview_error_badge",
|
||||
"미리보기 실패",
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[10px] leading-tight text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.identity.logo_preview_empty",
|
||||
"미리보기",
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -377,6 +377,10 @@ empty = "No IdP configurations found."
|
||||
|
||||
[msg.dev.clients.general.identity]
|
||||
logo_help = "PNG or SVG URL shown on the consent and authentication screens."
|
||||
logo_invalid = "The app logo URL format is invalid. Enter an http or https address."
|
||||
logo_preview_loading = "Loading the logo preview."
|
||||
logo_preview_ready = "Logo preview loaded."
|
||||
logo_preview_failed = "Failed to load the logo preview. Check the URL or image access policy."
|
||||
subtitle = "Set the application name, description, and logo."
|
||||
|
||||
[msg.dev.clients.general.redirect]
|
||||
@@ -1378,6 +1382,9 @@ description_placeholder = "Description Placeholder"
|
||||
logo = "App Logo URL"
|
||||
logo_placeholder = "https://example.com/logo.png"
|
||||
logo_preview = "Logo Preview"
|
||||
logo_open = "Open in new tab"
|
||||
logo_preview_error_badge = "Preview failed"
|
||||
logo_preview_empty = "Preview"
|
||||
name = "Name"
|
||||
name_placeholder = "My Awesome Application"
|
||||
title = "Application Identity"
|
||||
|
||||
@@ -377,6 +377,10 @@ subtitle = "이 애플리케이션의 외부 IdP 설정을 관리합니다."
|
||||
|
||||
[msg.dev.clients.general.identity]
|
||||
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
|
||||
logo_invalid = "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요."
|
||||
logo_preview_loading = "로고 미리보기를 불러오는 중입니다."
|
||||
logo_preview_ready = "로고 미리보기를 확인했습니다."
|
||||
logo_preview_failed = "로고 미리보기를 불러오지 못했습니다. URL 또는 이미지 접근 권한을 확인하세요."
|
||||
subtitle = "앱 이름과 설명, 로고를 설정합니다."
|
||||
|
||||
[msg.dev.clients.general.redirect]
|
||||
@@ -1377,6 +1381,9 @@ description_placeholder = "앱에 대한 간단한 설명을 입력하세요."
|
||||
logo = "앱 로고 URL"
|
||||
logo_placeholder = "https://example.com/logo.png"
|
||||
logo_preview = "로고 미리보기"
|
||||
logo_open = "새 탭에서 열기"
|
||||
logo_preview_error_badge = "미리보기 실패"
|
||||
logo_preview_empty = "미리보기"
|
||||
name = "앱 이름"
|
||||
name_placeholder = "예: 멋진 애플리케이션"
|
||||
title = "애플리케이션 정보"
|
||||
|
||||
@@ -377,6 +377,10 @@ empty = ""
|
||||
|
||||
[msg.dev.clients.general.identity]
|
||||
logo_help = ""
|
||||
logo_invalid = ""
|
||||
logo_preview_loading = ""
|
||||
logo_preview_ready = ""
|
||||
logo_preview_failed = ""
|
||||
subtitle = ""
|
||||
|
||||
[msg.dev.clients.general.redirect]
|
||||
@@ -1378,6 +1382,9 @@ description_placeholder = ""
|
||||
logo = ""
|
||||
logo_placeholder = ""
|
||||
logo_preview = ""
|
||||
logo_open = ""
|
||||
logo_preview_error_badge = ""
|
||||
logo_preview_empty = ""
|
||||
name = ""
|
||||
name_placeholder = ""
|
||||
title = ""
|
||||
|
||||
Reference in New Issue
Block a user