mirror of https://github.com/fantasticit/think.git
feat: improve update user, reset password
This commit is contained in:
parent
355b666704
commit
95400da337
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>`,
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue