Skip to content

Commit f1031b9

Browse files
committed
feat: endpoint for updating users profile details
1 parent 75d568f commit f1031b9

11 files changed

+117
-48
lines changed

src/auth/auth.service.ts

+38
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,44 @@ export class AuthService {
7373
}
7474
}
7575

76+
public async updateFirebaseUserEmail(firebaseUid: string, newEmail: string) {
77+
try {
78+
const firebaseUser = await this.firebase.admin.auth().updateUser(firebaseUid, {
79+
email: newEmail,
80+
});
81+
return firebaseUser;
82+
} catch (err) {
83+
const errorCode = err.code;
84+
85+
if (errorCode === 'auth/invalid-email') {
86+
this.logger.warn({
87+
error: FIREBASE_ERRORS.UPDATE_USER_INVALID_EMAIL,
88+
status: HttpStatus.BAD_REQUEST,
89+
});
90+
throw new HttpException(FIREBASE_ERRORS.UPDATE_USER_INVALID_EMAIL, HttpStatus.BAD_REQUEST);
91+
} else if (
92+
errorCode === 'auth/email-already-in-use' ||
93+
errorCode === 'auth/email-already-exists'
94+
) {
95+
this.logger.warn({
96+
error: FIREBASE_ERRORS.UPDATE_USER_ALREADY_EXISTS,
97+
status: HttpStatus.BAD_REQUEST,
98+
});
99+
throw new HttpException(FIREBASE_ERRORS.UPDATE_USER_ALREADY_EXISTS, HttpStatus.BAD_REQUEST);
100+
} else {
101+
this.logger.warn({
102+
error: FIREBASE_ERRORS.UPDATE_USER_FIREBASE_ERROR,
103+
errorMessage: errorCode,
104+
status: HttpStatus.INTERNAL_SERVER_ERROR,
105+
});
106+
throw new HttpException(
107+
FIREBASE_ERRORS.CREATE_USER_FIREBASE_ERROR,
108+
HttpStatus.INTERNAL_SERVER_ERROR,
109+
);
110+
}
111+
}
112+
}
113+
76114
public async getFirebaseUser(email: string) {
77115
const firebaseUser = await this.firebase.admin.auth().getUserByEmail(email);
78116
return firebaseUser;

src/firebase/firebase-auth.guard.ts

+6-11
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,8 @@ import {
88
UnauthorizedException,
99
} from '@nestjs/common';
1010
import { Request } from 'express';
11-
import {
12-
AUTH_GUARD_MISSING_HEADER,
13-
AUTH_GUARD_PARSING_ERROR,
14-
AUTH_GUARD_TOKEN_EXPIRED,
15-
AUTH_GUARD_USER_NOT_FOUND,
16-
} from 'src/logger/constants';
17-
import { FIREBASE_ERRORS } from 'src/utils/errors';
11+
12+
import { AUTH_GUARD_ERRORS, FIREBASE_ERRORS } from 'src/utils/errors';
1813
import { AuthService } from '../auth/auth.service';
1914
import { UserService } from '../user/user.service';
2015
import { IFirebaseUser } from './firebase-user.interface';
@@ -35,7 +30,7 @@ export class FirebaseAuthGuard implements CanActivate {
3530

3631
if (!authorization) {
3732
this.logger.warn({
38-
error: AUTH_GUARD_MISSING_HEADER,
33+
error: AUTH_GUARD_ERRORS.MISSING_HEADER,
3934
errorMessage: `FirebaseAuthGuard: Authorisation failed for ${request.originalUrl}`,
4035
});
4136
throw new UnauthorizedException('Unauthorized: missing required Authorization token');
@@ -47,14 +42,14 @@ export class FirebaseAuthGuard implements CanActivate {
4742
} catch (error) {
4843
if (error.code === 'auth/id-token-expired') {
4944
this.logger.warn({
50-
error: AUTH_GUARD_TOKEN_EXPIRED,
45+
error: AUTH_GUARD_ERRORS.TOKEN_EXPIRED,
5146
errorMessage: `FireabaseAuthGuard: Authorisation failed for ${request.originalUrl}`,
5247
status: HttpStatus.UNAUTHORIZED,
5348
});
5449
throw new HttpException(FIREBASE_ERRORS.ID_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED);
5550
}
5651
this.logger.warn({
57-
error: AUTH_GUARD_PARSING_ERROR,
52+
error: AUTH_GUARD_ERRORS.PARSING_ERROR,
5853
errorMessage: `FirebaseAuthGuard: Authorisation failed for ${request.originalUrl}`,
5954
status: HttpStatus.INTERNAL_SERVER_ERROR,
6055
});
@@ -74,7 +69,7 @@ export class FirebaseAuthGuard implements CanActivate {
7469
} catch (error) {
7570
if (error.message === 'USER NOT FOUND') {
7671
this.logger.warn({
77-
error: AUTH_GUARD_USER_NOT_FOUND,
72+
error: AUTH_GUARD_ERRORS.USER_NOT_FOUND,
7873
errorMessage: `FirebaseAuthGuard: Authorisation failed for ${request.originalUrl}`,
7974
status: HttpStatus.INTERNAL_SERVER_ERROR,
8075
});

src/logger/constants.ts

-9
This file was deleted.

src/partner-admin/partner-admin-auth.guard.ts

+5-10
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,7 @@ import {
99
import { InjectRepository } from '@nestjs/typeorm';
1010
import { Request } from 'express';
1111
import { UserEntity } from 'src/entities/user.entity';
12-
import {
13-
AUTH_GUARD_MISSING_HEADER,
14-
AUTH_GUARD_PARSING_ERROR,
15-
AUTH_GUARD_TOKEN_EXPIRED,
16-
} from 'src/logger/constants';
17-
import { FIREBASE_ERRORS } from 'src/utils/errors';
12+
import { AUTH_GUARD_ERRORS } from 'src/utils/errors';
1813
import { Repository } from 'typeorm';
1914
import { AuthService } from '../auth/auth.service';
2015

@@ -32,7 +27,7 @@ export class PartnerAdminAuthGuard implements CanActivate {
3227
const { authorization } = request.headers;
3328
if (!authorization) {
3429
this.logger.warn({
35-
error: AUTH_GUARD_MISSING_HEADER,
30+
error: AUTH_GUARD_ERRORS.MISSING_HEADER,
3631
errorMessage: `PartnerAdminAuthGuard: Authorisation failed for ${request.originalUrl}`,
3732
});
3833
return false;
@@ -45,14 +40,14 @@ export class PartnerAdminAuthGuard implements CanActivate {
4540
} catch (error) {
4641
if (error.code === 'auth/id-token-expired') {
4742
this.logger.warn({
48-
error: AUTH_GUARD_TOKEN_EXPIRED,
43+
error: AUTH_GUARD_ERRORS.TOKEN_EXPIRED,
4944
errorMessage: `PartnerAdminAuthGuard: Authorisation failed for ${request.originalUrl}`,
5045
});
51-
throw new HttpException(FIREBASE_ERRORS.ID_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED);
46+
throw new HttpException(AUTH_GUARD_ERRORS.TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED);
5247
}
5348

5449
this.logger.warn({
55-
error: AUTH_GUARD_PARSING_ERROR,
50+
error: AUTH_GUARD_ERRORS.PARSING_ERROR,
5651
errorMessage: `PartnerAdminAuthGuard: Authorisation failed for ${request.originalUrl}`,
5752
});
5853

src/partner-admin/super-admin-auth.guard.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import {
99
import { InjectRepository } from '@nestjs/typeorm';
1010
import { Request } from 'express';
1111
import { UserEntity } from 'src/entities/user.entity';
12-
import { AUTH_GUARD_PARSING_ERROR, AUTH_GUARD_TOKEN_EXPIRED } from 'src/logger/constants';
13-
import { FIREBASE_ERRORS } from 'src/utils/errors';
12+
import { AUTH_GUARD_ERRORS, FIREBASE_ERRORS } from 'src/utils/errors';
1413
import { Repository } from 'typeorm';
1514
import { AuthService } from '../auth/auth.service';
1615

@@ -41,14 +40,16 @@ export class SuperAdminAuthGuard implements CanActivate {
4140
} catch (error) {
4241
if (error.code === 'auth/id-token-expired') {
4342
this.logger.warn({
44-
error: AUTH_GUARD_TOKEN_EXPIRED,
43+
error: AUTH_GUARD_ERRORS.TOKEN_EXPIRED,
4544
errorMessage: `Authorisation failed for ${request.originalUrl}`,
45+
status: HttpStatus.UNAUTHORIZED,
4646
});
4747
throw new HttpException(FIREBASE_ERRORS.ID_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED);
4848
}
4949
this.logger.warn({
50-
error: AUTH_GUARD_PARSING_ERROR,
50+
error: AUTH_GUARD_ERRORS.PARSING_ERROR,
5151
errorMessage: `Authorisation failed for ${request.originalUrl}`,
52+
status: HttpStatus.INTERNAL_SERVER_ERROR,
5253
});
5354
throw new HttpException(
5455
`SuperAdminAuthGuard - Error parsing firebase user: ${error}`,

src/user/dtos/update-user.dto.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ApiProperty } from '@nestjs/swagger';
2-
import { IsBoolean, IsDate, IsOptional, IsString } from 'class-validator';
2+
import { IsBoolean, IsDate, IsEmail, IsOptional, IsString } from 'class-validator';
33
import { EMAIL_REMINDERS_FREQUENCY } from '../../utils/constants';
44

55
export class UpdateUserDto {
@@ -32,4 +32,9 @@ export class UpdateUserDto {
3232
@IsOptional()
3333
@ApiProperty({ type: 'date' })
3434
lastActiveAt: Date;
35+
36+
@IsEmail({})
37+
@IsOptional()
38+
@ApiProperty({ type: 'email' })
39+
email: string;
3540
}

src/user/user.controller.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class UserController {
4646
@UseGuards(FirebaseAuthGuard)
4747
async getUserByFirebaseId(@Req() req: Request): Promise<GetUserDto> {
4848
const user = req['user'];
49-
this.userService.updateUser({ lastActiveAt: new Date() }, user);
49+
this.userService.updateUser({ lastActiveAt: new Date() }, user.id);
5050
return user;
5151
}
5252

@@ -100,7 +100,14 @@ export class UserController {
100100
@Patch()
101101
@UseGuards(FirebaseAuthGuard)
102102
async updateUser(@Body() updateUserDto: UpdateUserDto, @Req() req: Request) {
103-
return await this.userService.updateUser(updateUserDto, req['user'] as GetUserDto);
103+
return await this.userService.updateUser(updateUserDto, req['user'].user.id);
104+
}
105+
106+
@ApiBearerAuth()
107+
@Patch('/admin/:id')
108+
@UseGuards(SuperAdminAuthGuard)
109+
async adminUpdateUser(@Param() { id }, @Body() updateUserDto: UpdateUserDto) {
110+
return await this.userService.updateUser(updateUserDto, id);
104111
}
105112

106113
@ApiBearerAuth()

src/user/user.service.spec.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const updateUserDto: Partial<UpdateUserDto> = {
4848
contactPermission: true,
4949
serviceEmailsPermission: false,
5050
signUpLanguage: 'en',
51+
5152
};
5253

5354
const mockSubscriptionUserServiceMethods = {};
@@ -279,26 +280,31 @@ describe('UserService', () => {
279280
describe('updateUser', () => {
280281
it('when supplied a firebase user dto, it should return a user', async () => {
281282
const repoSaveSpy = jest.spyOn(repo, 'save');
283+
const authServiceUpdateEmailSpy = jest.spyOn(mockAuthService, 'updateFirebaseUserEmail');
282284

283-
const user = await service.updateUser(updateUserDto, { user: mockUserEntity });
284-
expect(user.name).toBe('new name');
285-
expect(user.email).toBe('[email protected]');
285+
const user = await service.updateUser(updateUserDto, mockUserEntity.id);
286+
expect(user.name).toBe(updateUserDto.name);
287+
expect(user.email).toBe(updateUserDto.email);
286288
expect(user.contactPermission).toBe(true);
287289
expect(user.serviceEmailsPermission).toBe(false);
288290

289291
expect(repoSaveSpy).toHaveBeenCalledWith({ ...mockUserEntity, ...updateUserDto });
290292
expect(repoSaveSpy).toHaveBeenCalled();
293+
expect(authServiceUpdateEmailSpy).toHaveBeenCalledWith(
294+
mockUserEntity.firebaseUid,
295+
updateUserDto.email,
296+
);
291297
});
292298

293299
it('should not fail update on crisp api call errors', async () => {
294300
const mocked = jest.mocked(updateCrispProfile);
295301
mocked.mockRejectedValue(new Error('Crisp API call failed'));
296302

297-
const user = await service.updateUser(updateUserDto, { user: mockUserEntity });
303+
const user = await service.updateUser(updateUserDto, mockUserEntity.id);
298304
await new Promise(process.nextTick); // wait for async funcs to resolve
299305
expect(mocked).toHaveBeenCalled();
300-
expect(user.name).toBe('new name');
301-
expect(user.email).toBe('[email protected]');
306+
expect(user.name).toBe(updateUserDto.name);
307+
expect(user.email).toBe(updateUserDto.email);
302308

303309
mocked.mockReset();
304310
});
@@ -307,11 +313,11 @@ describe('UserService', () => {
307313
const mocked = jest.mocked(updateMailchimpProfile);
308314
mocked.mockRejectedValue(new Error('Mailchimp API call failed'));
309315

310-
const user = await service.updateUser(updateUserDto, { user: mockUserEntity });
316+
const user = await service.updateUser(updateUserDto, mockUserEntity.id);
311317
await new Promise(process.nextTick); // wait for async funcs to resolve
312318
expect(mocked).toHaveBeenCalled();
313-
expect(user.name).toBe('new name');
314-
expect(user.email).toBe('[email protected]');
319+
expect(user.name).toBe(updateUserDto.name);
320+
expect(user.email).toBe(updateUserDto.email);
315321

316322
mocked.mockReset();
317323
});

src/user/user.service.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SubscriptionUserService } from 'src/subscription-user/subscription-user
1010
import { TherapySessionService } from 'src/therapy-session/therapy-session.service';
1111
import { SIGNUP_TYPE } from 'src/utils/constants';
1212
import { FIREBASE_ERRORS } from 'src/utils/errors';
13+
import { FIREBASE_EVENTS } from 'src/utils/logs';
1314
import {
1415
createServiceUserProfiles,
1516
updateServiceUserProfilesUser,
@@ -191,12 +192,27 @@ export class UserService {
191192
return await this.deleteUser(user);
192193
}
193194

194-
public async updateUser(updateUserDto: Partial<UpdateUserDto>, { user: { id } }: GetUserDto) {
195-
const user = await this.userRepository.findOneBy({ id });
195+
public async updateUser(updateUserDto: Partial<UpdateUserDto>, userId: string) {
196+
const user = await this.userRepository.findOneBy({ id: userId });
196197

197198
if (!user) {
198199
throw new HttpException('USER NOT FOUND', HttpStatus.NOT_FOUND);
199200
}
201+
202+
if (updateUserDto.email) {
203+
// check whether email has been updated already in firebase
204+
const firebaseUser = await this.authService.getFirebaseUser(user.email);
205+
if (firebaseUser.email !== updateUserDto.email) {
206+
await this.authService.updateFirebaseUserEmail(user.firebaseUid, updateUserDto.email);
207+
this.logger.log({ event: FIREBASE_EVENTS.UPDATE_FIREBASE_USER_EMAIL, userId: user.id });
208+
} else {
209+
this.logger.log({
210+
event: FIREBASE_EVENTS.UPDATE_FIREBASE_EMAIL_ALREADY_UPDATED,
211+
userId: user.id,
212+
});
213+
}
214+
}
215+
200216
const newUserData: UserEntity = {
201217
...user,
202218
...updateUserDto,

src/utils/errors.ts

+11
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,15 @@ export enum FIREBASE_ERRORS {
77
CREATE_USER_WEAK_PASSWORD = 'CREATE_USER_WEAK_PASSWORD',
88
CREATE_USER_ALREADY_EXISTS = 'CREATE_USER_ALREADY_EXISTS',
99
ID_TOKEN_EXPIRED = 'ID_TOKEN_EXPIRED',
10+
UPDATE_USER_INVALID_EMAIL = 'UPDATE_USER_INVALID_EMAIL',
11+
UPDATE_USER_WEAK_PASSWORD = 'UPDATE_USER_WEAK_PASSWORD',
12+
UPDATE_USER_ALREADY_EXISTS = 'UPDATE_USER_ALREADY_EXISTS',
13+
UPDATE_USER_FIREBASE_ERROR = 'UPDATE_USER_FIREBASE_ERROR',
14+
}
15+
16+
export enum AUTH_GUARD_ERRORS {
17+
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
18+
PARSING_ERROR = 'PARSING_ERROR',
19+
MISSING_HEADER = 'MISSING_HEADER',
20+
USER_NOT_FOUND = 'USER_NOT_FOUND',
1021
}

src/utils/logs.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum FIREBASE_EVENTS {
2+
UPDATE_FIREBASE_USER_EMAIL = 'UPDATE_FIREBASE_USER_EMAIL',
3+
UPDATE_FIREBASE_EMAIL_ALREADY_UPDATED = 'UPDATE_FIREBASE_EMAIL_ALREADY_UPDATED',
4+
}

0 commit comments

Comments
 (0)