feat(users): add reset password functionality for users and implement forgot password page
This commit is contained in:
@@ -1,48 +1,100 @@
|
||||
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 { apiLogin, apiQueryProfile } from '@/services/master/AuthController';
|
||||
import { parseJwt } from '@/utils/jwt';
|
||||
import { getLogoImage } from '@/utils/logo';
|
||||
import { getBrowserId, getToken, removeToken, setToken } from '@/utils/storage';
|
||||
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 { history, useIntl, useModel } from '@umijs/max';
|
||||
import { Image, theme } from 'antd';
|
||||
import { useEffect } from 'react';
|
||||
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 getDomainTitle = () => {
|
||||
switch (process.env.DOMAIN_ENV) {
|
||||
case 'gms':
|
||||
return 'gms.title';
|
||||
case 'sgw':
|
||||
return 'sgw.title';
|
||||
case 'spole':
|
||||
return 'spole.title';
|
||||
default:
|
||||
return 'Smatec Master';
|
||||
}
|
||||
};
|
||||
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 token = getToken();
|
||||
if (!token) {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
}
|
||||
const parsed = parseJwt(token);
|
||||
const { exp } = parsed;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const oneHour = 60 * 60;
|
||||
if (exp - now < oneHour) {
|
||||
removeToken();
|
||||
const isRefreshTokenExpired = checkRefreshTokenExpired(refreshToken);
|
||||
if (isRefreshTokenExpired) {
|
||||
removeAccessToken();
|
||||
removeRefreshToken();
|
||||
return;
|
||||
} else {
|
||||
const userInfo = await apiQueryProfile();
|
||||
if (userInfo) {
|
||||
@@ -66,147 +118,263 @@ const LoginPage = () => {
|
||||
}, []);
|
||||
|
||||
const handleLogin = async (values: MasterModel.LoginRequestBody) => {
|
||||
try {
|
||||
const { email, password } = values;
|
||||
const resp = await apiLogin({
|
||||
guid: getBrowserId(),
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (resp?.token) {
|
||||
setToken(resp.token);
|
||||
const userInfo = await apiQueryProfile();
|
||||
if (userInfo) {
|
||||
flushSync(() => {
|
||||
setInitialState((s: any) => ({
|
||||
...s,
|
||||
currentUserProfile: userInfo,
|
||||
}));
|
||||
});
|
||||
}
|
||||
if (redirect) {
|
||||
history.push(redirect);
|
||||
} else {
|
||||
history.push(ROUTER_HOME);
|
||||
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!',
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
height: '100vh',
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||
}}
|
||||
>
|
||||
<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: intl.formatMessage({
|
||||
id: 'master.auth.login.title',
|
||||
defaultMessage: 'Đăng nhập',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
onFinish={async (values: MasterModel.LoginRequestBody) =>
|
||||
handleLogin(values)
|
||||
}
|
||||
>
|
||||
<>
|
||||
<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: intl.formatMessage({
|
||||
id: 'master.auth.validation.email',
|
||||
defaultMessage: 'Email không được để trống!',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name="password"
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
autoComplete: 'current-password',
|
||||
prefix: (
|
||||
<LockOutlined
|
||||
style={{
|
||||
color: token.colorText,
|
||||
}}
|
||||
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!',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
</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%',
|
||||
backgroundColor: isDark ? '#000' : 'white',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<Footer />
|
||||
{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>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
export default LoginPage;
|
||||
|
||||
Reference in New Issue
Block a user