Files
SMATEC-FRONTEND/src/pages/Auth/index.tsx

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;