Skip to content

Commit

Permalink
feat(apps): add a new user password resetting module (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yuiham authored Jun 2, 2022
1 parent fb81b48 commit 70d3151
Show file tree
Hide file tree
Showing 17 changed files with 463 additions and 25 deletions.
2 changes: 2 additions & 0 deletions apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ export default function prepareApps() {
return mountRouter(
[
loadRoutes<IPageMeta>('./landing', '/login'),
loadRoutes<IPageMeta>('./user', '/user'),
loadRoutes<IPageMeta>('./main', '/'),
],
{
noSession: '/login',
noSafeSession: '/user/password',
noPermission: '/login',
}
)
Expand Down
1 change: 1 addition & 0 deletions apps/landing/index.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
}

.logo {
width: 100%;
margin-bottom: 100px;
}

Expand Down
26 changes: 21 additions & 5 deletions apps/landing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,25 @@ export default function Login() {

const { handleSubmit, errorMsg, clearErrorMsg, loading } = useLogin(
(data) => {
dispatchLogin(data.token!, data.userId!)
push(from)
if (data.passwordExpired) {
dispatchLogin(
{
token: data.token!,
session: data.userId!,
passwordExpired: true,
},
{
persisted: false,
}
)
} else {
dispatchLogin({
token: data.token!,
session: data.userId!,
passwordExpired: false,
})
push(from)
}
},
() => {
refForm.setFieldsValue({ password: '' })
Expand All @@ -45,13 +62,13 @@ export default function Login() {
className={`${styles.container} ${styles.formContainer}`}
bordered={false}
>
<Logo type="common" className={styles.logo} logoWidth={140} />
<Form
className={styles.form}
onFinish={handleSubmit}
layout="vertical"
form={refForm}
>
<Logo type="common" className={styles.logo} logoWidth={140} />
<Form.Item name="username" rules={[{ required: true }]}>
<Input
prefix={<UserOutlined />}
Expand All @@ -67,10 +84,9 @@ export default function Login() {
validateStatus: 'error',
})}
>
<Input
<Input.Password
prefix={<KeyOutlined />}
placeholder={t('form.password')}
type="password"
size="large"
disabled={loading}
onInput={clearErrorMsg}
Expand Down
86 changes: 86 additions & 0 deletions apps/user/[-1]password/index.module.less
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;
}
172 changes: 172 additions & 0 deletions apps/user/[-1]password/index.tsx
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 }
}
3 changes: 3 additions & 0 deletions apps/user/[-1]password/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { definePage } from '@/model/page'

export default definePage({})
13 changes: 13 additions & 0 deletions apps/user/[-1]password/translations/en.yaml
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
13 changes: 13 additions & 0 deletions apps/user/[-1]password/translations/zh.yaml
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: 重置密码成功
6 changes: 6 additions & 0 deletions apps/user/layout.sync.tsx
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>
}
3 changes: 3 additions & 0 deletions apps/user/layouts/index.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.blank-layout {
min-height: 100vh;
}
9 changes: 9 additions & 0 deletions apps/user/layouts/index.tsx
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
9 changes: 9 additions & 0 deletions apps/user/meta.ts
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('../')
},
})
Loading

0 comments on commit 70d3151

Please sign in to comment.