From 36a1038d1292d8deb449c55f2ba6a2762fb7236b Mon Sep 17 00:00:00 2001 From: gitlawr Date: Wed, 24 Jun 2026 12:02:24 +0800 Subject: [PATCH] feat(login): render SSO button data-driven from /auth/config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend now advertises the active external auth provider on ``/auth/config`` as a single ``external_auth: {type, login_url} | null`` field (replacing the per-provider ``is_oidc`` / ``is_saml`` booleans). This is the API needed to add CAS without per-provider UI conditionals. Wire the login UI accordingly: - ``useSSOAuth`` exposes a single ``loginWithExternalAuth()`` action and ``options.external_auth`` carrying the provider info; the OIDC- and SAML-specific exports are gone. - ``LoginForm`` renders one SSO button whenever ``external_auth`` is set, navigating to ``login_url``. New providers (CAS, future LDAP / Azure AD / …) need zero UI changes — only a backend route. - ``LocalUserForm`` and ``LoginKit`` type definitions drop the per-provider booleans. --- src/pages/login/apis/index.ts | 15 ++++-- .../login/components/local-user-form.tsx | 2 - src/pages/login/components/login-form.tsx | 24 ++------- src/pages/login/hooks/use-sso-auth.ts | 51 +++++++++---------- src/plugins/types.ts | 10 ++-- 5 files changed, 45 insertions(+), 57 deletions(-) diff --git a/src/pages/login/apis/index.ts b/src/pages/login/apis/index.ts index 3a7433bf..af223223 100644 --- a/src/pages/login/apis/index.ts +++ b/src/pages/login/apis/index.ts @@ -9,9 +9,6 @@ export const AUTH_API = '/auth'; export const AUTH_CONFIG_API = '/auth/config'; -export const AUTH_OIDC_LOGIN_API = '/auth/oidc/login'; -export const AUTH_SAML_LOGIN_API = '/auth/saml/login'; - export const login = async ( params: { username: string; password: string }, options?: any @@ -53,10 +50,18 @@ export const updatePassword = async (params: any) => { }); }; +export type ExternalAuth = { + // Provider kind (``OIDC`` / ``SAML`` / ``CAS`` / …). Stays a free-form + // string so adding a new provider on the backend doesn't require a + // TypeScript change here. + type: string; + // Browser-facing login URL the SSO button should navigate to. + login_url: string; +}; + export const fetchAuthConfig = async () => { return request<{ - is_saml: boolean; - is_oidc: boolean; + external_auth: ExternalAuth | null; first_time_setup: boolean; get_initial_password_command: string; }>(AUTH_CONFIG_API); diff --git a/src/pages/login/components/local-user-form.tsx b/src/pages/login/components/local-user-form.tsx index ba38106e..b5ab4720 100644 --- a/src/pages/login/components/local-user-form.tsx +++ b/src/pages/login/components/local-user-form.tsx @@ -45,8 +45,6 @@ interface LocalUserFormProps { form: FormInstance; loading?: boolean; loginOption: { - saml: boolean; - oidc: boolean; first_time_setup: boolean; get_initial_password_command: string; }; diff --git a/src/pages/login/components/login-form.tsx b/src/pages/login/components/login-form.tsx index 47c999b1..5139d325 100644 --- a/src/pages/login/components/login-form.tsx +++ b/src/pages/login/components/login-form.tsx @@ -182,18 +182,12 @@ const LoginForm = () => { }; const handleLoginWithThirdParty = () => { - if (SSOAuth.options.oidc) { - SSOAuth.loginWithOIDC(); - } else if (SSOAuth.options.saml) { - SSOAuth.loginWithSAML(); - } + SSOAuth.loginWithExternalAuth(); setLoading(true); setAuthError(null); }; - const hasThirdPartyLogin = useMemo(() => { - return SSOAuth.options.oidc || SSOAuth.options.saml; - }, [SSOAuth.options]); + const hasThirdPartyLogin = !!SSOAuth.options.external_auth; const isThirdPartyAuthHandling = useMemo(() => { return loading && !authError; @@ -205,18 +199,8 @@ const LoginForm = () => { return ( - {SSOAuth.options.oidc && ( - - - {intl.formatMessage( - { id: 'common.external.login' }, - { type: 'SSO' } - )} - - - )} - {SSOAuth.options.saml && ( - + {SSOAuth.options.external_auth && ( + {intl.formatMessage( { id: 'common.external.login' }, diff --git a/src/pages/login/hooks/use-sso-auth.ts b/src/pages/login/hooks/use-sso-auth.ts index c4c5a95e..33784a0c 100644 --- a/src/pages/login/hooks/use-sso-auth.ts +++ b/src/pages/login/hooks/use-sso-auth.ts @@ -1,15 +1,13 @@ // hooks/useSSOAuth.ts import { history, useIntl } from '@umijs/max'; import { useEffect, useState } from 'react'; -import { - AUTH_OIDC_LOGIN_API, - AUTH_SAML_LOGIN_API, - fetchAuthConfig -} from '../apis'; +import { ExternalAuth, fetchAuthConfig } from '../apis'; type LoginOption = { - saml: boolean; - oidc: boolean; + // Active external auth provider, or ``null`` when only local login is + // configured. Drives the SSO button: when set, render a button that + // navigates to ``external_auth.login_url``. + external_auth: ExternalAuth | null; first_time_setup: boolean; get_initial_password_command: string; }; @@ -26,8 +24,7 @@ export function useSSOAuth({ onLoading?: (loading: boolean) => void; }) { const [loginOption, setLoginOption] = useState({ - saml: false, - oidc: false, + external_auth: null, first_time_setup: false, get_initial_password_command: '' }); @@ -38,29 +35,28 @@ export function useSSOAuth({ const params = new URLSearchParams(location.search); const sso = params.get('sso'); - const oidcLogin = () => { - window.location.href = AUTH_OIDC_LOGIN_API; - }; - - const samlLogin = () => { - window.location.href = AUTH_SAML_LOGIN_API; + const loginWithExternalAuth = (auth: ExternalAuth | null) => { + if (auth) { + window.location.href = auth.login_url; + } }; const init = async () => { try { - const { is_oidc, is_saml, ...rest } = await fetchAuthConfig(); + const { external_auth, ...rest } = await fetchAuthConfig(); setLoginOption({ ...rest, - oidc: !!is_oidc, - saml: !!is_saml + external_auth: external_auth ?? null }); if (sso) { onLoading?.(true); - if (is_oidc) { - oidcLogin(); - } else if (is_saml) { - samlLogin(); + if (external_auth) { + loginWithExternalAuth(external_auth); } else { + // ``?sso`` deep-link landed on a server with no external auth + // configured. Surface the error AND release the loading + // state — otherwise the form is stuck on the spinner. + onLoading?.(false); onError?.( new Error(intl.formatMessage({ id: 'common.sso.noConfig' })) ); @@ -68,12 +64,15 @@ export function useSSOAuth({ } } catch (error: any) { setLoginOption({ - oidc: false, - saml: false, + external_auth: null, first_time_setup: false, get_initial_password_command: '' }); onLoading?.(false); + // ``fetchAuthConfig`` failed (network, server 5xx, …). Without + // propagating, the login UI silently falls back to local-only — + // which can mask a real ``?sso`` redirect failure. + onError?.(error); } }; @@ -84,7 +83,7 @@ export function useSSOAuth({ return { isSSOLogin: !!sso, options: loginOption, - loginWithOIDC: oidcLogin, - loginWithSAML: samlLogin + loginWithExternalAuth: () => + loginWithExternalAuth(loginOption.external_auth) }; } diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 6b7b1251..20d7682e 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -20,13 +20,15 @@ export interface LoginKit { }; useSSOAuth: (opts: any) => { options: { - saml: boolean; - oidc: boolean; + // Active external auth provider (e.g. ``{type: "CAS", login_url: + // "/auth/cas/login"}``) or ``null`` when only local login is + // configured. The login UI renders an SSO button only when this + // is non-null and navigates to ``login_url``. + external_auth: { type: string; login_url: string } | null; first_time_setup: boolean; get_initial_password_command: string; }; - loginWithOIDC: () => void; - loginWithSAML: () => void; + loginWithExternalAuth: () => void; }; userInfo: any; setUserInfo: (info: any) => void;