Skip to content

Commit a20a2ac

Browse files
committed
Rework e-mail change
Fixes: #45
1 parent 303e9e5 commit a20a2ac

File tree

10 files changed

+146
-8
lines changed

10 files changed

+146
-8
lines changed

apps/api/src/auth/auth.controller.ts

+13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Controller,
55
HttpCode,
66
Logger,
7+
NotFoundException,
78
Post,
89
Req,
910
UseGuards,
@@ -21,6 +22,7 @@ import { ChangePasswordDto } from './dto/change-password.dto';
2122
import { UserRegisterDto } from './dto/user-register.dto';
2223
import { AuthenticatedGuard } from './guards/authenticated.guard';
2324
import { LocalAuthGuard } from './guards/local-auth.guard';
25+
import { UpdateEmailDto } from './dto/update-email.dto';
2426

2527
@Controller('auth')
2628
@ApiTags('auth')
@@ -85,4 +87,15 @@ export class AuthController {
8587
await this.authService.updateUserPassword(user._id, body.newPassword);
8688
return User.toPrivateDto(user);
8789
}
90+
91+
@Post('change-email')
92+
async changeEmail(@Req() request: Request, @Body() dto: UpdateEmailDto) {
93+
const userId = new Types.ObjectId(request.user.id);
94+
const user = await this.authService.validateUserByUserId(userId, dto.password);
95+
96+
await this.authService.updateUserEmail(user._id, dto.email);
97+
await this.authEmailsService.sendEmailConfirmation(user._id);
98+
99+
return User.toPrivateDto(user);
100+
}
88101
}

apps/api/src/auth/auth.service.ts

+5
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export class AuthService {
6464
return this.usersService.findOneByIdAndUpdate(userId, { 'auth.password': hash });
6565
}
6666

67+
async updateUserEmail(userId: Types.ObjectId, email: string): Promise<UserDocument> {
68+
await this.usersService.setConfirmed(userId, false);
69+
return this.usersService.updateEmail(userId, email);
70+
}
71+
6772
private hashPassword(input: string): Promise<string> {
6873
return bcrypt.hash(input, 12);
6974
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { IsEmail, Length, Validate } from 'class-validator';
2+
import { IUpdateEmailDto } from 'shared-types';
3+
import { EmailNotTakenRule } from '../../rules/email-not-taken.rule';
4+
5+
export class UpdateEmailDto implements IUpdateEmailDto {
6+
@Length(4, 32)
7+
password: string;
8+
9+
@IsEmail()
10+
@Validate(EmailNotTakenRule)
11+
email: string;
12+
}

apps/api/src/models/users/users.service.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
1+
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
22
import { FilterQuery, Types, UpdateQuery } from 'mongoose';
33
import { GravatarService } from '../../gravatar/gravatar.service';
44
import { ImagesService } from '../../images/images.service';
@@ -56,6 +56,21 @@ export class UsersService {
5656
return await this.userRepository.findOneByIdAndUpdate(id, user);
5757
}
5858

59+
async updateEmail(id: Types.ObjectId, email: string): Promise<UserDocument | null> {
60+
const user = await this.findById(id);
61+
if (!user) return null;
62+
63+
if (user.profile.email == email) {
64+
throw new BadRequestException('New email address is identical to the current one');
65+
}
66+
67+
return this.userRepository.findOneByIdAndUpdate(id, {
68+
$set: {
69+
'profile.email': email,
70+
},
71+
});
72+
}
73+
5974
async delete(id: Types.ObjectId): Promise<UserDocument> {
6075
const user = await this.userRepository.deleteOneById(id);
6176
this.imagesService.deleteImage(user.profile);

apps/client/src/components/Pages/User/UserEmailChangeTab.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import UserEmailChangeForm from '../../User/UserEmailChangeForm';
2+
13
function UserEmailChangeTab() {
24
return (
35
<div className="mt-8">
4-
<h2 className="mb-4 text-3xl">Change E-mail address</h2>
6+
<h2 className="mb-6 text-3xl">Change E-mail address</h2>
7+
<UserEmailChangeForm />
58
</div>
69
);
710
}

apps/client/src/components/Pages/User/UserPasswordChangeTab.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import UserChangePasswordForm from '../../User/UserChangePasswordForm';
33
function UserPasswordChangeTab() {
44
return (
55
<div className="mt-8">
6-
<h2 className="mb-4 text-3xl">Change password</h2>
6+
<h2 className="mb-6 text-3xl">Change password</h2>
77
<UserChangePasswordForm />
88
</div>
99
);

apps/client/src/components/Pages/User/UserSecurityTab.tsx

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BsShieldLock } from 'react-icons/bs';
1+
import { BsEnvelopeAt, BsShieldLock } from 'react-icons/bs';
22
import IconButton from '../../IconButton';
33
import UserDangerZone from '../../User/UserDangerZone';
44
import { Link } from 'react-router-dom';
@@ -10,9 +10,14 @@ function UserSecurityTab() {
1010
<p className="mb-6 text-muted">
1111
Manage options to secure your account and protect your privacy.
1212
</p>
13-
<Link to="../change-password">
14-
<IconButton icon={BsShieldLock}>Change password</IconButton>
15-
</Link>
13+
<div className="flex gap-4">
14+
<Link to="../change-password">
15+
<IconButton icon={BsShieldLock}>Change password</IconButton>
16+
</Link>
17+
<Link to="../change-email">
18+
<IconButton icon={BsEnvelopeAt}>Change E-mail</IconButton>
19+
</Link>
20+
</div>
1621

1722
<UserDangerZone />
1823
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import axios from 'axios';
2+
import { useContext, useState } from 'react';
3+
import { useForm } from 'react-hook-form';
4+
import toast from 'react-hot-toast';
5+
import { IChangePasswordDto, IUpdateEmailDto } from 'shared-types';
6+
import { UserContext } from '../../context/UserContext';
7+
import { Utils } from '../../utils/utils';
8+
import Form from '../Form/Form';
9+
import FormField from '../Form/FormField';
10+
import FormInput from '../Form/FormInput';
11+
import FormSubmitButton from '../Form/FormSubmitButton';
12+
import Alert from '../Helpers/Alert';
13+
import { useNavigate } from 'react-router-dom';
14+
15+
type Inputs = {
16+
password: string;
17+
email: string;
18+
};
19+
20+
function UserEmailChangeForm() {
21+
const { register, handleSubmit } = useForm<Inputs>();
22+
const [loading, setLoading] = useState(false);
23+
const [error, setError] = useState<string | null>(null);
24+
25+
const navigate = useNavigate();
26+
27+
function onSubmit(inputs: Inputs) {
28+
setLoading(true);
29+
setError(null);
30+
31+
const dto: IUpdateEmailDto = inputs;
32+
33+
axios
34+
.post<void>(`/api/auth/change-email`, dto)
35+
.then(async () => {
36+
navigate('..');
37+
toast.success(`Successfully changed e-mail address`);
38+
})
39+
.catch((err) => setError(Utils.requestErrorToString(err)))
40+
.finally(() => setLoading(false));
41+
}
42+
43+
return (
44+
<Form
45+
onSubmit={handleSubmit(onSubmit)}
46+
loading={loading}
47+
>
48+
{error && <Alert>{error}</Alert>}
49+
50+
<FormField
51+
label="Current password"
52+
hint="Confirm your current password"
53+
required
54+
>
55+
<FormInput
56+
type="password"
57+
required
58+
{...register('password', { required: true })}
59+
/>
60+
</FormField>
61+
62+
<FormField
63+
label="New E-mail address"
64+
required
65+
>
66+
<FormInput
67+
type="email"
68+
required
69+
{...register('email', { required: true })}
70+
/>
71+
</FormField>
72+
73+
<FormSubmitButton>Change E-mail address</FormSubmitButton>
74+
<span className="ms-4 text-muted">(You will need to confirm this e-mail)</span>
75+
</Form>
76+
);
77+
}
78+
export default UserEmailChangeForm;

packages/shared-types/src/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ProductDto } from './product/ProductDto';
2525
import { ICreateSecurityRuleDto } from './security/ICreateSecurityRuleDto';
2626
import { IDeleteSecurityRuleDto } from './security/IDeleteSecurityRoleDto';
2727
import { IUpdateSecurityRuleDto } from './security/IUpdateSecurityRuleDto';
28+
import { IUpdateEmailDto } from './user/IUpdateEmailDto';
2829
import { IUpdateUserDto } from './user/IUpdateUserDto';
2930
import { PrivateUserDto } from './user/PrivateUserDto';
3031
import { UserDto } from './user/UserDto';
@@ -69,6 +70,7 @@ export {
6970
UserLoginDto,
7071
WarehouseDto,
7172
IResetPasswordDto,
72-
IChangePasswordDto
73+
IChangePasswordDto,
74+
IUpdateEmailDto
7375
};
7476

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
export interface IUpdateEmailDto {
3+
password: string;
4+
email: string;
5+
}

0 commit comments

Comments
 (0)