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 { 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 { useUser } from 'data/user';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import Router from 'next/router'; import Router from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { ResetPassword } from './reset-password';
import { UserSetting } from './setting'; import { UserSetting } from './setting';
const { Text } = Typography; const { Text } = Typography;
@ -12,11 +13,17 @@ const { Text } = Typography;
export const User: React.FC = () => { export const User: React.FC = () => {
const { user, loading, error, toLogin, logout } = useUser(); const { user, loading, error, toLogin, logout } = useUser();
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
const [resetVisible, toggleResetVisible] = useToggle(false);
const toAdmin = useCallback(() => { const toAdmin = useCallback(() => {
Router.push('/admin'); 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 (loading) return <Button icon={<IconSpin />} theme="borderless" type="tertiary" />;
if (error || !user) { if (error || !user) {
@ -37,6 +44,9 @@ export const User: React.FC = () => {
<Dropdown.Item onClick={() => toggleVisible(true)}> <Dropdown.Item onClick={() => toggleVisible(true)}>
<Text></Text> <Text></Text>
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item onClick={toggleResetVisible}>
<Text></Text>
</Dropdown.Item>
{user.isSystemAdmin ? ( {user.isSystemAdmin ? (
<Dropdown.Item onClick={toAdmin}> <Dropdown.Item onClick={toAdmin}>
<Text></Text> <Text></Text>
@ -64,6 +74,9 @@ export const User: React.FC = () => {
/> />
</Dropdown> </Dropdown>
<UserSetting visible={visible} toggleVisible={toggleVisible} /> <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 { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { Upload } from 'components/upload'; import { Upload } from 'components/upload';
import { useUser } from 'data/user'; import { useUser, useVerifyCode } from 'data/user';
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import { useInterval } from 'hooks/use-interval';
import { useToggle } from 'hooks/use-toggle';
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
interface IProps { interface IProps {
visible: boolean; visible: boolean;
@ -13,24 +15,60 @@ export const UserSetting: React.FC<IProps> = ({ visible, toggleVisible }) => {
const $form = useRef<FormApi>(); const $form = useRef<FormApi>();
const { user, loading, updateUser } = useUser(); const { user, loading, updateUser } = useUser();
const [currentAvatar, setCurrentAvatar] = useState(''); 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); $form.current.setValue('avatar', url);
setCurrentAvatar(url); setCurrentAvatar(url);
}; }, []);
const handleOk = () => { const handleOk = useCallback(() => {
$form.current.validate().then((values) => { $form.current.validate().then((values) => {
if (!values.email) { if (!values.email) {
delete values.email; delete values.email;
} }
updateUser(values); updateUser(values).then(() => {
Toast.success('账户信息已更新');
toggleVisible(false); toggleVisible(false);
}); });
}; });
const handleCancel = () => { }, [toggleVisible, updateUser]);
const handleCancel = useCallback(() => {
toggleVisible(false); 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(() => { useEffect(() => {
if (!user || !$form.current) return; 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 }} initValues={{ avatar: user.avatar, name: user.name, email: user.email }}
getFormApi={(formApi) => ($form.current = formApi)} getFormApi={(formApi) => ($form.current = formApi)}
labelPosition="left" labelPosition="left"
onChange={onFormChange}
> >
<Form.Slot label="头像"> <Form.Slot label="头像">
<Space align="end"> <Space align="end">
@ -58,6 +97,7 @@ export const UserSetting: React.FC<IProps> = ({ visible, toggleVisible }) => {
<Upload onOK={setAvatar} /> <Upload onOK={setAvatar} />
</Space> </Space>
</Form.Slot> </Form.Slot>
<Form.Input <Form.Input
label="账户" label="账户"
field="name" field="name"
@ -65,13 +105,34 @@ export const UserSetting: React.FC<IProps> = ({ visible, toggleVisible }) => {
disabled disabled
placeholder="请输入账户名称" placeholder="请输入账户名称"
></Form.Input> ></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 <Form.Input
disabled noLabel
label="邮箱" fieldStyle={{ paddingTop: 0 }}
field="email" placeholder={'请输入验证码'}
style={{ width: '100%' }} field="verifyCode"
placeholder="请输入账户邮箱" rules={[{ required: true, message: '请输入邮箱收到的验证码!' }]}
></Form.Input> />
</Col>
<Col span={8}>
<Button
disabled={!email || countDown > 0}
loading={sendVerifyCodeLoading}
onClick={getVerifyCode}
block
>
{hasSendVerifyCode ? countDown : '获取验证码'}
</Button>
</Col>
</Row>
</Form.Slot>
) : null}
</Form> </Form>
</Modal> </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 { Author } from 'components/author';
import { LogoImage, LogoText } from 'components/logo'; import { LogoImage, LogoText } from 'components/logo';
import { Seo } from 'components/seo'; import { Seo } from 'components/seo';
import { useResetPassword, useVerifyCode } from 'data/user'; import { ResetPassword } from 'components/user/reset-password';
import { useInterval } from 'hooks/use-interval';
import { useRouterQuery } from 'hooks/use-router-query'; import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle';
import Link from 'next/link'; import Link from 'next/link';
import Router from 'next/router'; import Router from 'next/router';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
@ -18,30 +16,7 @@ const { Title, Text } = Typography;
const Page = () => { const Page = () => {
const query = useRouterQuery(); const query = useRouterQuery();
const [email, setEmail] = useState(''); const onResetSucccess = useCallback(() => {
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) => {
Modal.confirm({ Modal.confirm({
title: <Title heading={5}></Title>, title: <Title heading={5}></Title>,
content: <Text>?</Text>, content: <Text>?</Text>,
@ -51,24 +26,7 @@ const Page = () => {
Router.push('/login', { query }); Router.push('/login', { query });
}, },
}); });
}); }, [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]);
return ( return (
<Layout className={styles.wrap}> <Layout className={styles.wrap}>
@ -80,72 +38,11 @@ const Page = () => {
<LogoText></LogoText> <LogoText></LogoText>
</Space> </Space>
</Title> </Title>
<Form <div className={styles.form}>
className={styles.form}
initValues={{ name: '', password: '' }}
onChange={onFormChange}
onSubmit={onFinish}
>
<Title type="tertiary" heading={5} style={{ marginBottom: 16, textAlign: 'center' }}> <Title type="tertiary" heading={5} style={{ marginBottom: 16, textAlign: 'center' }}>
</Title> </Title>
<ResetPassword onSuccess={onResetSucccess} />
<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>
<footer> <footer>
<Link <Link
href={{ href={{
@ -158,7 +55,7 @@ const Page = () => {
</Text> </Text>
</Link> </Link>
</footer> </footer>
</Form> </div>
</Content> </Content>
<Footer> <Footer>
<Author></Author> <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 { export class UpdateUserDto {
@IsString({ message: '用户头像类型错误正确类型为String' }) @IsString({ message: '用户头像类型错误正确类型为String' })
@ -9,4 +9,9 @@ export class UpdateUserDto {
@IsEmail() @IsEmail()
@IsOptional() @IsOptional()
readonly email?: string; 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(`请指定名称、密码和邮箱`); throw new Error(`请指定名称、密码和邮箱`);
} }
if (await this.userRepo.findOne({ name: config.name })) { if (await this.userRepo.findOne({ email: config.email })) {
return; return;
} }
@ -251,6 +251,17 @@ export class UserService {
*/ */
async updateUser(user: UserEntity, dto: UpdateUserDto): Promise<IUser> { async updateUser(user: UserEntity, dto: UpdateUserDto): Promise<IUser> {
const oldData = await this.userRepo.findOne(user.id); 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 res = await this.userRepo.merge(oldData, dto);
const ret = await this.userRepo.save(res); const ret = await this.userRepo.save(res);
return instanceToPlain(ret) as IUser; 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.redis.set(`verify-${email}`, verifyCode, 'EX', 5 * 60);
await this.systemService.sendEmail({ await this.systemService.sendEmail({
to: email, to: email,
subject: '验证码', subject: '云策文档-验证码',
html: `<p>您的验证码为 ${verifyCode}</p>`, html: `<p>您的验证码为 ${verifyCode}</p>`,
}); });
}; };