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_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 (
|
export const login = async (
|
||||||
params: { username: string; password: string },
|
params: { username: string; password: string },
|
||||||
options?: any
|
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 () => {
|
export const fetchAuthConfig = async () => {
|
||||||
return request<{
|
return request<{
|
||||||
is_saml: boolean;
|
external_auth: ExternalAuth | null;
|
||||||
is_oidc: boolean;
|
|
||||||
first_time_setup: boolean;
|
first_time_setup: boolean;
|
||||||
get_initial_password_command: string;
|
get_initial_password_command: string;
|
||||||
}>(AUTH_CONFIG_API);
|
}>(AUTH_CONFIG_API);
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ interface LocalUserFormProps {
|
|||||||
form: FormInstance;
|
form: FormInstance;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
loginOption: {
|
loginOption: {
|
||||||
saml: boolean;
|
|
||||||
oidc: boolean;
|
|
||||||
first_time_setup: boolean;
|
first_time_setup: boolean;
|
||||||
get_initial_password_command: string;
|
get_initial_password_command: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -182,18 +182,12 @@ const LoginForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginWithThirdParty = () => {
|
const handleLoginWithThirdParty = () => {
|
||||||
if (SSOAuth.options.oidc) {
|
SSOAuth.loginWithExternalAuth();
|
||||||
SSOAuth.loginWithOIDC();
|
|
||||||
} else if (SSOAuth.options.saml) {
|
|
||||||
SSOAuth.loginWithSAML();
|
|
||||||
}
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setAuthError(null);
|
setAuthError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasThirdPartyLogin = useMemo(() => {
|
const hasThirdPartyLogin = !!SSOAuth.options.external_auth;
|
||||||
return SSOAuth.options.oidc || SSOAuth.options.saml;
|
|
||||||
}, [SSOAuth.options]);
|
|
||||||
|
|
||||||
const isThirdPartyAuthHandling = useMemo(() => {
|
const isThirdPartyAuthHandling = useMemo(() => {
|
||||||
return loading && !authError;
|
return loading && !authError;
|
||||||
@@ -205,18 +199,8 @@ const LoginForm = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Buttons>
|
<Buttons>
|
||||||
{SSOAuth.options.oidc && (
|
{SSOAuth.options.external_auth && (
|
||||||
<ButtonWrapper onClick={SSOAuth.loginWithOIDC}>
|
<ButtonWrapper onClick={SSOAuth.loginWithExternalAuth}>
|
||||||
<ButtonText>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: 'common.external.login' },
|
|
||||||
{ type: 'SSO' }
|
|
||||||
)}
|
|
||||||
</ButtonText>
|
|
||||||
</ButtonWrapper>
|
|
||||||
)}
|
|
||||||
{SSOAuth.options.saml && (
|
|
||||||
<ButtonWrapper onClick={SSOAuth.loginWithSAML}>
|
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: 'common.external.login' },
|
{ id: 'common.external.login' },
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
// hooks/useSSOAuth.ts
|
// hooks/useSSOAuth.ts
|
||||||
import { history, useIntl } from '@umijs/max';
|
import { history, useIntl } from '@umijs/max';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import { ExternalAuth, fetchAuthConfig } from '../apis';
|
||||||
AUTH_OIDC_LOGIN_API,
|
|
||||||
AUTH_SAML_LOGIN_API,
|
|
||||||
fetchAuthConfig
|
|
||||||
} from '../apis';
|
|
||||||
|
|
||||||
type LoginOption = {
|
type LoginOption = {
|
||||||
saml: boolean;
|
// Active external auth provider, or ``null`` when only local login is
|
||||||
oidc: boolean;
|
// 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;
|
first_time_setup: boolean;
|
||||||
get_initial_password_command: string;
|
get_initial_password_command: string;
|
||||||
};
|
};
|
||||||
@@ -26,8 +24,7 @@ export function useSSOAuth({
|
|||||||
onLoading?: (loading: boolean) => void;
|
onLoading?: (loading: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const [loginOption, setLoginOption] = useState<LoginOption>({
|
const [loginOption, setLoginOption] = useState<LoginOption>({
|
||||||
saml: false,
|
external_auth: null,
|
||||||
oidc: false,
|
|
||||||
first_time_setup: false,
|
first_time_setup: false,
|
||||||
get_initial_password_command: ''
|
get_initial_password_command: ''
|
||||||
});
|
});
|
||||||
@@ -38,29 +35,28 @@ export function useSSOAuth({
|
|||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const sso = params.get('sso');
|
const sso = params.get('sso');
|
||||||
|
|
||||||
const oidcLogin = () => {
|
const loginWithExternalAuth = (auth: ExternalAuth | null) => {
|
||||||
window.location.href = AUTH_OIDC_LOGIN_API;
|
if (auth) {
|
||||||
};
|
window.location.href = auth.login_url;
|
||||||
|
}
|
||||||
const samlLogin = () => {
|
|
||||||
window.location.href = AUTH_SAML_LOGIN_API;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
const { is_oidc, is_saml, ...rest } = await fetchAuthConfig();
|
const { external_auth, ...rest } = await fetchAuthConfig();
|
||||||
setLoginOption({
|
setLoginOption({
|
||||||
...rest,
|
...rest,
|
||||||
oidc: !!is_oidc,
|
external_auth: external_auth ?? null
|
||||||
saml: !!is_saml
|
|
||||||
});
|
});
|
||||||
if (sso) {
|
if (sso) {
|
||||||
onLoading?.(true);
|
onLoading?.(true);
|
||||||
if (is_oidc) {
|
if (external_auth) {
|
||||||
oidcLogin();
|
loginWithExternalAuth(external_auth);
|
||||||
} else if (is_saml) {
|
|
||||||
samlLogin();
|
|
||||||
} else {
|
} 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?.(
|
onError?.(
|
||||||
new Error(intl.formatMessage({ id: 'common.sso.noConfig' }))
|
new Error(intl.formatMessage({ id: 'common.sso.noConfig' }))
|
||||||
);
|
);
|
||||||
@@ -68,12 +64,15 @@ export function useSSOAuth({
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setLoginOption({
|
setLoginOption({
|
||||||
oidc: false,
|
external_auth: null,
|
||||||
saml: false,
|
|
||||||
first_time_setup: false,
|
first_time_setup: false,
|
||||||
get_initial_password_command: ''
|
get_initial_password_command: ''
|
||||||
});
|
});
|
||||||
onLoading?.(false);
|
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 {
|
return {
|
||||||
isSSOLogin: !!sso,
|
isSSOLogin: !!sso,
|
||||||
options: loginOption,
|
options: loginOption,
|
||||||
loginWithOIDC: oidcLogin,
|
loginWithExternalAuth: () =>
|
||||||
loginWithSAML: samlLogin
|
loginWithExternalAuth(loginOption.external_auth)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,15 @@ export interface LoginKit {
|
|||||||
};
|
};
|
||||||
useSSOAuth: (opts: any) => {
|
useSSOAuth: (opts: any) => {
|
||||||
options: {
|
options: {
|
||||||
saml: boolean;
|
// Active external auth provider (e.g. ``{type: "CAS", login_url:
|
||||||
oidc: boolean;
|
// "/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;
|
first_time_setup: boolean;
|
||||||
get_initial_password_command: string;
|
get_initial_password_command: string;
|
||||||
};
|
};
|
||||||
loginWithOIDC: () => void;
|
loginWithExternalAuth: () => void;
|
||||||
loginWithSAML: () => void;
|
|
||||||
};
|
};
|
||||||
userInfo: any;
|
userInfo: any;
|
||||||
setUserInfo: (info: any) => void;
|
setUserInfo: (info: any) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user