add sso login logic
This commit is contained in:
Generated
+12452
-14747
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||||
|
|||||||
@@ -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': '继续',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user