feat: improve update user, reset password

This commit is contained in:
fantasticit 2022-07-03 09:51:25 +08:00
parent 355b666704
commit 95400da337
7 changed files with 239 additions and 140 deletions

View File

@ -1,10 +1,11 @@
import { IconSpin } from '@douyinfe/semi-icons';
import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui';
import { Avatar, Button, Dropdown, Modal, Toast, Typography } from '@douyinfe/semi-ui';
import { useUser } from 'data/user';
import { useToggle } from 'hooks/use-toggle';
import Router from 'next/router';
import React, { useCallback } from 'react';
import { ResetPassword } from './reset-password';
import { UserSetting } from './setting';
const { Text } = Typography;
@ -12,11 +13,17 @@ const { Text } = Typography;
export const User: React.FC = () => {
const { user, loading, error, toLogin, logout } = useUser();
const [visible, toggleVisible] = useToggle(false);
const [resetVisible, toggleResetVisible] = useToggle(false);
const toAdmin = useCallback(() => {
Router.push('/admin');
}, []);
const onResetSuccess = useCallback(() => {
Toast.success('请重新登录');
Router.replace(`/login?redirect=${Router.asPath}`);
}, []);
if (loading) return <Button icon={<IconSpin />} theme="borderless" type="tertiary" />;
if (error || !user) {
@ -37,6 +44,9 @@ export const User: React.FC = () => {
<Dropdown.Item onClick={() => toggleVisible(true)}>
<Text></Text>
</Dropdown.Item>
<Dropdown.Item onClick={toggleResetVisible}>
<Text></Text>
</Dropdown.Item>
{user.isSystemAdmin ? (
<Dropdown.Item onClick={toAdmin}>
<Text></Text>
@ -64,6 +74,9 @@ export const User: React.FC = () => {
/>
</Dropdown>
<UserSetting visible={visible} toggleVisible={toggleVisible} />
<Modal title="重置密码" visible={resetVisible} onCancel={toggleResetVisible} footer={null}>
<ResetPassword onSuccess={onResetSuccess} />
</Modal>
</>
);
};

View File

@ -0,0 +1,112 @@
import { Button, Col, Form, Row, Toast, Typography } from '@douyinfe/semi-ui';
import { useResetPassword, useVerifyCode } from 'data/user';
import { useInterval } from 'hooks/use-interval';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useState } from 'react';
export const ResetPassword = ({ onSuccess }) => {
const [email, setEmail] = useState('');
const [hasSendVerifyCode, toggleHasSendVerifyCode] = useToggle(false);
const [countDown, setCountDown] = useState(0);
const { reset, loading } = useResetPassword();
const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode();
const onFormChange = useCallback((formState) => {
setEmail(formState.values.email);
}, []);
const { start, stop } = useInterval(() => {
setCountDown((v) => {
if (v - 1 <= 0) {
stop();
toggleHasSendVerifyCode(false);
return 0;
}
return v - 1;
});
}, 1000);
const onFinish = useCallback(
(values) => {
reset(values).then((res) => {
onSuccess && onSuccess();
});
},
[reset, onSuccess]
);
const getVerifyCode = useCallback(() => {
stop();
sendVerifyCode({ email })
.then(() => {
Toast.success('请前往邮箱查收验证码');
setCountDown(60);
start();
toggleHasSendVerifyCode(true);
})
.catch(() => {
toggleHasSendVerifyCode(false);
});
}, [email, toggleHasSendVerifyCode, sendVerifyCode, start, stop]);
return (
<Form initValues={{ name: '', password: '' }} onChange={onFormChange} onSubmit={onFinish}>
<Form.Input
noLabel
field="email"
placeholder={'请输入邮箱'}
rules={[
{
type: 'email',
message: '请输入正确的邮箱地址!',
},
{
required: true,
message: '请输入邮箱地址!',
},
]}
/>
<Row gutter={8} style={{ paddingTop: 12 }}>
<Col span={16}>
<Form.Input
noLabel
fieldStyle={{ paddingTop: 0 }}
placeholder={'请输入验证码'}
field="verifyCode"
rules={[{ required: true, message: '请输入邮箱收到的验证码!' }]}
/>
</Col>
<Col span={8}>
<Button disabled={!email || countDown > 0} loading={sendVerifyCodeLoading} onClick={getVerifyCode} block>
{hasSendVerifyCode ? countDown : '获取验证码'}
</Button>
</Col>
</Row>
<Form.Input
noLabel
mode="password"
field="password"
label="密码"
style={{ width: '100%' }}
placeholder="输入用户密码"
rules={[{ required: true, message: '请输入新密码' }]}
/>
<Form.Input
noLabel
mode="password"
field="confirmPassword"
label="密码"
style={{ width: '100%' }}
placeholder="确认用户密码"
rules={[{ required: true, message: '请再次输入密码' }]}
/>
<Button htmlType="submit" type="primary" theme="solid" block loading={loading} style={{ margin: '16px 0' }}>
</Button>
</Form>
);
};

View File

@ -1,8 +1,10 @@
import { Avatar, Form, Modal, Space } from '@douyinfe/semi-ui';
import { Avatar, Button, Col, Form, Modal, Row, Space, Toast } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { Upload } from 'components/upload';
import { useUser } from 'data/user';
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
import { useUser, useVerifyCode } from 'data/user';
import { useInterval } from 'hooks/use-interval';
import { useToggle } from 'hooks/use-toggle';
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
interface IProps {
visible: boolean;
@ -13,24 +15,60 @@ export const UserSetting: React.FC<IProps> = ({ visible, toggleVisible }) => {
const $form = useRef<FormApi>();
const { user, loading, updateUser } = useUser();
const [currentAvatar, setCurrentAvatar] = useState('');
const [email, setEmail] = useState('');
const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode();
const [hasSendVerifyCode, toggleHasSendVerifyCode] = useToggle(false);
const [countDown, setCountDown] = useState(0);
const setAvatar = (url) => {
const setAvatar = useCallback((url) => {
$form.current.setValue('avatar', url);
setCurrentAvatar(url);
};
}, []);
const handleOk = () => {
const handleOk = useCallback(() => {
$form.current.validate().then((values) => {
if (!values.email) {
delete values.email;
}
updateUser(values);
updateUser(values).then(() => {
Toast.success('账户信息已更新');
toggleVisible(false);
});
};
const handleCancel = () => {
});
}, [toggleVisible, updateUser]);
const handleCancel = useCallback(() => {
toggleVisible(false);
};
}, [toggleVisible]);
const onFormChange = useCallback((formState) => {
setEmail(formState.values.email);
}, []);
const { start, stop } = useInterval(() => {
setCountDown((v) => {
if (v - 1 <= 0) {
stop();
toggleHasSendVerifyCode(false);
return 0;
}
return v - 1;
});
}, 1000);
const getVerifyCode = useCallback(() => {
stop();
sendVerifyCode({ email })
.then(() => {
Toast.success('请前往邮箱查收验证码');
setCountDown(60);
start();
toggleHasSendVerifyCode(true);
})
.catch(() => {
toggleHasSendVerifyCode(false);
});
}, [email, toggleHasSendVerifyCode, sendVerifyCode, start, stop]);
useEffect(() => {
if (!user || !$form.current) return;
@ -51,6 +89,7 @@ export const UserSetting: React.FC<IProps> = ({ visible, toggleVisible }) => {
initValues={{ avatar: user.avatar, name: user.name, email: user.email }}
getFormApi={(formApi) => ($form.current = formApi)}
labelPosition="left"
onChange={onFormChange}
>
<Form.Slot label="头像">
<Space align="end">
@ -58,6 +97,7 @@ export const UserSetting: React.FC<IProps> = ({ visible, toggleVisible }) => {
<Upload onOK={setAvatar} />
</Space>
</Form.Slot>
<Form.Input
label="账户"
field="name"
@ -65,13 +105,34 @@ export const UserSetting: React.FC<IProps> = ({ visible, toggleVisible }) => {
disabled
placeholder="请输入账户名称"
></Form.Input>
<Form.Input label="邮箱" field="email" style={{ width: '100%' }} placeholder="请输入账户邮箱"></Form.Input>
{email && email !== user.email ? (
<Form.Slot label="验证码">
<Row gutter={8}>
<Col span={16}>
<Form.Input
disabled
label="邮箱"
field="email"
style={{ width: '100%' }}
placeholder="请输入账户邮箱"
></Form.Input>
noLabel
fieldStyle={{ paddingTop: 0 }}
placeholder={'请输入验证码'}
field="verifyCode"
rules={[{ required: true, message: '请输入邮箱收到的验证码!' }]}
/>
</Col>
<Col span={8}>
<Button
disabled={!email || countDown > 0}
loading={sendVerifyCodeLoading}
onClick={getVerifyCode}
block
>
{hasSendVerifyCode ? countDown : '获取验证码'}
</Button>
</Col>
</Row>
</Form.Slot>
) : null}
</Form>
</Modal>
);

View File

@ -1,11 +1,9 @@
import { Button, Col, Form, Layout, Modal, Row, Space, Toast, Typography } from '@douyinfe/semi-ui';
import { Layout, Modal, Space, Typography } from '@douyinfe/semi-ui';
import { Author } from 'components/author';
import { LogoImage, LogoText } from 'components/logo';
import { Seo } from 'components/seo';
import { useResetPassword, useVerifyCode } from 'data/user';
import { useInterval } from 'hooks/use-interval';
import { ResetPassword } from 'components/user/reset-password';
import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle';
import Link from 'next/link';
import Router from 'next/router';
import React, { useCallback, useState } from 'react';
@ -18,30 +16,7 @@ const { Title, Text } = Typography;
const Page = () => {
const query = useRouterQuery();
const [email, setEmail] = useState('');
const [hasSendVerifyCode, toggleHasSendVerifyCode] = useToggle(false);
const [countDown, setCountDown] = useState(0);
const { reset, loading } = useResetPassword();
const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode();
const onFormChange = useCallback((formState) => {
setEmail(formState.values.email);
}, []);
const { start, stop } = useInterval(() => {
setCountDown((v) => {
if (v - 1 <= 0) {
stop();
toggleHasSendVerifyCode(false);
return 0;
}
return v - 1;
});
}, 1000);
const onFinish = useCallback(
(values) => {
reset(values).then((res) => {
const onResetSucccess = useCallback(() => {
Modal.confirm({
title: <Title heading={5}></Title>,
content: <Text>?</Text>,
@ -51,24 +26,7 @@ const Page = () => {
Router.push('/login', { query });
},
});
});
},
[reset, query]
);
const getVerifyCode = useCallback(() => {
stop();
sendVerifyCode({ email })
.then(() => {
Toast.success('请前往邮箱查收验证码');
setCountDown(60);
start();
toggleHasSendVerifyCode(true);
})
.catch(() => {
toggleHasSendVerifyCode(false);
});
}, [email, toggleHasSendVerifyCode, sendVerifyCode, start, stop]);
}, [query]);
return (
<Layout className={styles.wrap}>
@ -80,72 +38,11 @@ const Page = () => {
<LogoText></LogoText>
</Space>
</Title>
<Form
className={styles.form}
initValues={{ name: '', password: '' }}
onChange={onFormChange}
onSubmit={onFinish}
>
<div className={styles.form}>
<Title type="tertiary" heading={5} style={{ marginBottom: 16, textAlign: 'center' }}>
</Title>
<Form.Input
noLabel
field="email"
placeholder={'请输入邮箱'}
rules={[
{
type: 'email',
message: '请输入正确的邮箱地址!',
},
{
required: true,
message: '请输入邮箱地址!',
},
]}
/>
<Row gutter={8} style={{ paddingTop: 12 }}>
<Col span={16}>
<Form.Input
noLabel
fieldStyle={{ paddingTop: 0 }}
placeholder={'请输入验证码'}
field="verifyCode"
rules={[{ required: true, message: '请输入邮箱收到的验证码!' }]}
/>
</Col>
<Col span={8}>
<Button disabled={!email || countDown > 0} loading={sendVerifyCodeLoading} onClick={getVerifyCode} block>
{hasSendVerifyCode ? countDown : '获取验证码'}
</Button>
</Col>
</Row>
<Form.Input
noLabel
mode="password"
field="password"
label="密码"
style={{ width: '100%' }}
placeholder="输入用户密码"
rules={[{ required: true, message: '请输入新密码' }]}
/>
<Form.Input
noLabel
mode="password"
field="confirmPassword"
label="密码"
style={{ width: '100%' }}
placeholder="确认用户密码"
rules={[{ required: true, message: '请再次输入密码' }]}
/>
<Button htmlType="submit" type="primary" theme="solid" block loading={loading} style={{ margin: '16px 0' }}>
</Button>
<ResetPassword onSuccess={onResetSucccess} />
<footer>
<Link
href={{
@ -158,7 +55,7 @@ const Page = () => {
</Text>
</Link>
</footer>
</Form>
</div>
</Content>
<Footer>
<Author></Author>

View File

@ -1,4 +1,4 @@
import { IsEmail, IsOptional, IsString } from 'class-validator';
import { IsEmail, IsOptional, IsString, MinLength } from 'class-validator';
export class UpdateUserDto {
@IsString({ message: '用户头像类型错误正确类型为String' })
@ -9,4 +9,9 @@ export class UpdateUserDto {
@IsEmail()
@IsOptional()
readonly email?: string;
@MinLength(5, { message: '邮箱验证码至少5个字符' })
@IsString({ message: '邮箱验证码错误正确类型为String' })
@IsOptional({ message: '邮箱验证码不能为空' })
verifyCode?: string;
}

View File

@ -64,7 +64,7 @@ export class UserService {
throw new Error(`请指定名称、密码和邮箱`);
}
if (await this.userRepo.findOne({ name: config.name })) {
if (await this.userRepo.findOne({ email: config.email })) {
return;
}
@ -251,6 +251,17 @@ export class UserService {
*/
async updateUser(user: UserEntity, dto: UpdateUserDto): Promise<IUser> {
const oldData = await this.userRepo.findOne(user.id);
if (oldData.email !== dto.email) {
if (await this.userRepo.findOne({ where: { email: dto.email } })) {
throw new HttpException('该邮箱已被注册', HttpStatus.BAD_REQUEST);
}
if (!(await this.verifyService.checkVerifyCode(dto.email, dto.verifyCode))) {
throw new HttpException('验证码不正确,请检查', HttpStatus.BAD_REQUEST);
}
}
const res = await this.userRepo.merge(oldData, dto);
const ret = await this.userRepo.save(res);
return instanceToPlain(ret) as IUser;

View File

@ -40,7 +40,7 @@ export class VerifyService {
await this.redis.set(`verify-${email}`, verifyCode, 'EX', 5 * 60);
await this.systemService.sendEmail({
to: email,
subject: '验证码',
subject: '云策文档-验证码',
html: `<p>您的验证码为 ${verifyCode}</p>`,
});
};