refactor: login form based on sso auth
This commit is contained in:
Generated
+14760
-12465
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 { WarningFilled } from '@ant-design/icons';
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
|
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
|
||||||
import './block.less';
|
|
||||||
interface AlertInfoProps {
|
interface AlertInfoProps {
|
||||||
type: Global.MessageType;
|
type: Global.MessageType;
|
||||||
message: React.ReactNode;
|
message: React.ReactNode;
|
||||||
@@ -18,6 +18,81 @@ interface AlertInfoProps {
|
|||||||
overlayScrollerProps?: OverlayScrollerOptions;
|
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`
|
const TitleWrapper = styled.div`
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--ant-color-text);
|
color: var(--ant-color-text);
|
||||||
@@ -46,12 +121,12 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
|||||||
maxHeight = 86,
|
maxHeight = 86,
|
||||||
overlayScrollerProps = {}
|
overlayScrollerProps = {}
|
||||||
} = props;
|
} = props;
|
||||||
|
const { styles } = useStyles();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{message ? (
|
{message ? (
|
||||||
<div
|
<div
|
||||||
className={classNames('alert-info-block', type)}
|
className={classNames(styles.alertBlockInfo, type)}
|
||||||
style={{ ...style }}
|
style={{ ...style }}
|
||||||
>
|
>
|
||||||
<Typography.Paragraph
|
<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.editmode': 'Edit mode',
|
||||||
'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': 'Log in',
|
||||||
'common.button.oidclogin': 'Log In With OIDC',
|
'common.button.login.local': 'Log in with local user',
|
||||||
'common.button.samllogin': 'Log In With SAML',
|
'common.button.oidclogin': 'Log in with OIDC',
|
||||||
|
'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',
|
||||||
@@ -253,5 +254,8 @@ export default {
|
|||||||
'common.tips.escape.disable':
|
'common.tips.escape.disable':
|
||||||
'Click Cancel or the X at the top right to close.',
|
'Click Cancel or the X at the top right to close.',
|
||||||
'common.button.clearSelection': 'Clear Selection',
|
'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.shortcut': 'キーボードショートカット',
|
||||||
'common.button.add': '追加',
|
'common.button.add': '追加',
|
||||||
'common.button.login': 'ログイン',
|
'common.button.login': 'ログイン',
|
||||||
'common.button.oidclogin': 'OIDCでログイン',
|
'common.button.login.local': 'ローカルユーザーでログイン',
|
||||||
'common.button.samllogin': 'SAMLでログイン',
|
'common.button.oidclogin': 'OIDC でログイン',
|
||||||
|
'common.button.samllogin': 'SAML でログイン',
|
||||||
'common.button.select': '選択',
|
'common.button.select': '選択',
|
||||||
'common.button.selected': '選択済み',
|
'common.button.selected': '選択済み',
|
||||||
'common.button.continue': '続行',
|
'common.button.continue': '続行',
|
||||||
@@ -253,7 +254,10 @@ export default {
|
|||||||
'common.tips.escape.disable':
|
'common.tips.escape.disable':
|
||||||
'Click Cancel or the X at the top right to close.',
|
'Click Cancel or the X at the top right to close.',
|
||||||
'common.button.clearSelection': 'Clear Selection',
|
'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) ==========
|
// ========== 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.'
|
// 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.'
|
// 13. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.'
|
||||||
// 14. 'common.button.clearSelection': 'Clear Selection',
|
// 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export default {
|
|||||||
'common.button.shortcut': 'Сочетания клавиш',
|
'common.button.shortcut': 'Сочетания клавиш',
|
||||||
'common.button.add': 'Добавить',
|
'common.button.add': 'Добавить',
|
||||||
'common.button.login': 'Войти',
|
'common.button.login': 'Войти',
|
||||||
|
'common.button.login.local': 'Войти с локальным пользователем',
|
||||||
'common.button.oidclogin': 'Войти с OIDC',
|
'common.button.oidclogin': 'Войти с OIDC',
|
||||||
'common.button.samllogin': 'Войти с SAML',
|
'common.button.samllogin': 'Войти с SAML',
|
||||||
'common.button.select': 'Выбрать',
|
'common.button.select': 'Выбрать',
|
||||||
@@ -252,9 +253,12 @@ export default {
|
|||||||
'common.tips.escape.disable':
|
'common.tips.escape.disable':
|
||||||
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
|
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
|
||||||
'common.button.clearSelection': 'Сбросить выбор',
|
'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) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
// 1. 'common.login.thirdparty': 'or login with'
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ export default {
|
|||||||
'common.button.editmode': '编辑模式',
|
'common.button.editmode': '编辑模式',
|
||||||
'common.button.shortcut': '快捷键',
|
'common.button.shortcut': '快捷键',
|
||||||
'common.button.add': '添加',
|
'common.button.add': '添加',
|
||||||
'common.button.login': 'Use a local user',
|
'common.button.login': '登录',
|
||||||
'common.button.oidclogin': 'Log In With OIDC',
|
'common.button.login.local': '本地用户登录',
|
||||||
'common.button.samllogin': 'Log In With SAML',
|
'common.button.oidclogin': '使用 OIDC 登录',
|
||||||
|
'common.button.samllogin': '使用 SAML 登录',
|
||||||
'common.button.select': '选择',
|
'common.button.select': '选择',
|
||||||
'common.button.selected': '已选择',
|
'common.button.selected': '已选择',
|
||||||
'common.button.continue': '继续',
|
'common.button.continue': '继续',
|
||||||
@@ -246,5 +247,8 @@ export default {
|
|||||||
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
|
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
|
||||||
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
||||||
'common.button.clearSelection': '清除选择',
|
'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 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 LangSelect from '@/components/lang-select';
|
||||||
import SealInput from '@/components/seal-form/seal-input';
|
import SealInput from '@/components/seal-form/seal-input';
|
||||||
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
|
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
|
||||||
import externalLinks from '@/constants/external-links';
|
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 { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
import { history, useIntl, useModel } from '@umijs/max';
|
import { useIntl, useModel } from '@umijs/max';
|
||||||
import { Button, Checkbox, Form } from 'antd';
|
import { Button, Checkbox, Divider, Form, Spin, Tooltip, message } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import CryptoJS from 'crypto-js';
|
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
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';
|
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 }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
header: css`
|
header: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -38,40 +58,28 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
.anticon:hover {
|
.anticon:hover {
|
||||||
color: ${token.colorTextTertiary};
|
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 = () => {
|
const LoginForm = () => {
|
||||||
type LoginOption = {
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
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 { initialState, setInitialState } = useModel('@@initialState') || {};
|
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||||
|
const [authError, setAuthError] = useState<Error | null>(null);
|
||||||
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
|
||||||
@@ -94,6 +102,7 @@ const LoginForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [intl]);
|
}, [intl]);
|
||||||
|
|
||||||
const gotoDefaultPage = async (userInfo: any) => {
|
const gotoDefaultPage = async (userInfo: any) => {
|
||||||
checkDefaultPage(userInfo, true);
|
checkDefaultPage(userInfo, true);
|
||||||
};
|
};
|
||||||
@@ -112,121 +121,97 @@ const LoginForm = () => {
|
|||||||
return userInfo;
|
return userInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
const encryptPassword = (password: string) => {
|
// error handling for authentication
|
||||||
const psw = CryptoJS.AES?.encrypt?.(password, CRYPT_TEXT).toString();
|
const handleOnError = (error: Error) => {
|
||||||
return psw;
|
setAuthError(error);
|
||||||
};
|
messageApi.error({
|
||||||
const decryptPassword = (password: string) => {
|
duration: 5,
|
||||||
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
|
content: (
|
||||||
const res = bytes.toString(CryptoJS.enc.Utf8);
|
<div className={styles.errorMessage}>
|
||||||
return res;
|
<div className="title">
|
||||||
|
{intl.formatMessage({ id: 'common.login.auth.failed' })}
|
||||||
|
</div>
|
||||||
|
<div className="message">{error?.message || 'Unknown error'}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const callRememberMe = async (values: any) => {
|
// local user authentication
|
||||||
const { autoLogin } = values;
|
const { handleLogin } = useLocalAuth({
|
||||||
if (autoLogin) {
|
fetchUserInfo,
|
||||||
await rememberMe(REMEMBER_ME_KEY, {
|
form,
|
||||||
um: encryptPassword(values.username),
|
onSuccess: async (userInfo) => {
|
||||||
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';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
setUserInfo(userInfo);
|
setUserInfo(userInfo);
|
||||||
if (values.autoLogin) {
|
|
||||||
await callRememberMe(values);
|
|
||||||
} else {
|
|
||||||
await removeRememberMe(REMEMBER_ME_KEY);
|
|
||||||
}
|
|
||||||
if (!userInfo?.require_password_change) {
|
if (!userInfo?.require_password_change) {
|
||||||
gotoDefaultPage(userInfo);
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasThirdPartyLogin = useMemo(() => {
|
||||||
|
return SSOAuth.options.oidc || SSOAuth.options.saml;
|
||||||
|
}, [SSOAuth.options]);
|
||||||
|
|
||||||
|
const renderThirdPartyLoginButtons = () => {
|
||||||
|
if (!hasThirdPartyLogin) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const isThirdPartyAuthHandling = useMemo(() => {
|
||||||
callGetRememberMe();
|
return SSOAuth.isSSOLogin && !authError;
|
||||||
}, []);
|
}, [SSOAuth.isSSOLogin, authError]);
|
||||||
|
|
||||||
if (sso && !err) {
|
return (
|
||||||
return <div>Handle SSO callback...</div>;
|
<div>
|
||||||
} else if (err) {
|
{contextHolder}
|
||||||
return (
|
<div className={styles.header}>
|
||||||
<div style={{ color: 'red' }}>Error to log in by SSO: {err.message}</div>
|
<ThemeDropActions></ThemeDropActions>
|
||||||
);
|
<LangSelect />
|
||||||
} else {
|
</div>
|
||||||
return (
|
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.header}>
|
{isThirdPartyAuthHandling ? (
|
||||||
<ThemeDropActions></ThemeDropActions>
|
<Spin>
|
||||||
<LangSelect />
|
<span style={{ color: 'var(--ant-color-text)' }}>
|
||||||
</div>
|
{intl.formatMessage({ id: 'common.login.auth' })}
|
||||||
<div>
|
</span>
|
||||||
|
</Spin>
|
||||||
|
) : (
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
style={{ width: '360px', margin: '0 auto' }}
|
style={{ width: '360px', margin: '0 auto' }}
|
||||||
@@ -240,7 +225,9 @@ const LoginForm = () => {
|
|||||||
required: true,
|
required: true,
|
||||||
message: intl.formatMessage(
|
message: intl.formatMessage(
|
||||||
{ id: 'common.form.rule.input' },
|
{ 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,
|
required: true,
|
||||||
message: intl.formatMessage(
|
message: intl.formatMessage(
|
||||||
{ id: 'common.form.rule.input' },
|
{ 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' })}
|
{intl.formatMessage({ id: 'common.button.forgotpassword' })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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
|
<Button
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
type={
|
type="primary"
|
||||||
loginOption.oidc === false && loginOption.saml === false
|
|
||||||
? 'primary'
|
|
||||||
: 'link'
|
|
||||||
}
|
|
||||||
block
|
block
|
||||||
style={{ height: '48px', fontSize: '14px' }}
|
style={{ height: '48px', fontSize: '14px' }}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: 'common.button.login' })}
|
{intl.formatMessage({ id: 'common.button.login' })}
|
||||||
</Button>
|
</Button>
|
||||||
|
{renderThirdPartyLoginButtons()}
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LoginForm;
|
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`
|
const Box = styled.div`
|
||||||
padding-top: 10%;
|
padding-top: 8%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -43,6 +43,8 @@ const FormWrapper = styled.div`
|
|||||||
border-radius: var(--border-radius-modal);
|
border-radius: var(--border-radius-modal);
|
||||||
width: max-content;
|
width: max-content;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 600px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
background-color: var(--color-modal-content-bg);
|
background-color: var(--color-modal-content-bg);
|
||||||
box-shadow: var(--color-modal-box-shadow);
|
box-shadow: var(--color-modal-box-shadow);
|
||||||
|
|||||||
Reference in New Issue
Block a user