add sso login logic

This commit is contained in:
forrestzhou
2025-08-12 17:19:04 +08:00
committed by jialin
parent 7f1bc33eed
commit 4bdde365cf
5 changed files with 12638 additions and 14876 deletions
+12452 -14747
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -6,8 +6,8 @@ export default {
'common.button.shortcut': 'Keyboard Shortcut', 'common.button.shortcut': 'Keyboard Shortcut',
'common.button.add': 'Add', 'common.button.add': 'Add',
'common.button.login': 'Use a local user', 'common.button.login': 'Use a local user',
'common.button.oidclogin': 'Login In With OIDC', 'common.button.oidclogin': 'Log In With OIDC',
'common.button.samllogin': 'Login In With SAML', 'common.button.samllogin': 'Log In With SAML',
'common.button.select': 'Select', 'common.button.select': 'Select',
'common.button.selected': 'Selected', 'common.button.selected': 'Selected',
'common.button.continue': 'Continue', 'common.button.continue': 'Continue',
+2 -2
View File
@@ -6,8 +6,8 @@ export default {
'common.button.shortcut': '快捷键', 'common.button.shortcut': '快捷键',
'common.button.add': '添加', 'common.button.add': '添加',
'common.button.login': 'Use a local user', 'common.button.login': 'Use a local user',
'common.button.oidclogin': 'Login In With OIDC', 'common.button.oidclogin': 'Log In With OIDC',
'common.button.samllogin': 'Login In With SAML', 'common.button.samllogin': 'Log In With SAML',
'common.button.select': '选择', 'common.button.select': '选择',
'common.button.selected': '已选择', 'common.button.selected': '已选择',
'common.button.continue': '继续', 'common.button.continue': '继续',
+167 -125
View File
@@ -12,17 +12,16 @@ import {
removeRememberMe removeRememberMe
} from '@/utils/localstore/index'; } from '@/utils/localstore/index';
import { LockOutlined, UserOutlined } from '@ant-design/icons'; import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { useIntl, useModel } from '@umijs/max'; import { history, useIntl, useModel } from '@umijs/max';
import { Button, Checkbox, Form } from 'antd'; import { Button, Checkbox, Form } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import CryptoJS from 'crypto-js'; import CryptoJS from 'crypto-js';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
import { login } from '../apis'; import { login } from '../apis';
import { checkDefaultPage } from '../utils'; import { checkDefaultPage } from '../utils';
const authConfig = await fetchAuthConfig('/get_config'); // get authentication configuration
const useStyles = createStyles(({ token, css }) => ({ const useStyles = createStyles(({ token, css }) => ({
header: css` header: css`
display: flex; display: flex;
@@ -42,25 +41,37 @@ const useStyles = createStyles(({ token, css }) => ({
` `
})); }));
// function authentication configuration method // function authentication configuration method
async function fetchAuthConfig(url) { async function fetchAuthConfig(url: string) {
try { try {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); return response.json();
} catch (error) { } catch (error) {
console.error('OIDC config error:', error); console.error('OIDC config error:', error);
throw error; throw error;
} }
}; }
const LoginForm = () => { const LoginForm = () => {
type LoginOption = {
saml?: boolean;
oidc?: boolean;
};
const [err, setErr] = useState<Error | null>(null);
const [loginOption, setLoginOption] = useState<LoginOption>({
saml: false,
oidc: false
});
const { styles } = useStyles(); const { styles } = useStyles();
const [userInfo, setUserInfo] = useAtom(userAtom); const [userInfo, setUserInfo] = useAtom(userAtom);
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom); const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
const { initialState, setInitialState } = useModel('@@initialState') || {}; const { initialState, setInitialState } = useModel('@@initialState') || {};
const intl = useIntl(); const intl = useIntl();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { location } = history;
const params = new URLSearchParams(location.search);
const sso = params.get('sso'); // OIDC callback information
const renderWelCome = useMemo(() => { const renderWelCome = useMemo(() => {
return ( return (
<div <div
@@ -83,7 +94,6 @@ const LoginForm = () => {
</div> </div>
); );
}, [intl]); }, [intl]);
const gotoDefaultPage = async (userInfo: any) => { const gotoDefaultPage = async (userInfo: any) => {
checkDefaultPage(userInfo, true); checkDefaultPage(userInfo, true);
}; };
@@ -134,36 +144,46 @@ const LoginForm = () => {
form.setFieldsValue({ username, password, autoLogin: true }); form.setFieldsValue({ username, password, autoLogin: true });
} }
}; };
// OIDC certification // OIDC certification
const handleOidcLogin = async () => { const handleOidcLogin = async () => {
const authUrl = `${authConfig.base_entrypoint}auth?response_type=code&client_id=${authConfig.CLIENT_ID}&redirect_uri=${authConfig.redirect_uri}&scope=openid profile email&state=random_state_string`; window.location.href = '/auth/oidc/login';
window.location.href = authUrl;}; };
// SAML certification // SAML certification
const handleSamlLogin = async () => { const handleSamlLogin = async () => {
window.location.href = "/auth/saml/login";} window.location.href = '/auth/saml/login';
// Handling certification callbacks };
useEffect(() => {
const params = new URLSearchParams(window.location.search); // Handling SSO callbacks
const code = params.get('code'); // OIDC callback information useEffect(() => {
console.log(code) fetchAuthConfig('/get_config')
const samlResponse = params.get('SAMLResponse'); // SAML callback information .then((authConfig) => {
const allParams = Object.fromEntries(params.entries()); if (authConfig.is_oidc) {
history.replaceState({}, '', window.location.pathname); setLoginOption({
if (code) { login({ oidc: true
code: code });
}).then(async () => { } else if (authConfig.is_saml) {
const userInfo = await fetchUserInfo(); if (authConfig.is_saml) {
await setUserInfo(userInfo); setLoginOption({
gotoDefaultPage(userInfo); saml: true
}); });
}; }
if (samlResponse) { login({ }
SAMLResponse: decodeURIComponent(samlResponse) })
}).then(async () => { .catch((error) => {
const userInfo = await fetchUserInfo(); setLoginOption({ oidc: false, saml: false });
await setUserInfo(userInfo); });
gotoDefaultPage(userInfo); if (sso) {
})}; fetchUserInfo()
.then((userInfo) => {
setUserInfo(userInfo);
gotoDefaultPage({});
})
.catch((error) => {
console.log(error);
setErr(error);
});
}
}, []); }, []);
const handleLogin = async (values: any) => { const handleLogin = async (values: any) => {
try { try {
@@ -193,103 +213,125 @@ const LoginForm = () => {
callGetRememberMe(); callGetRememberMe();
}, []); }, []);
return ( if (sso && !err) {
<div> return <div>Handle SSO callback...</div>;
<div className={styles.header}> } else if (err) {
<ThemeDropActions></ThemeDropActions> return (
<LangSelect /> <div style={{ color: 'red' }}>Error to log in by SSO: {err.message}</div>
</div> );
} else {
return (
<div> <div>
<Form <div className={styles.header}>
form={form} <ThemeDropActions></ThemeDropActions>
style={{ width: '360px', margin: '0 auto' }} <LangSelect />
onFinish={handleLogin} </div>
> <div>
{renderWelCome} <Form
<Form.Item form={form}
name="username" style={{ width: '360px', margin: '0 auto' }}
rules={[ onFinish={handleLogin}
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{ name: intl.formatMessage({ id: 'common.form.username' }) }
)
}
]}
> >
<SealInput.Input {renderWelCome}
label={intl.formatMessage({ id: 'common.form.username' })} <Form.Item
prefix={<UserOutlined />} name="username"
/> rules={[
</Form.Item> {
required: true,
<Form.Item message: intl.formatMessage(
name="password" { id: 'common.form.rule.input' },
rules={[ { name: intl.formatMessage({ id: 'common.form.username' }) }
{ )
required: true, }
message: intl.formatMessage( ]}
{ id: 'common.form.rule.input' },
{ name: intl.formatMessage({ id: 'common.form.password' }) }
)
}
]}
>
<SealInput.Password
prefix={<LockOutlined />}
label={intl.formatMessage({ id: 'common.form.password' })}
/>
</Form.Item>
<div
className="flex-center flex-between"
style={{
marginBottom: 24
}}
>
<Form.Item noStyle name="autoLogin" valuePropName="checked">
<Checkbox style={{ marginLeft: 5 }}>
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
{intl.formatMessage({ id: 'common.login.rember' })}
</span>
</Checkbox>
</Form.Item>
<Button
type="link"
size="small"
href={externalLinks.resetPassword}
target="_blank"
style={{ padding: 0 }}
> >
{intl.formatMessage({ id: 'common.button.forgotpassword' })} <SealInput.Input
</Button> label={intl.formatMessage({ id: 'common.form.username' })}
</div> prefix={<UserOutlined />}
<Button onClick={handleOidcLogin} />
type="primary" </Form.Item>
block
style={{ height: '48px', fontSize: '14px', display: authConfig?.is_oidc ? 'block': 'none'}} <Form.Item
> name="password"
{intl.formatMessage({ id: 'common.button.oidclogin' })} rules={[
</Button> {
<Button onClick={handleSamlLogin} required: true,
type="primary" message: intl.formatMessage(
block { id: 'common.form.rule.input' },
style={{ height: '48px', fontSize: '14px', display: authConfig?.is_saml ? 'block': 'none'}} { name: intl.formatMessage({ id: 'common.form.password' }) }
> )
{intl.formatMessage({ id: 'common.button.samllogin' })} }
</Button> ]}
<Button >
htmlType="submit" <SealInput.Password
prefix={<LockOutlined />}
label={intl.formatMessage({ id: 'common.form.password' })}
/>
</Form.Item>
<div
className="flex-center flex-between"
style={{
marginBottom: 24
}}
>
<Form.Item noStyle name="autoLogin" valuePropName="checked">
<Checkbox style={{ marginLeft: 5 }}>
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
{intl.formatMessage({ id: 'common.login.rember' })}
</span>
</Checkbox>
</Form.Item>
<Button
type="link" type="link"
block size="small"
style={{ height: '48px', fontSize: '14px' }} href={externalLinks.resetPassword}
target="_blank"
style={{ padding: 0 }}
> >
{intl.formatMessage({ id: 'common.button.login' })} {intl.formatMessage({ id: 'common.button.forgotpassword' })}
</Button> </Button>
</Form> </div>
<Button
onClick={handleOidcLogin}
type="primary"
block
style={{
height: '48px',
fontSize: '14px',
display: loginOption.oidc ? 'block' : 'none'
}}
>
{intl.formatMessage({ id: 'common.button.oidclogin' })}
</Button>
<Button
onClick={handleSamlLogin}
type="primary"
block
style={{
height: '48px',
fontSize: '14px',
display: loginOption.saml ? 'block' : 'none'
}}
>
{intl.formatMessage({ id: 'common.button.samllogin' })}
</Button>
<Button
htmlType="submit"
type={
loginOption.oidc === false && loginOption.saml === false
? 'primary'
: 'link'
}
block
style={{ height: '48px', fontSize: '14px' }}
>
{intl.formatMessage({ id: 'common.button.login' })}
</Button>
</Form>
</div>
</div> </div>
</div> );
); }
}; };
export default LoginForm; export default LoginForm;
+15
View File
@@ -277,6 +277,21 @@ const Users: React.FC = () => {
); );
}} }}
/> />
<Column
title={intl.formatMessage({ id: 'users.form.source' })}
dataIndex="source"
key="source"
ellipsis={{
showTitle: false
}}
render={(text, record) => {
return (
<AutoTooltip ghost minWidth={20}>
{text}
</AutoTooltip>
);
}}
/>
<Column <Column
title={intl.formatMessage({ id: 'common.table.createTime' })} title={intl.formatMessage({ id: 'common.table.createTime' })}
dataIndex="created_at" dataIndex="created_at"