Files
gpustack-ui/src/pages/login/index.tsx
T

274 lines
7.5 KiB
TypeScript

import LogoIcon from '@/assets/images/gpustack-logo.png';
import { initialPasswordAtom, userAtom } from '@/atoms/user';
import { resetStorageUserSettings } from '@/atoms/utils';
import DarkMask from '@/components/dark-mask';
import Footer from '@/components/footer';
import LangSelect from '@/components/lang-select';
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
import { PasswordReg } from '@/config';
import { GPUSTACK_API_BASE_URL } from '@/config/settings';
import { COLOR_PRIMARY } from '@/config/theme/constants';
import externalLinks from '@/constants/external-links';
import useUserSettings from '@/hooks/use-user-settings';
import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
import { getGPUStackPlugin } from '@/plugins';
import {
CRYPT_TEXT,
IS_FIRST_LOGIN,
writeState
} from '@/utils/localstore/index';
import { CoreUIProvider } from '@gpustack/core-ui';
import {
getAllLocales,
request,
setLocale,
useIntl,
useModel,
useNavigate
} from '@umijs/max';
import { ConfigProvider, theme } from 'antd';
import { createStyles } from 'antd-style';
import CryptoJS from 'crypto-js';
import { useAtom } from 'jotai';
import { useCallback, useEffect, useMemo } from 'react';
import { flushSync } from 'react-dom';
import styled from 'styled-components';
import { updatePassword as updatePasswordApi } from './apis';
import Background from './components/background';
import LoginForm from './components/login-form';
import PasswordForm from './components/password-form';
import { useLocalAuth } from './hooks/use-local-auth';
import { useSSOAuth } from './hooks/use-sso-auth';
import { checkDefaultPage } from './utils';
const COLOR_LINK = '#1677ff';
const useStyles = createStyles(({ token, css }) => ({
header: css`
display: flex;
align-items: center;
gap: 8px;
position: fixed;
right: 0;
top: 0;
height: 60px;
padding: 20px;
z-index: 100;
.anticon-global {
color: ${token.colorText};
}
.anticon:hover {
color: ${token.colorTextTertiary};
}
`,
formContainer: css`
height: calc(100vh - 64px);
width: 100%;
`
}));
const Box = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 100vh;
.ant-form-item-label {
line-height: 1;
padding-bottom: 4px;
}
.ant-form-item-label > label:after {
display: none;
}
.ant-form-item-label > label {
height: auto !important;
}
`;
const FormWrapper = styled.div`
margin: 0 auto;
margin-top: 50vh;
width: 400px;
transform: translateY(-50%);
position: relative;
z-index: 999;
border-radius: var(--border-radius-modal);
height: max-content;
max-height: 660px;
padding: 40px;
background-color: var(--color-modal-content-bg);
box-shadow: var(--color-modal-box-shadow);
.field-wrapper {
background-color: transparent !important;
}
.ant-input-outlined.ant-input-status-error:not(.ant-input-disabled) {
background-color: transparent !important;
}
`;
const decryptInitialPassword = (password: string) => {
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
return bytes.toString(CryptoJS.enc.Utf8);
};
const Login = () => {
const { styles } = useStyles();
const intl = useIntl();
const navigate = useNavigate();
const userSettingsStorage = useUserSettingsStorage();
const { themeData, userSettings, isDarkTheme } = useUserSettings();
const [userInfo, setUserInfo] = useAtom(userAtom);
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
const { initialState, setInitialState } = useModel('@@initialState') || {};
const plugin = getGPUStackPlugin();
const CustomLoginComponent = plugin?.login?.CustomLoginComponent;
const shouldUseCustomLogin = plugin?.login?.shouldUseCustomLogin;
const useCustomLogin = useMemo(
() => shouldUseCustomLogin?.(userSettings),
[shouldUseCustomLogin, userSettings]
);
const gotoDefaultPage = async (info: any) => {
if (!info || info?.require_password_change) {
return;
}
checkDefaultPage(info, true);
if (!initialState?.currentUser) {
setInitialState((preState: any) => ({
...preState,
currentUser: info
}));
}
};
useEffect(() => {
gotoDefaultPage(userInfo);
}, [userInfo]);
const fetchUserInfo = useCallback(async () => {
const info = await initialState?.fetchUserInfo?.();
if (info) {
flushSync(() => {
setInitialState((s: any) => ({
...s,
currentUser: info
}));
});
}
return info;
}, [initialState, setInitialState]);
const onPasswordChanged = useCallback(() => {
resetStorageUserSettings();
writeState(IS_FIRST_LOGIN, null);
}, []);
const kit = useMemo(
() => ({
useLocalAuth,
useSSOAuth,
userInfo,
setUserInfo,
fetchUserInfo,
checkDefaultPage,
initialPassword,
setInitialPassword,
decryptInitialPassword,
updatePassword: updatePasswordApi,
passwordReg: PasswordReg,
onPasswordChanged,
resetPasswordUrl: externalLinks.resetPassword,
formLogoUrl: LogoIcon
}),
[
userInfo,
setUserInfo,
fetchUserInfo,
initialPassword,
setInitialPassword,
onPasswordChanged
]
);
return (
<ConfigProvider
componentSize="large"
theme={{
algorithm: userSettings.isDarkTheme
? theme.darkAlgorithm
: theme.defaultAlgorithm,
...themeData,
token: {
...themeData?.token,
colorPrimary:
plugin?.getPrimaryColor?.(userSettings)?.colorPrimary ||
COLOR_PRIMARY
}
}}
>
<CoreUIProvider
config={{
apiBaseUrl: GPUSTACK_API_BASE_URL,
theme: userSettings.theme,
iconUrl: '',
isDarkTheme: userSettings.isDarkTheme,
defaultColorPrimary: COLOR_PRIMARY
}}
hooks={
{
useUserSettings: useUserSettings as any,
useUserSettingsStorage: () => userSettingsStorage,
useIntl: useIntl
// useCurrentUser / useTableFetch are required by the
// 1.0.10 CoreHooks type but the login page doesn't use
// either — nothing here lists data or reads the current
// user. Cast away rather than mounting stubs that would
// never run.
} as any
}
i18n={intl}
locale={{
getAllLocales: getAllLocales,
setLocale: setLocale
}}
services={{
request: request,
router: {
push: (path: string) => navigate(path),
replace: (path: string) => navigate(path, { replace: true }),
goBack: () => navigate(-1)
}
}}
>
<div className={styles.header}>
<ThemeDropActions></ThemeDropActions>
<LangSelect />
</div>
<DarkMask></DarkMask>
<Background isDarkTheme={isDarkTheme} />
{useCustomLogin && CustomLoginComponent ? (
<CustomLoginComponent kit={kit} />
) : (
<Box>
<div className={styles.formContainer}>
<FormWrapper>
{userInfo?.require_password_change ? (
<PasswordForm />
) : (
<LoginForm />
)}
</FormWrapper>
</div>
<Footer />
</Box>
)}
</CoreUIProvider>
</ConfigProvider>
);
};
export default Login;