From c96a5350a7a4118e5ca10fe65aa344de120dcf87 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 30 Mar 2026 13:29:36 +0900 Subject: [PATCH] =?UTF-8?q?code-check=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/dev_handler.go | 2 +- .../features/clients/ClientGeneralPage.tsx | 173 +++++++++++++----- devfront/src/lib/keyUtils.ts | 28 +-- .../tests/devfront-clients-lifecycle.spec.ts | 24 ++- locales/en.toml | 47 +++++ locales/ko.toml | 47 +++++ locales/template.toml | 47 +++++ userfront/assets/translations/en.toml | 1 + userfront/assets/translations/ko.toml | 1 + userfront/assets/translations/template.toml | 1 + 10 files changed, 302 insertions(+), 69 deletions(-) diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index fe93a76a..94baa6b1 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -143,7 +143,7 @@ type clientUpsertRequest struct { ResponseTypes *[]string `json:"responseTypes"` TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"` JwksUri *string `json:"jwksUri"` - Jwks interface{} `json:"jwks"` + Jwks interface{} `json:"jwks"` Metadata *map[string]interface{} `json:"metadata"` } diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 85762cea..fce5a6ac 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -49,6 +49,20 @@ interface ScopeItem { } type SecurityProfile = "private" | "pkce"; +type TokenEndpointAuthMethod = + | "none" + | "client_secret_basic" + | "private_key_jwt"; + +function isTokenEndpointAuthMethod( + value: string, +): value is TokenEndpointAuthMethod { + return ( + value === "none" || + value === "client_secret_basic" || + value === "private_key_jwt" + ); +} function readMetadataString( metadata: Record, @@ -96,17 +110,17 @@ function ClientGeneralPage() { const [status, setStatus] = useState("active"); const [initialStatus, setInitialStatus] = useState("active"); const [redirectUris, setRedirectUris] = useState(""); - + // Public Key Registration States - const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] = useState< - "none" | "client_secret_basic" | "private_key_jwt" - >("client_secret_basic"); + const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] = + useState("client_secret_basic"); const [jwksSource, setJwksSource] = useState<"uri" | "inline">("inline"); const [jwksUri, setJwksUri] = useState(""); const [jwksText, setJwksText] = useState(""); - const [requestObjectSigningAlg, setRequestObjectSigningAlg] = useState("RS256"); + const [requestObjectSigningAlg, setRequestObjectSigningAlg] = + useState("RS256"); const [headlessLoginEnabled, setHeadlessLoginEnabled] = useState(false); - + const [scopes, setScopes] = useState(() => [ { id: "1", @@ -136,40 +150,55 @@ function ClientGeneralPage() { setStatus(client.status); setInitialStatus(client.status); - const savedAuthMethod = client.tokenEndpointAuthMethod || (client.type === "pkce" ? "none" : "client_secret_basic"); - setTokenEndpointAuthMethod(savedAuthMethod as any); - + const savedAuthMethod = + client.tokenEndpointAuthMethod || + (client.type === "pkce" ? "none" : "client_secret_basic"); + if (isTokenEndpointAuthMethod(savedAuthMethod)) { + setTokenEndpointAuthMethod(savedAuthMethod); + } + if (client.jwksUri) { setJwksUri(client.jwksUri); setJwksSource("uri"); } else if (client.jwks) { - setJwksText(typeof client.jwks === 'string' ? client.jwks : JSON.stringify(client.jwks, null, 2)); + setJwksText( + typeof client.jwks === "string" + ? client.jwks + : JSON.stringify(client.jwks, null, 2), + ); setJwksSource("inline"); } const metadata = client.metadata ?? {}; - if (typeof metadata.description === "string") setDescription(metadata.description); + if (typeof metadata.description === "string") + setDescription(metadata.description); if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url); - + setHeadlessLoginEnabled(!!metadata.headless_login_enabled); - + // Fallbacks from metadata if top-level fields are empty if (!client.tokenEndpointAuthMethod) { - const metaAuth = readMetadataString(metadata, "token_endpoint_auth_method"); - if (metaAuth === "none" || metaAuth === "client_secret_basic" || metaAuth === "private_key_jwt") { - setTokenEndpointAuthMethod(metaAuth); - } + const metaAuth = readMetadataString( + metadata, + "token_endpoint_auth_method", + ); + if (isTokenEndpointAuthMethod(metaAuth)) { + setTokenEndpointAuthMethod(metaAuth); + } } - + if (!client.jwksUri && !client.jwks) { - const metaJwksUri = readMetadataString(metadata, "jwks_uri"); - if (metaJwksUri) { - setJwksUri(metaJwksUri); - setJwksSource("uri"); - } + const metaJwksUri = readMetadataString(metadata, "jwks_uri"); + if (metaJwksUri) { + setJwksUri(metaJwksUri); + setJwksSource("uri"); + } } - - const savedRequestObjectSigningAlg = readMetadataString(metadata, "request_object_signing_alg"); + + const savedRequestObjectSigningAlg = readMetadataString( + metadata, + "request_object_signing_alg", + ); if (savedRequestObjectSigningAlg) { setRequestObjectSigningAlg(savedRequestObjectSigningAlg); } else if (savedAuthMethod === "private_key_jwt") { @@ -191,12 +220,15 @@ function ClientGeneralPage() { } }, [data]); - const securityProfile: SecurityProfile = clientType === "pkce" ? "pkce" : "private"; + const securityProfile: SecurityProfile = + clientType === "pkce" ? "pkce" : "private"; const handleSecurityProfileChange = (profile: SecurityProfile) => { setClientType(profile); if (profile === "pkce") { - setTokenEndpointAuthMethod(headlessLoginEnabled ? "private_key_jwt" : "none"); + setTokenEndpointAuthMethod( + headlessLoginEnabled ? "private_key_jwt" : "none", + ); } else { setTokenEndpointAuthMethod("client_secret_basic"); } @@ -261,15 +293,35 @@ function ClientGeneralPage() { if (headlessLoginEnabled) { if (jwksSource === "uri") { if (!trimmedJwksUri) { - validationErrors.push(t("msg.dev.clients.general.public_key.validation.missing_jwks_uri", "JWKS URI를 입력해야 합니다.")); + validationErrors.push( + t( + "msg.dev.clients.general.public_key.validation.missing_jwks_uri", + "JWKS URI를 입력해야 합니다.", + ), + ); } else if (!isValidUrl(trimmedJwksUri)) { - validationErrors.push(t("msg.dev.clients.general.public_key.validation.invalid_jwks_uri", "JWKS URI 형식이 올바르지 않습니다.")); + validationErrors.push( + t( + "msg.dev.clients.general.public_key.validation.invalid_jwks_uri", + "JWKS URI 형식이 올바르지 않습니다.", + ), + ); } } else if (jwksSource === "inline") { if (!trimmedJwksText) { - validationErrors.push(t("msg.dev.clients.general.public_key.validation.missing_jwks_inline", "공개키(JWKS 또는 SSH-RSA)를 입력해야 합니다.")); + validationErrors.push( + t( + "msg.dev.clients.general.public_key.validation.missing_jwks_inline", + "공개키(JWKS 또는 SSH-RSA)를 입력해야 합니다.", + ), + ); } else if (!isValidJson(trimmedJwksText)) { - validationErrors.push(t("msg.dev.clients.general.public_key.validation.invalid_jwks_inline", "입력값이 유효한 JSON(JWKS) 형식이 아닙니다. SSH-RSA의 경우 'ssh-rsa'로 시작해야 합니다.")); + validationErrors.push( + t( + "msg.dev.clients.general.public_key.validation.invalid_jwks_inline", + "입력값이 유효한 JSON(JWKS) 형식이 아닙니다. SSH-RSA의 경우 'ssh-rsa'로 시작해야 합니다.", + ), + ); } } @@ -288,9 +340,13 @@ function ClientGeneralPage() { const mutation = useMutation({ mutationFn: async () => { const scopeNames = scopes.map((scope) => scope.name).filter(Boolean); - - let finalJwks: any = undefined; - if (tokenEndpointAuthMethod === "private_key_jwt" && jwksSource === "inline" && trimmedJwksText) { + + let finalJwks: ClientUpsertRequest["jwks"]; + if ( + tokenEndpointAuthMethod === "private_key_jwt" && + jwksSource === "inline" && + trimmedJwksText + ) { try { finalJwks = JSON.parse(trimmedJwksText); } catch (e) { @@ -303,7 +359,10 @@ function ClientGeneralPage() { type: clientType, scopes: scopeNames, tokenEndpointAuthMethod, - jwksUri: tokenEndpointAuthMethod === "private_key_jwt" && jwksSource === "uri" ? trimmedJwksUri : undefined, + jwksUri: + tokenEndpointAuthMethod === "private_key_jwt" && jwksSource === "uri" + ? trimmedJwksUri + : undefined, jwks: finalJwks, metadata: { description, @@ -848,22 +907,32 @@ function ClientGeneralPage() { {securityProfile === "pkce" && ( -
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} >
-
-
)} @@ -910,7 +979,10 @@ function ClientGeneralPage() { )}

- + {t("ui.common.enabled", "Enabled")} @@ -984,7 +1056,10 @@ function ClientGeneralPage() { {jwksSource === "uri" && (