-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(apps): add a new user password resetting module (#141)
- Loading branch information
Showing
17 changed files
with
463 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,7 @@ | |
} | ||
|
||
.logo { | ||
width: 100%; | ||
margin-bottom: 100px; | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
.wrapper { | ||
background: #F7F8F9; | ||
} | ||
|
||
.layout { | ||
min-height: 100vh; | ||
width: fit-content; | ||
margin: auto; | ||
flex-direction: row; | ||
} | ||
|
||
.container { | ||
display: flex; | ||
justify-content: center; | ||
max-width: 100vw; | ||
min-height: 100vh; | ||
|
||
&:global(.ant-card) { | ||
background: #F7F8F9; | ||
} | ||
|
||
:global(.ant-card-body) { | ||
min-height: 100vh; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
flex-direction: column; | ||
padding-left: 60px; | ||
padding-right: 60px; | ||
} | ||
|
||
&.form-container { | ||
:global(.ant-card-body) { | ||
width: 480px; | ||
} | ||
} | ||
|
||
.form { | ||
width: 100%; | ||
} | ||
|
||
.toolbar { | ||
align-self: flex-end; | ||
.switch-language { | ||
span { | ||
margin-left: 4px; | ||
vertical-align: middle; | ||
} | ||
} | ||
} | ||
|
||
.password-tip { | ||
font-size: 16px; | ||
margin-bottom: 16px; | ||
|
||
.password-tip-icon { | ||
color: #faad14; | ||
margin-right: 8px; | ||
} | ||
} | ||
} | ||
|
||
.reset-result { | ||
width: 100%; | ||
margin-bottom: 24px; | ||
|
||
.reset-result-title { | ||
text-align: center; | ||
font-size: 16px; | ||
margin-bottom: 16px; | ||
|
||
.reset-result-icon { | ||
color: #52c41a; | ||
margin-right: 8px; | ||
} | ||
} | ||
} | ||
|
||
.logo { | ||
width: 100%; | ||
margin-bottom: 100px; | ||
} | ||
|
||
.bgImg { | ||
width: 540px; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { useCallback, useRef, useState } from 'react' | ||
import { AxiosError } from 'axios' | ||
import { Button, Card, Form, Input, Layout } from 'antd' | ||
import { | ||
CheckCircleFilled, | ||
DownOutlined, | ||
FrownOutlined, | ||
KeyOutlined, | ||
} from '@ant-design/icons' | ||
import { useI18n } from '@i18n-macro' | ||
import { useAuthState } from '@store/auth' | ||
import { UserInfo } from '@/api/model' | ||
import { doUserLogout, resetUserPassword } from '@/api/hooks/platform' | ||
import LanguageDropdown from '@/components/LanguageDropdown' | ||
import { Logo } from '@/components/Logo' | ||
import loginBgImg from '/img/background/login.svg' | ||
|
||
import styles from './index.module.less' | ||
|
||
export default function ResetPassword() { | ||
const { t } = useI18n() | ||
|
||
const clearPersistedAuth = useAuthState((state) => state.clearPersistedAuth) | ||
const dispatchLogout = useAuthState((state) => state.logout) | ||
const userId = useAuthState((state) => state.session) | ||
const passwordExpired = useAuthState((state) => state.passwordExpired) | ||
|
||
const [resetAlread, setResetAlread] = useState(false) | ||
|
||
const [refForm] = Form.useForm() | ||
const refPassword = useRef<Input>(null) | ||
|
||
const { handleSubmit, errorMsg, clearErrorMsg, loading } = useResetPassword( | ||
(data) => { | ||
if (data) { | ||
clearPersistedAuth() | ||
setResetAlread(true) | ||
} | ||
}, | ||
() => { | ||
refForm.setFieldsValue({ password: '' }) | ||
refPassword.current?.focus() | ||
} | ||
) | ||
|
||
const backToLogin = useCallback(() => { | ||
doUserLogout().then(dispatchLogout) | ||
}, [dispatchLogout]) | ||
|
||
return ( | ||
<div className={styles.wrapper}> | ||
<Layout className={styles.layout}> | ||
<Card | ||
className={`${styles.container} ${styles.formContainer}`} | ||
bordered={false} | ||
> | ||
<Logo type="common" className={styles.logo} logoWidth={140} /> | ||
{resetAlread ? ( | ||
<div className={styles.resetResult}> | ||
<div className={styles.resetResultTitle}> | ||
<CheckCircleFilled className={styles.resetResultIcon} /> | ||
{t('message.success')} | ||
</div> | ||
<Button type="primary" size="large" block onClick={backToLogin}> | ||
{t('result.login')} | ||
</Button> | ||
</div> | ||
) : ( | ||
<Form | ||
className={styles.form} | ||
onFinish={handleSubmit} | ||
layout="vertical" | ||
form={refForm} | ||
> | ||
{passwordExpired && ( | ||
<div className={styles.passwordTip}> | ||
<FrownOutlined className={styles.passwordTipIcon} /> | ||
{t('form.resetTips')} | ||
</div> | ||
)} | ||
<Form.Item name="userId" hidden initialValue={userId}> | ||
<Input disabled /> | ||
</Form.Item> | ||
<Form.Item | ||
name="password" | ||
{...(errorMsg && { | ||
help: t('message.error', { msg: errorMsg }), | ||
validateStatus: 'error', | ||
})} | ||
rules={[ | ||
{ | ||
required: true, | ||
pattern: | ||
/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[a-zA-Z0-9~!@#$%^&*]{8,16}$/, | ||
message: t('rules.password'), | ||
}, | ||
]} | ||
> | ||
<Input.Password | ||
prefix={<KeyOutlined />} | ||
placeholder={t('form.password')} | ||
size="large" | ||
minLength={8} | ||
maxLength={20} | ||
disabled={loading} | ||
onInput={clearErrorMsg} | ||
ref={refPassword} | ||
/> | ||
</Form.Item> | ||
<Form.Item> | ||
<Button | ||
type="primary" | ||
htmlType="submit" | ||
size="large" | ||
loading={loading} | ||
block | ||
> | ||
{t('form.submit')} | ||
</Button> | ||
</Form.Item> | ||
</Form> | ||
)} | ||
<div className={styles.toolbar}> | ||
<LanguageDropdown className={styles.switchLanguage}> | ||
<a> | ||
{t('switch_language')} <DownOutlined /> | ||
</a> | ||
</LanguageDropdown> | ||
</div> | ||
</Card> | ||
<Card className={styles.container} bordered={false}> | ||
<img className={styles.bgImg} src={loginBgImg} alt="Landing" /> | ||
</Card> | ||
</Layout> | ||
</div> | ||
) | ||
} | ||
|
||
function useResetPassword( | ||
onSuccess: (data: UserInfo) => void, | ||
onFailure: () => void | ||
) { | ||
const { t } = useI18n() | ||
|
||
const [loading, setLoading] = useState(false) | ||
|
||
const [errorMsg, setErrorMsg] = useState<string>() | ||
|
||
const clearErrorMsg = () => setErrorMsg(undefined) | ||
|
||
const handleSubmit = async (form: { userId: string; password: string }) => { | ||
clearErrorMsg() | ||
setLoading(true) | ||
try { | ||
const result = await resetUserPassword({ | ||
userId: form.userId, | ||
password: form.password, | ||
}) | ||
setLoading(false) | ||
onSuccess(result.data.data!) | ||
} catch (err) { | ||
setErrorMsg( | ||
t(`error:${(err as AxiosError).response?.data?.code}`, 'unknown') | ||
) | ||
onFailure() | ||
// when success, this component will be unmounted immediately so no need to setLoading() | ||
setLoading(false) | ||
} | ||
} | ||
|
||
return { handleSubmit, loading, errorMsg, clearErrorMsg } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { definePage } from '@/model/page' | ||
|
||
export default definePage({}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
name: Reset Password | ||
switch_language: Switch Language | ||
form: | ||
resetTips: Your password is expired, please reset it | ||
password: New Password | ||
submit: Reset | ||
rules: | ||
password: 'Password must be 8 to 20 characters that must contain uppercase letters, lowercase letters and numbers, and may contain visible special characters !@#$%^&*' | ||
result: | ||
login: Back to Sign in | ||
message: | ||
error: 'Failed to reset your password: {{ msg }}' | ||
success: Your password has been reset |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
name: 重置密码 | ||
switch_language: 更换语言 | ||
form: | ||
resetTips: 密码已过期,请重置密码 | ||
password: 新密码 | ||
submit: 重置密码 | ||
rules: | ||
password: 密码必须是 6 - 20 个字符,必须包含大写字母、小写字母和数字的组合,可包含特殊字符 ~!@#$%^&* | ||
result: | ||
login: 重新登入 | ||
message: | ||
error: '重置密码失败: {{ msg }}' | ||
success: 重置密码成功 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { PropsWithChildren } from 'react' | ||
import BlankLayout from './layouts' | ||
|
||
export default function ({ children }: PropsWithChildren<{}>) { | ||
return <BlankLayout>{children}</BlankLayout> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.blank-layout { | ||
min-height: 100vh; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { FC } from 'react' | ||
import { Layout } from 'antd' | ||
import styles from './index.module.less' | ||
|
||
const BlankLayout: FC = (props) => { | ||
return <Layout className={styles.blankLayout}>{props.children}</Layout> | ||
} | ||
|
||
export default BlankLayout |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { definePage } from '@/model/page' | ||
import { resolveRoute } from '@pages-macro' | ||
|
||
export default definePage({ | ||
role: ['user'], | ||
redirect(_, location) { | ||
if (location.pathname === resolveRoute()) return resolveRoute('../') | ||
}, | ||
}) |
Oops, something went wrong.