381 lines
12 KiB
TypeScript
381 lines
12 KiB
TypeScript
import Footer from '@/components/Footer';
|
|
import LangSwitches from '@/components/Lang/LanguageSwitcherAuth';
|
|
import ThemeSwitcherAuth from '@/components/Theme/ThemeSwitcherAuth';
|
|
import { THEME_KEY } from '@/constants';
|
|
import { ROUTER_HOME } from '@/constants/routes';
|
|
import {
|
|
apiForgotPassword,
|
|
apiLogin,
|
|
apiQueryProfile,
|
|
} from '@/services/master/AuthController';
|
|
import { checkRefreshTokenExpired } from '@/utils/jwt';
|
|
import { getDomainTitle, getLogoImage } from '@/utils/logo';
|
|
import {
|
|
getBrowserId,
|
|
getRefreshToken,
|
|
removeAccessToken,
|
|
removeRefreshToken,
|
|
setAccessToken,
|
|
setRefreshToken,
|
|
} from '@/utils/storage';
|
|
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
|
import { LoginFormPage, ProFormText } from '@ant-design/pro-components';
|
|
import { FormattedMessage, history, useIntl, useModel } from '@umijs/max';
|
|
import { Button, ConfigProvider, Flex, Image, message, theme } from 'antd';
|
|
import { CSSProperties, useEffect, useState } from 'react';
|
|
import { flushSync } from 'react-dom';
|
|
import mobifontLogo from '../../../public/mobifont-logo.png';
|
|
type LoginType = 'login' | 'forgot';
|
|
|
|
// Form wrapper with animation
|
|
const FormWrapper = ({
|
|
children,
|
|
key,
|
|
}: {
|
|
children: React.ReactNode;
|
|
key: string;
|
|
}) => {
|
|
const style: CSSProperties = {
|
|
animation: 'fadeInSlide 0.4s ease-out forwards',
|
|
};
|
|
|
|
return (
|
|
<div key={key} style={style}>
|
|
<style>{`
|
|
@keyframes fadeInSlide {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
`}</style>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const LoginPage = () => {
|
|
const [isDark, setIsDark] = useState(
|
|
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') === 'dark',
|
|
);
|
|
const { token } = theme.useToken();
|
|
const [messageApi, contextHolder] = message.useMessage();
|
|
const urlParams = new URL(window.location.href).searchParams;
|
|
const redirect = urlParams.get('redirect');
|
|
const intl = useIntl();
|
|
const { setInitialState } = useModel('@@initialState');
|
|
const [loginType, setLoginType] = useState<LoginType>('login');
|
|
|
|
// Listen for theme changes from ThemeSwitcherAuth
|
|
useEffect(() => {
|
|
const handleThemeChange = (e: Event) => {
|
|
const customEvent = e as CustomEvent<{ theme: 'light' | 'dark' }>;
|
|
setIsDark(customEvent.detail.theme === 'dark');
|
|
};
|
|
|
|
window.addEventListener('theme-change', handleThemeChange as EventListener);
|
|
return () => {
|
|
window.removeEventListener(
|
|
'theme-change',
|
|
handleThemeChange as EventListener,
|
|
);
|
|
};
|
|
}, []);
|
|
const checkLogin = async () => {
|
|
const refreshToken = getRefreshToken();
|
|
if (!refreshToken) {
|
|
return;
|
|
}
|
|
const isRefreshTokenExpired = checkRefreshTokenExpired(refreshToken);
|
|
if (isRefreshTokenExpired) {
|
|
removeAccessToken();
|
|
removeRefreshToken();
|
|
return;
|
|
} else {
|
|
const userInfo = await apiQueryProfile();
|
|
if (userInfo) {
|
|
flushSync(() => {
|
|
setInitialState((s: any) => ({
|
|
...s,
|
|
currentUserProfile: userInfo,
|
|
}));
|
|
});
|
|
}
|
|
if (redirect) {
|
|
history.push(redirect);
|
|
} else {
|
|
history.push(ROUTER_HOME);
|
|
}
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
checkLogin();
|
|
}, []);
|
|
|
|
const handleLogin = async (values: MasterModel.LoginRequestBody) => {
|
|
const { email, password } = values;
|
|
if (loginType === 'login') {
|
|
try {
|
|
const resp = await apiLogin({
|
|
guid: getBrowserId(),
|
|
email,
|
|
password,
|
|
});
|
|
if (resp?.token) {
|
|
setAccessToken(resp.token);
|
|
setRefreshToken(resp.refresh_token);
|
|
const userInfo = await apiQueryProfile();
|
|
if (userInfo) {
|
|
flushSync(() => {
|
|
setInitialState((s: any) => ({
|
|
...s,
|
|
currentUserProfile: userInfo,
|
|
}));
|
|
});
|
|
}
|
|
if (redirect) {
|
|
history.push(redirect);
|
|
} else {
|
|
history.push(ROUTER_HOME);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
}
|
|
} else {
|
|
try {
|
|
const host = window.location.origin;
|
|
const body: MasterModel.ForgotPasswordRequestBody = {
|
|
email: email,
|
|
host: host,
|
|
};
|
|
const resp = await apiForgotPassword(body);
|
|
if (!resp.error) {
|
|
messageApi.success(
|
|
intl.formatMessage({
|
|
id: 'master.auth.forgot.message.success',
|
|
defaultMessage:
|
|
'Request sent successfully, please check your email!',
|
|
}),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error when send reset password: ', error);
|
|
messageApi.error(
|
|
intl.formatMessage({
|
|
id: 'master.auth.forgot.message.fail',
|
|
defaultMessage: 'Request failed, please try again later!',
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ConfigProvider
|
|
theme={{
|
|
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
backgroundColor: isDark ? '#000' : 'white',
|
|
height: '100vh',
|
|
}}
|
|
>
|
|
{contextHolder}
|
|
<LoginFormPage
|
|
backgroundImageUrl="https://mdn.alipayobjects.com/huamei_gcee1x/afts/img/A*y0ZTS6WLwvgAAAAAAAAAAAAADml6AQ/fmt.webp"
|
|
logo={getLogoImage()}
|
|
backgroundVideoUrl="https://gw.alipayobjects.com/v/huamei_gcee1x/afts/video/jXRBRK_VAwoAAAAAAAAAAAAAK4eUAQBr"
|
|
title={
|
|
<span style={{ color: token.colorBgContainer }}>
|
|
{intl.formatMessage({
|
|
id: getDomainTitle(),
|
|
defaultMessage: 'Smatec',
|
|
})}
|
|
</span>
|
|
}
|
|
containerStyle={{
|
|
backgroundColor: 'rgba(0, 0, 0,0.65)',
|
|
backdropFilter: 'blur(4px)',
|
|
}}
|
|
subTitle={<Image preview={false} src={mobifontLogo} />}
|
|
submitter={{
|
|
searchConfig: {
|
|
submitText:
|
|
loginType === 'login'
|
|
? intl.formatMessage({
|
|
id: 'master.auth.login.title',
|
|
defaultMessage: 'Đăng nhập',
|
|
})
|
|
: intl.formatMessage({
|
|
id: 'master.auth.forgot.button.title',
|
|
defaultMessage: 'Đăng nhập',
|
|
}),
|
|
},
|
|
}}
|
|
onFinish={async (values: MasterModel.LoginRequestBody) =>
|
|
handleLogin(values)
|
|
}
|
|
>
|
|
<FormWrapper key={loginType}>
|
|
{loginType === 'login' && (
|
|
<>
|
|
<ProFormText
|
|
name="email"
|
|
fieldProps={{
|
|
autoComplete: 'email',
|
|
autoFocus: true,
|
|
size: 'large',
|
|
prefix: (
|
|
<UserOutlined
|
|
// style={{
|
|
// color: token.colorText,
|
|
// }}
|
|
className={'prefixIcon'}
|
|
/>
|
|
),
|
|
}}
|
|
placeholder={intl.formatMessage({
|
|
id: 'master.auth.login.email',
|
|
defaultMessage: 'Email',
|
|
})}
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: (
|
|
<FormattedMessage
|
|
id="master.users.email.required"
|
|
defaultMessage="The email is required"
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
type: 'email',
|
|
message: (
|
|
<FormattedMessage
|
|
id="master.users.email.invalid"
|
|
defaultMessage="Invalid email address"
|
|
/>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
<ProFormText.Password
|
|
name="password"
|
|
fieldProps={{
|
|
size: 'large',
|
|
autoComplete: 'current-password',
|
|
prefix: <LockOutlined className={'prefixIcon'} />,
|
|
}}
|
|
placeholder={intl.formatMessage({
|
|
id: 'master.auth.password',
|
|
defaultMessage: 'Mật khẩu',
|
|
})}
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: intl.formatMessage({
|
|
id: 'master.auth.validation.password',
|
|
defaultMessage: 'Mật khẩu không được để trống!',
|
|
}),
|
|
},
|
|
]}
|
|
/>
|
|
</>
|
|
)}
|
|
{loginType === 'forgot' && (
|
|
<>
|
|
<ProFormText
|
|
name="email"
|
|
fieldProps={{
|
|
autoComplete: 'email',
|
|
autoFocus: true,
|
|
size: 'large',
|
|
prefix: <UserOutlined className={'prefixIcon'} />,
|
|
}}
|
|
placeholder={intl.formatMessage({
|
|
id: 'master.auth.login.email',
|
|
defaultMessage: 'Email',
|
|
})}
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: (
|
|
<FormattedMessage
|
|
id="master.users.email.required"
|
|
defaultMessage="The email is required"
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
type: 'email',
|
|
message: (
|
|
<FormattedMessage
|
|
id="master.users.email.invalid"
|
|
defaultMessage="Invalid email address"
|
|
/>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</>
|
|
)}
|
|
</FormWrapper>
|
|
<Flex
|
|
justify="flex-end"
|
|
align="flex-start"
|
|
style={{ marginBlockEnd: 16 }}
|
|
>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
onClick={() => {
|
|
if (loginType === 'login') {
|
|
setLoginType('forgot');
|
|
} else {
|
|
setLoginType('login');
|
|
}
|
|
}}
|
|
>
|
|
{loginType === 'login' ? (
|
|
<FormattedMessage
|
|
id="master.auth.forgot.title"
|
|
defaultMessage="Quên mật khẩu?"
|
|
/>
|
|
) : (
|
|
<FormattedMessage
|
|
id="master.auth.backToLogin.title"
|
|
defaultMessage="Back to Login"
|
|
/>
|
|
)}
|
|
</Button>
|
|
</Flex>
|
|
</LoginFormPage>
|
|
<div className="absolute top-5 right-5 z-50 flex gap-4">
|
|
<ThemeSwitcherAuth />
|
|
<LangSwitches />
|
|
</div>
|
|
<div
|
|
style={{
|
|
backgroundColor: 'transparent',
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
zIndex: 99,
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<Footer />
|
|
</div>
|
|
</div>
|
|
</ConfigProvider>
|
|
);
|
|
};
|
|
export default LoginPage;
|