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 type { AxiosError } from "axios";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
ExternalLink,
|
||||||
Info,
|
Info,
|
||||||
Plus,
|
Plus,
|
||||||
Save,
|
Save,
|
||||||
@@ -133,6 +134,9 @@ function ClientGeneralPage() {
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [logoUrl, setLogoUrl] = useState("");
|
const [logoUrl, setLogoUrl] = useState("");
|
||||||
|
const [logoPreviewStatus, setLogoPreviewStatus] = useState<
|
||||||
|
"idle" | "loading" | "loaded" | "error"
|
||||||
|
>("idle");
|
||||||
const [clientType, setClientType] = useState<ClientType>("private");
|
const [clientType, setClientType] = useState<ClientType>("private");
|
||||||
const [status, setStatus] = useState<ClientStatus>("active");
|
const [status, setStatus] = useState<ClientStatus>("active");
|
||||||
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
|
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
|
||||||
@@ -240,6 +244,21 @@ function ClientGeneralPage() {
|
|||||||
|
|
||||||
const securityProfile: SecurityProfile =
|
const securityProfile: SecurityProfile =
|
||||||
clientType === "pkce" ? "pkce" : "private";
|
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) => {
|
const handleSecurityProfileChange = (profile: SecurityProfile) => {
|
||||||
setClientType(profile);
|
setClientType(profile);
|
||||||
@@ -438,6 +457,15 @@ function ClientGeneralPage() {
|
|||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async () => {
|
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 scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
|
||||||
|
|
||||||
const effectiveTokenEndpointAuthMethod =
|
const effectiveTokenEndpointAuthMethod =
|
||||||
@@ -457,7 +485,7 @@ function ClientGeneralPage() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
metadata: {
|
metadata: {
|
||||||
description,
|
description,
|
||||||
logo_url: logoUrl,
|
logo_url: trimmedLogoUrl,
|
||||||
structured_scopes: scopes,
|
structured_scopes: scopes,
|
||||||
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
|
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
|
||||||
headless_login_enabled: headlessLoginEnabled,
|
headless_login_enabled: headlessLoginEnabled,
|
||||||
@@ -722,6 +750,8 @@ function ClientGeneralPage() {
|
|||||||
<Input
|
<Input
|
||||||
value={logoUrl}
|
value={logoUrl}
|
||||||
onChange={(e) => setLogoUrl(e.target.value)}
|
onChange={(e) => setLogoUrl(e.target.value)}
|
||||||
|
aria-invalid={!hasValidLogoUrl}
|
||||||
|
className={!hasValidLogoUrl ? "border-destructive" : ""}
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"ui.dev.clients.general.identity.logo_placeholder",
|
"ui.dev.clients.general.identity.logo_placeholder",
|
||||||
"https://example.com/logo.png",
|
"https://example.com/logo.png",
|
||||||
@@ -733,19 +763,100 @@ function ClientGeneralPage() {
|
|||||||
"인증 화면에 표시될 PNG/SVG URL입니다.",
|
"인증 화면에 표시될 PNG/SVG URL입니다.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</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>
|
||||||
<div className="flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/40 shrink-0">
|
<div
|
||||||
{logoUrl ? (
|
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
|
<img
|
||||||
src={logoUrl}
|
key={trimmedLogoUrl}
|
||||||
|
src={trimmedLogoUrl}
|
||||||
alt={t(
|
alt={t(
|
||||||
"ui.dev.clients.general.identity.logo_preview",
|
"ui.dev.clients.general.identity.logo_preview",
|
||||||
"Logo Preview",
|
"Logo Preview",
|
||||||
)}
|
)}
|
||||||
className="h-full w-full object-contain"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -377,6 +377,10 @@ empty = "No IdP configurations found."
|
|||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = "PNG or SVG URL shown on the consent and authentication screens."
|
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."
|
subtitle = "Set the application name, description, and logo."
|
||||||
|
|
||||||
[msg.dev.clients.general.redirect]
|
[msg.dev.clients.general.redirect]
|
||||||
@@ -1378,6 +1382,9 @@ description_placeholder = "Description Placeholder"
|
|||||||
logo = "App Logo URL"
|
logo = "App Logo URL"
|
||||||
logo_placeholder = "https://example.com/logo.png"
|
logo_placeholder = "https://example.com/logo.png"
|
||||||
logo_preview = "Logo Preview"
|
logo_preview = "Logo Preview"
|
||||||
|
logo_open = "Open in new tab"
|
||||||
|
logo_preview_error_badge = "Preview failed"
|
||||||
|
logo_preview_empty = "Preview"
|
||||||
name = "Name"
|
name = "Name"
|
||||||
name_placeholder = "My Awesome Application"
|
name_placeholder = "My Awesome Application"
|
||||||
title = "Application Identity"
|
title = "Application Identity"
|
||||||
|
|||||||
@@ -377,6 +377,10 @@ subtitle = "이 애플리케이션의 외부 IdP 설정을 관리합니다."
|
|||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
|
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
|
||||||
|
logo_invalid = "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요."
|
||||||
|
logo_preview_loading = "로고 미리보기를 불러오는 중입니다."
|
||||||
|
logo_preview_ready = "로고 미리보기를 확인했습니다."
|
||||||
|
logo_preview_failed = "로고 미리보기를 불러오지 못했습니다. URL 또는 이미지 접근 권한을 확인하세요."
|
||||||
subtitle = "앱 이름과 설명, 로고를 설정합니다."
|
subtitle = "앱 이름과 설명, 로고를 설정합니다."
|
||||||
|
|
||||||
[msg.dev.clients.general.redirect]
|
[msg.dev.clients.general.redirect]
|
||||||
@@ -1377,6 +1381,9 @@ description_placeholder = "앱에 대한 간단한 설명을 입력하세요."
|
|||||||
logo = "앱 로고 URL"
|
logo = "앱 로고 URL"
|
||||||
logo_placeholder = "https://example.com/logo.png"
|
logo_placeholder = "https://example.com/logo.png"
|
||||||
logo_preview = "로고 미리보기"
|
logo_preview = "로고 미리보기"
|
||||||
|
logo_open = "새 탭에서 열기"
|
||||||
|
logo_preview_error_badge = "미리보기 실패"
|
||||||
|
logo_preview_empty = "미리보기"
|
||||||
name = "앱 이름"
|
name = "앱 이름"
|
||||||
name_placeholder = "예: 멋진 애플리케이션"
|
name_placeholder = "예: 멋진 애플리케이션"
|
||||||
title = "애플리케이션 정보"
|
title = "애플리케이션 정보"
|
||||||
|
|||||||
@@ -377,6 +377,10 @@ empty = ""
|
|||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = ""
|
logo_help = ""
|
||||||
|
logo_invalid = ""
|
||||||
|
logo_preview_loading = ""
|
||||||
|
logo_preview_ready = ""
|
||||||
|
logo_preview_failed = ""
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
[msg.dev.clients.general.redirect]
|
[msg.dev.clients.general.redirect]
|
||||||
@@ -1378,6 +1382,9 @@ description_placeholder = ""
|
|||||||
logo = ""
|
logo = ""
|
||||||
logo_placeholder = ""
|
logo_placeholder = ""
|
||||||
logo_preview = ""
|
logo_preview = ""
|
||||||
|
logo_open = ""
|
||||||
|
logo_preview_error_badge = ""
|
||||||
|
logo_preview_empty = ""
|
||||||
name = ""
|
name = ""
|
||||||
name_placeholder = ""
|
name_placeholder = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user