feat(login): render SSO button data-driven from /auth/config

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.
This commit is contained in:
gitlawr
2026-06-24 18:59:19 +08:00
committed by jialin
parent ee42a9d0ed
commit 36a1038d12
5 changed files with 45 additions and 57 deletions
+10 -5
View File
@@ -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);
@@ -45,8 +45,6 @@ interface LocalUserFormProps {
form: FormInstance;
loading?: boolean;
loginOption: {
saml: boolean;
oidc: boolean;
first_time_setup: boolean;
get_initial_password_command: string;
};
+4 -20
View File
@@ -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 (
<Buttons>
{SSOAuth.options.oidc && (
<ButtonWrapper onClick={SSOAuth.loginWithOIDC}>
<ButtonText>
{intl.formatMessage(
{ id: 'common.external.login' },
{ type: 'SSO' }
)}
</ButtonText>
</ButtonWrapper>
)}
{SSOAuth.options.saml && (
<ButtonWrapper onClick={SSOAuth.loginWithSAML}>
{SSOAuth.options.external_auth && (
<ButtonWrapper onClick={SSOAuth.loginWithExternalAuth}>
<ButtonText>
{intl.formatMessage(
{ id: 'common.external.login' },
+25 -26
View File
@@ -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<LoginOption>({
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)
};
}
+6 -4
View File
@@ -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;