refactor: login form based on sso auth

main
jialin 5 months ago
parent 8f50af917f
commit cf70bd2dc1

File diff suppressed because it is too large Load Diff

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755020187776" class="icon" viewBox="0 0 1105 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7392" xmlns:xlink="http://www.w3.org/1999/xlink" width="138.125" height="128"><path d="M507.2896 133.44768l130.98496-71.4496v820.9408l-130.97984 69.71392V133.44768z" fill="#F38019" p-id="7393"></path><path d="M479.39072 323.968a724.30592 724.30592 0 0 0-77.056 16.6144C118.35904 417.01376 37.77536 603.8016 67.95776 701.696c30.11584 97.8944 161.03424 263.2192 439.2704 250.19904 0.13824-27.41248 0.13824-57.73824 0-90.89536-200.49408-26.37824-302.67904-88.82688-306.55488-187.40736-5.74464-147.74272 134.1696-216.90368 217.87136-229.0176 16.82432-2.49344 37.66272-5.67808 61.19936-8.6528l-0.27648-111.9488h-0.07168z m185.12384 107.65312c52.1984 5.6832 103.57248 18.69312 147.0464 44.44672-10.65984 5.19168-34.26816 19.73248-70.82496 43.47904l290.49344 66.39104-16.54272-206.58176-78.37184 44.30336c-116.10112-57.8048-205.19936-91.0336-267.29984-99.6864l-4.50048 107.648z" fill="#ADAFB3" p-id="7394"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<g fill="#C22E33">
<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -1,72 +0,0 @@
.alert-info-block {
padding-block: 6px;
padding-inline: 10px 16px;
position: relative;
padding-left: 32px;
text-align: left;
border-radius: var(--border-radius-base);
margin: 0;
border: 1px solid transparent;
.ant-typography {
margin-bottom: 0;
}
&.danger {
border-color: var(--ant-color-error-border);
background-color: var(--ant-color-error-bg);
}
&.warning {
border-color: var(--ant-color-warning-border);
background-color: var(--ant-color-warning-bg);
}
&.transition {
color: var(--ant-geekblue-7);
background: var(--ant-geekblue-1);
border-color: var(--ant-geekblue-3);
}
&.success {
border: 1px solid var(--ant-color-success);
color: var(--ant-color-success);
background: var(--ant-color-success-bg);
.content.success {
font-weight: var(--font-weight-normal);
}
}
.title {
position: absolute;
left: 0;
top: 0;
display: flex;
height: 32px;
padding: 5px 10px;
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0;
.info-icon {
&.danger {
color: var(--ant-color-error);
}
&.warning {
color: var(--ant-color-warning);
}
&.transition {
color: var(--ant-geekblue-7);
}
&.success {
color: var(--ant-color-success);
}
}
.text {
font-weight: var(--font-weight-bold);
}
}
}

@ -1,10 +1,10 @@
import { WarningFilled } from '@ant-design/icons';
import { Typography } from 'antd';
import { createStyles } from 'antd-style';
import classNames from 'classnames';
import React from 'react';
import styled from 'styled-components';
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
import './block.less';
interface AlertInfoProps {
type: Global.MessageType;
message: React.ReactNode;
@ -18,6 +18,81 @@ interface AlertInfoProps {
overlayScrollerProps?: OverlayScrollerOptions;
}
const useStyles = createStyles(({ token, css }) => {
return {
alertBlockInfo: css`
padding-block: 6px;
padding-inline: 10px 16px;
position: relative;
padding-left: 32px;
text-align: left;
border-radius: ${token.borderRadius}px;
margin: 0;
border: 1px solid transparent;
.ant-typography {
margin-bottom: 0;
}
&.danger {
border-color: ${token.colorErrorBorder};
background-color: ${token.colorErrorBg};
}
&.warning {
border-color: ${token.colorWarningBorder};
background-color: ${token.colorWarningBg};
}
&.transition {
color: ${token.geekblue7};
background: ${token.geekblue1};
border-color: ${token.geekblue3};
}
&.success {
border: 1px solid ${token.colorSuccess};
color: ${token.colorSuccessText};
background: ${token.colorSuccessBg};
.content.success {
font-weight: var(--font-weight-normal);
}
}
.title {
position: absolute;
left: 0;
top: 0;
display: flex;
height: 32px;
padding: 5px 10px;
border-radius: ${token.borderRadius}px ${token.borderRadius}px 0 0;
.info-icon {
&.danger {
color: ${token.colorErrorText};
}
&.warning {
color: ${token.colorWarningText};
}
&.transition {
color: ${token.geekblue7};
}
&.success {
color: ${token.colorSuccessText};
}
}
.text {
font-weight: var(--font-weight-bold);
}
}
`
};
});
const TitleWrapper = styled.div`
font-weight: 700;
color: var(--ant-color-text);
@ -46,12 +121,12 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
maxHeight = 86,
overlayScrollerProps = {}
} = props;
const { styles } = useStyles();
return (
<>
{message ? (
<div
className={classNames('alert-info-block', type)}
className={classNames(styles.alertBlockInfo, type)}
style={{ ...style }}
>
<Typography.Paragraph

@ -0,0 +1,26 @@
import { CloseOutlined } from '@ant-design/icons';
import { createStyles } from 'antd-style/lib/functions';
const useStyles = createStyles(({ token, css }) => ({
errorIcon: css`
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
border-radius: 50%;
background-color: ${token.colorErrorBgActive};
color: ${token.colorError};
`
}));
const ErrorIcon: React.FC<{ size?: number }> = ({ size }) => {
const { styles } = useStyles();
return (
<div className={styles.errorIcon}>
<CloseOutlined style={{ fontSize: size || 30 }} />
</div>
);
};
export default ErrorIcon;

@ -5,9 +5,10 @@ export default {
'common.button.editmode': 'Edit mode',
'common.button.shortcut': 'Keyboard Shortcut',
'common.button.add': 'Add',
'common.button.login': 'Use a local user',
'common.button.oidclogin': 'Log In With OIDC',
'common.button.samllogin': 'Log In With SAML',
'common.button.login': 'Log in',
'common.button.login.local': 'Log in with local user',
'common.button.oidclogin': 'Log in with OIDC',
'common.button.samllogin': 'Log in with SAML',
'common.button.select': 'Select',
'common.button.selected': 'Selected',
'common.button.continue': 'Continue',
@ -253,5 +254,8 @@ export default {
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection',
'common.select.count': '{count} selected'
'common.select.count': '{count} selected',
'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed',
'common.login.thirdparty': 'or login with'
};

@ -6,8 +6,9 @@ export default {
'common.button.shortcut': 'キーボードショートカット',
'common.button.add': '追加',
'common.button.login': 'ログイン',
'common.button.oidclogin': 'OIDCでログイン',
'common.button.samllogin': 'SAMLでログイン',
'common.button.login.local': 'ローカルユーザーでログイン',
'common.button.oidclogin': 'OIDC でログイン',
'common.button.samllogin': 'SAML でログイン',
'common.button.select': '選択',
'common.button.selected': '選択済み',
'common.button.continue': '続行',
@ -253,7 +254,10 @@ export default {
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection',
'common.select.count': '{count} selected'
'common.select.count': '{count} selected',
'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed',
'common.login.thirdparty': 'or login with'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@ -271,5 +275,8 @@ export default {
// 12. 'common.page.refresh.tips': 'Oops! Something went wrong. Try refreshing the page.'
// 13. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.'
// 14. 'common.button.clearSelection': 'Clear Selection',
// 15. 'common.select.count': '{count} selected'
// 15. 'common.select.count': '{count} selected',
// 16. 'common.login.auth': 'Authenticating...',
// 17. 'common.login.auth.failed': 'Authentication failed'
// 18. 'common.login.thirdparty': 'or login with'
// ========== End of To-Do List ==========

@ -6,6 +6,7 @@ export default {
'common.button.shortcut': 'Сочетания клавиш',
'common.button.add': 'Добавить',
'common.button.login': 'Войти',
'common.button.login.local': 'Войти с локальным пользователем',
'common.button.oidclogin': 'Войти с OIDC',
'common.button.samllogin': 'Войти с SAML',
'common.button.select': 'Выбрать',
@ -252,9 +253,12 @@ export default {
'common.tips.escape.disable':
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
'common.button.clearSelection': 'Сбросить выбор',
'common.select.count': '{count} Выбрано'
'common.select.count': '{count} Выбрано',
'common.login.auth': 'Аутентификация...',
'common.login.auth.failed': 'Ошибка аутентификации',
'common.login.thirdparty': 'or login with'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'common.login.thirdparty': 'or login with'
// ========== End of To-Do List ==========

@ -5,9 +5,10 @@ export default {
'common.button.editmode': '编辑模式',
'common.button.shortcut': '快捷键',
'common.button.add': '添加',
'common.button.login': 'Use a local user',
'common.button.oidclogin': 'Log In With OIDC',
'common.button.samllogin': 'Log In With SAML',
'common.button.login': '登录',
'common.button.login.local': '本地用户登录',
'common.button.oidclogin': '使用 OIDC 登录',
'common.button.samllogin': '使用 SAML 登录',
'common.button.select': '选择',
'common.button.selected': '已选择',
'common.button.continue': '继续',
@ -246,5 +247,8 @@ export default {
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
'common.button.clearSelection': '清除选择',
'common.select.count': '已选 {count} 项'
'common.select.count': '已选 {count} 项',
'common.login.auth': '认证中...',
'common.login.auth.failed': '认证失败',
'common.login.thirdparty': '或使用以下方式登录'
};

@ -1,27 +1,47 @@
import LogoIcon from '@/assets/images/gpustack-logo.png';
import { initialPasswordAtom, userAtom } from '@/atoms/user';
import OIDCIcon from '@/assets/images/oidc.svg';
import SAMLIcon from '@/assets/images/saml.svg';
import { userAtom } from '@/atoms/user';
import LangSelect from '@/components/lang-select';
import SealInput from '@/components/seal-form/seal-input';
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
import externalLinks from '@/constants/external-links';
import {
CRYPT_TEXT,
REMEMBER_ME_KEY,
getRememberMe,
rememberMe,
removeRememberMe
} from '@/utils/localstore/index';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { history, useIntl, useModel } from '@umijs/max';
import { Button, Checkbox, Form } from 'antd';
import { useIntl, useModel } from '@umijs/max';
import { Button, Checkbox, Divider, Form, Spin, Tooltip, message } from 'antd';
import { createStyles } from 'antd-style';
import CryptoJS from 'crypto-js';
import { useAtom } from 'jotai';
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { flushSync } from 'react-dom';
import { login } from '../apis';
import styled from 'styled-components';
import { useLocalAuth } from '../hooks/use-local-auth';
import { useSSOAuth } from '../hooks/use-sso-auth';
import { checkDefaultPage } from '../utils';
const Buttons = styled.div`
display: flex;
justify-content: center;
align-items: center;
gap: 32px;
`;
const ButtonWrapper = styled(Button).attrs({
shape: 'circle',
size: 'large',
color: 'default',
variant: 'filled'
})`
height: 42px;
width: 42px;
`;
const DividerWrapper = styled(Divider)`
margin-block: 24px !important;
.ant-divider-inner-text {
color: var(--ant-color-text-secondary);
}
`;
const useStyles = createStyles(({ token, css }) => ({
header: css`
display: flex;
@ -38,40 +58,28 @@ const useStyles = createStyles(({ token, css }) => ({
.anticon:hover {
color: ${token.colorTextTertiary};
}
`,
errorMessage: css`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
color: ${token.colorText};
.title {
font-weight: bold;
}
`
}));
// function authentication configuration method
async function fetchAuthConfig(url: string) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
} catch (error) {
console.error('OIDC config error:', error);
throw error;
}
}
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 [messageApi, contextHolder] = message.useMessage();
const { styles } = useStyles();
const [userInfo, setUserInfo] = useAtom(userAtom);
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
const { initialState, setInitialState } = useModel('@@initialState') || {};
const [authError, setAuthError] = useState<Error | null>(null);
const intl = useIntl();
const [form] = Form.useForm();
const { location } = history;
const params = new URLSearchParams(location.search);
const sso = params.get('sso'); // OIDC callback information
const renderWelCome = useMemo(() => {
return (
<div
@ -94,6 +102,7 @@ const LoginForm = () => {
</div>
);
}, [intl]);
const gotoDefaultPage = async (userInfo: any) => {
checkDefaultPage(userInfo, true);
};
@ -112,121 +121,97 @@ const LoginForm = () => {
return userInfo;
};
const encryptPassword = (password: string) => {
const psw = CryptoJS.AES?.encrypt?.(password, CRYPT_TEXT).toString();
return psw;
};
const decryptPassword = (password: string) => {
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
const res = bytes.toString(CryptoJS.enc.Utf8);
return res;
};
const callRememberMe = async (values: any) => {
const { autoLogin } = values;
if (autoLogin) {
await rememberMe(REMEMBER_ME_KEY, {
um: encryptPassword(values.username),
pw: encryptPassword(values.password),
f: true
});
} else {
await removeRememberMe(REMEMBER_ME_KEY);
}
};
const callGetRememberMe = async () => {
const rememberMe = await getRememberMe(REMEMBER_ME_KEY);
if (rememberMe?.f) {
const username = decryptPassword(rememberMe?.um);
const password = decryptPassword(rememberMe?.pw);
form.setFieldsValue({ username, password, autoLogin: true });
}
};
// OIDC certification
const handleOidcLogin = async () => {
window.location.href = '/auth/oidc/login';
};
// SAML certification
const handleSamlLogin = async () => {
window.location.href = '/auth/saml/login';
// error handling for authentication
const handleOnError = (error: Error) => {
setAuthError(error);
messageApi.error({
duration: 5,
content: (
<div className={styles.errorMessage}>
<div className="title">
{intl.formatMessage({ id: 'common.login.auth.failed' })}
</div>
<div className="message">{error?.message || 'Unknown error'}</div>
</div>
)
});
};
// Handling SSO callbacks
useEffect(() => {
fetchAuthConfig('/auth_config')
.then((authConfig) => {
if (authConfig.is_oidc) {
setLoginOption({
oidc: true
});
} else if (authConfig.is_saml) {
if (authConfig.is_saml) {
setLoginOption({
saml: true
});
}
}
})
.catch((error) => {
setLoginOption({ oidc: false, saml: false });
});
if (sso) {
fetchUserInfo()
.then((userInfo) => {
setUserInfo(userInfo);
gotoDefaultPage({});
})
.catch((error) => {
console.log(error);
setErr(error);
});
}
}, []);
const handleLogin = async (values: any) => {
try {
await login({
username: values.username,
password: values.password
});
const userInfo = await fetchUserInfo();
// local user authentication
const { handleLogin } = useLocalAuth({
fetchUserInfo,
form,
onSuccess: async (userInfo) => {
setUserInfo(userInfo);
if (values.autoLogin) {
await callRememberMe(values);
} else {
await removeRememberMe(REMEMBER_ME_KEY);
}
if (!userInfo?.require_password_change) {
gotoDefaultPage(userInfo);
} else {
setInitialPassword(encryptPassword(values.password));
}
} catch (error) {
// to do something
},
onError: (error) => {
// gpustack handle in the interceptor
}
};
});
// SSO hook
const SSOAuth = useSSOAuth({
fetchUserInfo,
onSuccess: (userInfo) => {
setUserInfo(userInfo);
gotoDefaultPage({});
},
onError: handleOnError
});
useEffect(() => {
callGetRememberMe();
}, []);
const hasThirdPartyLogin = useMemo(() => {
return SSOAuth.options.oidc || SSOAuth.options.saml;
}, [SSOAuth.options]);
if (sso && !err) {
return <div>Handle SSO callback...</div>;
} else if (err) {
const renderThirdPartyLoginButtons = () => {
if (!hasThirdPartyLogin) return null;
return (
<div style={{ color: 'red' }}>Error to log in by SSO: {err.message}</div>
<>
<DividerWrapper plain>
{intl.formatMessage({ id: 'common.login.thirdparty' })}
</DividerWrapper>
<Buttons>
{SSOAuth.options.oidc && (
<Tooltip title="OIDC">
<ButtonWrapper onClick={SSOAuth.loginWithOIDC}>
<img src={OIDCIcon} alt="" height={32} width={32} />
</ButtonWrapper>
</Tooltip>
)}
{SSOAuth.options.saml && (
<Tooltip title="SAML">
<ButtonWrapper onClick={SSOAuth.loginWithSAML}>
<img src={SAMLIcon} alt="" height={32} width={32} />
</ButtonWrapper>
</Tooltip>
)}
</Buttons>
</>
);
} else {
return (
};
const isThirdPartyAuthHandling = useMemo(() => {
return SSOAuth.isSSOLogin && !authError;
}, [SSOAuth.isSSOLogin, authError]);
return (
<div>
{contextHolder}
<div className={styles.header}>
<ThemeDropActions></ThemeDropActions>
<LangSelect />
</div>
<div>
<div className={styles.header}>
<ThemeDropActions></ThemeDropActions>
<LangSelect />
</div>
<div>
{isThirdPartyAuthHandling ? (
<Spin>
<span style={{ color: 'var(--ant-color-text)' }}>
{intl.formatMessage({ id: 'common.login.auth' })}
</span>
</Spin>
) : (
<Form
form={form}
style={{ width: '360px', margin: '0 auto' }}
@ -240,7 +225,9 @@ const LoginForm = () => {
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{ name: intl.formatMessage({ id: 'common.form.username' }) }
{
name: intl.formatMessage({ id: 'common.form.username' })
}
)
}
]}
@ -258,7 +245,9 @@ const LoginForm = () => {
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{ name: intl.formatMessage({ id: 'common.form.password' }) }
{
name: intl.formatMessage({ id: 'common.form.password' })
}
)
}
]}
@ -291,47 +280,20 @@ const LoginForm = () => {
{intl.formatMessage({ id: 'common.button.forgotpassword' })}
</Button>
</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'
}
type="primary"
block
style={{ height: '48px', fontSize: '14px' }}
>
{intl.formatMessage({ id: 'common.button.login' })}
</Button>
{renderThirdPartyLoginButtons()}
</Form>
</div>
)}
</div>
);
}
</div>
);
};
export default LoginForm;

@ -0,0 +1,98 @@
import { initialPasswordAtom } from '@/atoms/user';
import {
CRYPT_TEXT,
REMEMBER_ME_KEY,
getRememberMe,
rememberMe,
removeRememberMe
} from '@/utils/localstore/index';
import { FormInstance } from 'antd';
import CryptoJS from 'crypto-js';
import { useAtom } from 'jotai';
import { useEffect } from 'react';
import { login } from '../apis';
interface UseLocalAuthOptions {
fetchUserInfo: () => Promise<any>;
onSuccess?: (userInfo: any) => void;
onError?: (error: Error) => void;
form: FormInstance;
}
export const useLocalAuth = ({
fetchUserInfo,
onSuccess,
onError,
form
}: UseLocalAuthOptions) => {
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
// Encrypt password before storing
const encryptPassword = (password: string) => {
const psw = CryptoJS.AES?.encrypt?.(password, CRYPT_TEXT).toString();
return psw;
};
const decryptPassword = (password: string) => {
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
const res = bytes.toString(CryptoJS.enc.Utf8);
return res;
};
const callGetRememberMe = async () => {
const rememberMe = await getRememberMe(REMEMBER_ME_KEY);
if (rememberMe?.f) {
const username = decryptPassword(rememberMe?.um);
const password = decryptPassword(rememberMe?.pw);
form.setFieldsValue({ username, password, autoLogin: true });
}
};
// remember me
const callRememberMe = async (values: any) => {
const { autoLogin } = values;
if (autoLogin) {
await rememberMe(REMEMBER_ME_KEY, {
um: encryptPassword(values.username),
pw: encryptPassword(values.password),
f: true
});
} else {
await removeRememberMe(REMEMBER_ME_KEY);
}
};
// click login button
const handleLogin = async (values: any) => {
try {
await login({
username: values.username,
password: values.password
});
const userInfo = await fetchUserInfo();
if (values.autoLogin) {
await callRememberMe(values);
} else {
await removeRememberMe(REMEMBER_ME_KEY);
}
if (userInfo?.require_password_change) {
setInitialPassword(encryptPassword(values.password));
}
onSuccess?.(userInfo);
} catch (error) {
onError?.(error);
}
};
useEffect(() => {
callGetRememberMe();
}, []);
return {
handleLogin
};
};

@ -0,0 +1,77 @@
// hooks/useSSOAuth.ts
import { history } from '@umijs/max';
import { useEffect, useState } from 'react';
type LoginOption = {
saml: boolean;
oidc: boolean;
};
// sso configuration
async function fetchAuthConfig(url: string) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
} catch (error) {
console.error('OIDC config error:', error);
throw error;
}
}
export function useSSOAuth({
fetchUserInfo,
onSuccess,
onError
}: {
fetchUserInfo: () => Promise<any>;
onSuccess?: (userInfo: any) => void;
onError?: (err: Error) => void;
}) {
const [loginOption, setLoginOption] = useState<LoginOption>({
saml: false,
oidc: false
});
const { location } = history;
const params = new URLSearchParams(location.search);
const sso = params.get('sso');
const oidcLogin = () => {
window.location.href = '/auth/oidc/login';
};
const samlLogin = () => {
window.location.href = '/auth/saml/login';
};
useEffect(() => {
fetchAuthConfig('/auth_config')
.then((authConfig) => {
setLoginOption({
oidc: !!authConfig.is_oidc,
saml: !!authConfig.is_saml
});
})
.catch(() => {
setLoginOption({ oidc: false, saml: false });
});
if (sso) {
fetchUserInfo()
.then((userInfo) => {
onSuccess?.(userInfo);
})
.catch((error) => {
onError?.(error);
});
}
}, []);
return {
isSSOLogin: !!sso,
options: loginOption,
loginWithOIDC: oidcLogin,
loginWithSAML: samlLogin
};
}

@ -30,7 +30,7 @@ const Wrapper = styled.div<{ $isDarkTheme: boolean }>`
`;
const Box = styled.div`
padding-top: 10%;
padding-top: 8%;
display: flex;
flex-direction: column;
justify-content: space-between;
@ -43,6 +43,8 @@ const FormWrapper = styled.div`
border-radius: var(--border-radius-modal);
width: max-content;
height: max-content;
max-width: 800px;
max-height: 600px;
padding: 40px;
background-color: var(--color-modal-content-bg);
box-shadow: var(--color-modal-box-shadow);

Loading…
Cancel
Save