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:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user