From 8c6bc29c4d10178431193ef991ce7889147720b2 Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Tue, 18 Jun 2024 10:59:25 +0530 Subject: [PATCH 1/7] refactor(auth): Updated authentication logic --- src/auth/auth.e2e.spec.ts | 29 +------ src/auth/controller/auth.controller.ts | 35 ++------ src/auth/dto/signup.dto.ts | 16 ---- .../{signin.dto.ts => user-details.dto.ts} | 2 +- src/auth/service/auth.service.ts | 86 ++++++++++--------- 5 files changed, 56 insertions(+), 112 deletions(-) delete mode 100644 src/auth/dto/signup.dto.ts rename src/auth/dto/{signin.dto.ts => user-details.dto.ts} (92%) diff --git a/src/auth/auth.e2e.spec.ts b/src/auth/auth.e2e.spec.ts index 901122d..435d451 100644 --- a/src/auth/auth.e2e.spec.ts +++ b/src/auth/auth.e2e.spec.ts @@ -41,28 +41,7 @@ describe('Auth Controller Tests', () => { it('should be able to sign up using email', async () => { const response = await app.inject({ method: 'POST', - url: '/auth/sign-up', - payload: { - email: 'jane@example.com', - }, - }); - - expect(response.statusCode).toEqual(201); - expect(response.json().email).toEqual('jane@example.com'); - }); - - it('should be able to sign in using email', async () => { - await prisma.user.create({ - data: { - email: 'jane@example.com', - isEmailVerified: true, - authType: AuthType.EMAIL, - }, - }); - - const response = await app.inject({ - method: 'POST', - url: '/auth/sign-in', + url: '/auth/send-verification-email', payload: { email: 'jane@example.com', }, @@ -75,10 +54,9 @@ describe('Auth Controller Tests', () => { it('should send verification code to email on sign up', async () => { await app.inject({ method: 'POST', - url: '/auth/sign-up', + url: '/auth/send-verification-email', payload: { email: 'jane@example.com', - password: 'Password123', }, }); @@ -123,10 +101,9 @@ describe('Auth Controller Tests', () => { // Sign up await app.inject({ method: 'POST', - url: '/auth/sign-up', + url: '/auth/send-verification-email', payload: { email: 'jane@example.com', - password: 'Password123', }, }); diff --git a/src/auth/controller/auth.controller.ts b/src/auth/controller/auth.controller.ts index 6c280cb..a150f33 100644 --- a/src/auth/controller/auth.controller.ts +++ b/src/auth/controller/auth.controller.ts @@ -20,12 +20,10 @@ import { Public } from '../../decorators/public.decorator'; import { FacebookOAuthStrategyFactory } from '../../oauth/factory/facebook/facebook-strategy.factory'; import { LinkedInOAuthStrategyFactory } from '../../oauth/factory/linkedin/linkedin-strategy.factory'; import { AppleOAuthStrategyFactory } from '../../oauth/factory/apple/apple-strategy.factory'; -import { SignupDto } from '../dto/signup.dto'; -import { SigninDto } from '../dto/signin.dto'; +import { UserDetailsDto } from '../dto/user-details.dto'; import { EmailVerificationDto } from '../dto/email-verification.dto'; import { ApiBadRequestResponse, - ApiConflictResponse, ApiCreatedResponse, ApiNoContentResponse, ApiNotFoundResponse, @@ -313,29 +311,10 @@ export class AuthController { } @Public() - @Post('sign-up') + @Post('send-verification-email') @ApiOperation({ - summary: 'Sign up', - description: 'Sign up with email', - }) - @ApiCreatedResponse({ - description: 'User signed up successfully', - }) - @ApiConflictResponse({ - description: 'User with this email already exists', - }) - async signUp(@Body() dto: SignupDto) { - return await this.authService.signUp(dto); - } - - @Public() - @Post('sign-in') - @ApiOperation({ - summary: 'Sign in', - description: 'Sign in with email', - }) - @ApiNotFoundResponse({ - description: 'User not found', + summary: 'Sign in or sign up with email', + description: 'Sign in or sign up with email', }) @ApiCreatedResponse({ description: 'User signed in successfully', @@ -347,8 +326,8 @@ export class AuthController { }, }, }) - async signIn(@Body() dto: SigninDto) { - return await this.authService.signIn(dto); + async sendVerificationCode(@Body() dto: UserDetailsDto) { + return await this.authService.sendVerificationCode(dto); } @Public() @@ -396,7 +375,7 @@ export class AuthController { }, }) async verifyEmail(@Body() dto: EmailVerificationDto) { - return await this.authService.verifyEmail(dto.email, dto.code); + return await this.authService.verifyEmail(dto); } @Get('/social-accounts') diff --git a/src/auth/dto/signup.dto.ts b/src/auth/dto/signup.dto.ts deleted file mode 100644 index 0c7e4db..0000000 --- a/src/auth/dto/signup.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail } from 'class-validator'; - -export class SignupDto { - @IsEmail() - @Transform(({ value }) => value.toLowerCase()) - @ApiProperty({ - name: 'email', - description: 'User email. Must be a valid email address.', - required: true, - type: String, - example: 'johndoe@example.com', - }) - email: string; -} diff --git a/src/auth/dto/signin.dto.ts b/src/auth/dto/user-details.dto.ts similarity index 92% rename from src/auth/dto/signin.dto.ts rename to src/auth/dto/user-details.dto.ts index 20d4d6e..096c122 100644 --- a/src/auth/dto/signin.dto.ts +++ b/src/auth/dto/user-details.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail } from 'class-validator'; -export class SigninDto { +export class UserDetailsDto { @IsEmail() @Transform(({ value }) => value.toLowerCase()) @ApiProperty({ diff --git a/src/auth/service/auth.service.ts b/src/auth/service/auth.service.ts index 2f9463f..af1e4da 100644 --- a/src/auth/service/auth.service.ts +++ b/src/auth/service/auth.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, - ConflictException, ForbiddenException, Injectable, NotFoundException, @@ -8,9 +7,9 @@ import { import { PrismaService } from '../../prisma/prisma.service'; import { JwtService } from '@nestjs/jwt'; import { AuthType, SocialAccountType, User } from '@prisma/client'; -import { SignupDto } from '../dto/signup.dto'; +import { UserDetailsDto } from '../dto/user-details.dto'; import { MailService } from '../../mail/mail.service'; -import { SigninDto } from '../dto/signin.dto'; +import { EmailVerificationDto } from '../dto/email-verification.dto'; @Injectable() export class AuthService { @@ -20,37 +19,17 @@ export class AuthService { private mailService: MailService, ) {} - async signUp(dto: SignupDto) { + async sendVerificationCode(dto: UserDetailsDto) { const user = await this.createUserIfNotExists( dto.email, AuthType.EMAIL, null, null, - true, ); return user; } - async signIn(dto: SigninDto) { - const user = await this.prisma.user.findUnique({ - where: { - email: dto.email, - }, - select: { - isEmailVerified: true, - email: true, - }, - }); - - if (!user) { - throw new NotFoundException('User not found'); - } - - await this.sendEmailVerificationCode(dto.email); - return user; - } - async handleGoogleOAuthLogin(req: any) { const { emails, displayName: name, photos } = req.user; const email = emails[0].value; @@ -61,7 +40,6 @@ export class AuthService { AuthType.GOOGLE, name, profilePictureUrl, - false, ); const token = await this.generateToken(user); @@ -98,7 +76,6 @@ export class AuthService { AuthType.FACEBOOK, displayName, profilePictureUrl, - false, ); await this.connectSocialPlatform( SocialAccountType.FACEBOOK, @@ -137,7 +114,6 @@ export class AuthService { AuthType.LINKEDIN, displayName, picture, - false, ); await this.connectSocialPlatform( SocialAccountType.LINKEDIN, @@ -161,7 +137,6 @@ export class AuthService { AuthType.APPLE, displayName, null, - false, ); const token = await this.generateToken(user); @@ -194,7 +169,6 @@ export class AuthService { AuthType.GITHUB, name, avatar_url, - false, ); await this.connectSocialPlatform(SocialAccountType.GITHUB, user.id, req); } @@ -216,7 +190,9 @@ export class AuthService { await this.sendEmailVerificationCode(email); } - async verifyEmail(email: string, code: string) { + async verifyEmail(dto: EmailVerificationDto) { + const { email, code } = dto; + const verificationCode = await this.prisma.verificationCode.findUnique({ where: { email, @@ -235,7 +211,17 @@ export class AuthService { throw new BadRequestException('Code expired'); } - const user = await this.prisma.user.update({ + const user = await this.prisma.user.findUnique({ + where: { + email, + }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + const updatedUser = await this.prisma.user.update({ where: { email, }, @@ -258,12 +244,14 @@ export class AuthService { }, }); - await this.mailService.sendEmailVerifiedEmail(email); + // We send the email verified email only if the user is was not verified before + if (!user.isEmailVerified) + await this.mailService.sendEmailVerifiedEmail(email); - const token = await this.generateToken(user); + const token = await this.generateToken(updatedUser); return { - ...user, + ...updatedUser, token, }; } @@ -273,14 +261,11 @@ export class AuthService { authType: AuthType, name?: string, profilePictureUrl?: string, - throwErrorIfUserExists?: boolean, ) { email = email.toLowerCase(); let user = await this.findUserByEmail(email); - if (user && throwErrorIfUserExists) { - throw new ConflictException('User already exists'); - } + // We need to create the user if it doesn't exist yet if (!user) { user = await this.prisma.user.create({ @@ -289,7 +274,7 @@ export class AuthService { name: name, profilePictureUrl: profilePictureUrl, authType, - isEmailVerified: authType !== AuthType.EMAIL, + isEmailVerified: authType !== AuthType.EMAIL, // If the user signs up with OAuth, we consider the email as verified settings: { create: {}, }, @@ -303,9 +288,28 @@ export class AuthService { isEmailVerified: true, }, }); + } else { + // And if it exists, we need to update the user data + user = await this.prisma.user.update({ + where: { + email, + }, + data: { + name, + profilePictureUrl, + }, + select: { + id: true, + email: true, + name: true, + profilePictureUrl: true, + authType: true, + isEmailVerified: true, + }, + }); + } - await this.sendEmailVerificationCode(email); - } else if (!user.isEmailVerified) { + if (!user.isEmailVerified) { await this.sendEmailVerificationCode(email); } From c8da26f672add496ce7bbaa1fb866e6a286b57dd Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Tue, 18 Jun 2024 18:23:27 +0530 Subject: [PATCH 2/7] add user onboard status --- .vscode/settings.json | 3 +++ src/auth/guard/auth/auth.guard.ts | 13 +++++++++++++ src/decorators/bypass-onboarding.decorator.ts | 11 +++++++++++ .../migration.sql | 2 ++ src/prisma/schema.prisma | 1 + src/user/controller/user.controller.ts | 9 +++++++++ src/user/service/user.service.ts | 12 ++++++++++++ src/user/user.e2e.spec.ts | 1 + 8 files changed, 52 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 src/decorators/bypass-onboarding.decorator.ts create mode 100644 src/prisma/migrations/20240618125311_add_user_onboard_status/migration.sql diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9f84477 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["onboarded"] +} diff --git a/src/auth/guard/auth/auth.guard.ts b/src/auth/guard/auth/auth.guard.ts index fa294ca..aa738d3 100644 --- a/src/auth/guard/auth/auth.guard.ts +++ b/src/auth/guard/auth/auth.guard.ts @@ -3,6 +3,7 @@ import { ExecutionContext, ForbiddenException, Injectable, + UnauthorizedException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { Request } from 'express'; @@ -10,6 +11,7 @@ import { Reflector } from '@nestjs/core'; import { IS_PUBLIC_KEY } from '../../../decorators/public.decorator'; import { PrismaService } from '../../../prisma/prisma.service'; import { User } from '@prisma/client'; +import { ONBOARDING_BYPASSED } from '../../../decorators/bypass-onboarding.decorator'; const X_E2E_USER_EMAIL = 'x-e2e-user-email'; @@ -74,6 +76,17 @@ export class AuthGuard implements CanActivate { } } + const onboardingBypassed = + this.reflector.getAllAndOverride(ONBOARDING_BYPASSED, [ + context.getHandler(), + context.getClass(), + ]) ?? false; + + // If the onboarding is not finished, we throw an UnauthorizedException. + if (!onboardingBypassed && !user.onboarded) { + throw new UnauthorizedException('Onboarding not finished'); + } + // We attach the user to the request object. request['user'] = user; return true; diff --git a/src/decorators/bypass-onboarding.decorator.ts b/src/decorators/bypass-onboarding.decorator.ts new file mode 100644 index 0000000..7818a18 --- /dev/null +++ b/src/decorators/bypass-onboarding.decorator.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from '@nestjs/common'; + +/** + * There are some routes that we want the users to be able to access + * even before they are done with the onboarding process. This decorator + * is used to mark those routes. + */ + +export const ONBOARDING_BYPASSED = 'onboarding_bypassed'; +export const BypassOnboardingCheck = () => + SetMetadata(ONBOARDING_BYPASSED, true); diff --git a/src/prisma/migrations/20240618125311_add_user_onboard_status/migration.sql b/src/prisma/migrations/20240618125311_add_user_onboard_status/migration.sql new file mode 100644 index 0000000..1675ee8 --- /dev/null +++ b/src/prisma/migrations/20240618125311_add_user_onboard_status/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "onboarded" BOOLEAN NOT NULL DEFAULT false; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index b9d135a..44beaec 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -30,6 +30,7 @@ model User { email String? @unique name String? location String? + onboarded Boolean @default(false) profilePictureUrl String? socialAccounts SocialAccount[] authType AuthType diff --git a/src/user/controller/user.controller.ts b/src/user/controller/user.controller.ts index 95f5b2a..f011993 100644 --- a/src/user/controller/user.controller.ts +++ b/src/user/controller/user.controller.ts @@ -4,6 +4,7 @@ import { Controller, Delete, Get, + Patch, Put, Req, Res, @@ -29,6 +30,7 @@ import { LinkedInOAuthStrategyFactory } from '../../oauth/factory/linkedin/linke import { AuthGuard } from '@nestjs/passport'; import { UpdateUserSettingsDto } from '../dto/update-user-settings.dto'; import { UserSettingsDto } from '../dto/user-settings.dto'; +import { BypassOnboardingCheck } from '../../decorators/bypass-onboarding.decorator'; @Controller('user') @ApiBearerAuth() @@ -40,6 +42,7 @@ export class UserController { ) {} @Get() + @BypassOnboardingCheck() @ApiOperation({ summary: 'Get current user', description: 'Get the currently logged in user', @@ -143,6 +146,12 @@ export class UserController { return this.userService.updateSettings(user.id, data); } + @Patch('onboard') + @BypassOnboardingCheck() + async onboardUser(@CurrentUser() user: User) { + return this.userService.onboardUser(user); + } + /** * Delete the entire user account */ diff --git a/src/user/service/user.service.ts b/src/user/service/user.service.ts index b267090..7fc7817 100644 --- a/src/user/service/user.service.ts +++ b/src/user/service/user.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ConflictException, Inject, Injectable, InternalServerErrorException, @@ -138,6 +139,17 @@ export class UserService { } } + async onboardUser(user: User) { + if (user.onboarded) { + throw new ConflictException('User has already been onboarded'); + } + + await this.prisma.user.update({ + where: { id: user.id }, + data: { onboarded: true }, + }); + } + updateSettings(id: string, data: UpdateUserSettingsDto) { return this.prisma.userSettings.update({ where: { diff --git a/src/user/user.e2e.spec.ts b/src/user/user.e2e.spec.ts index f221814..5f6f5b9 100644 --- a/src/user/user.e2e.spec.ts +++ b/src/user/user.e2e.spec.ts @@ -44,6 +44,7 @@ describe('User Controller Tests', () => { name: 'John Doe', isEmailVerified: true, authType: AuthType.EMAIL, + onboarded: true, }, }); }); From f68921857dd7148180ba74a6359d41289dffea74 Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Tue, 18 Jun 2024 18:28:25 +0530 Subject: [PATCH 3/7] fix failing test --- src/reviews/reviews.e2e.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/reviews/reviews.e2e.spec.ts b/src/reviews/reviews.e2e.spec.ts index ea26288..a724e53 100644 --- a/src/reviews/reviews.e2e.spec.ts +++ b/src/reviews/reviews.e2e.spec.ts @@ -51,6 +51,7 @@ describe('Reviws Controller Tests', () => { email: 'johndoe@example.com', name: 'John Doe', isEmailVerified: true, + onboarded: true, authType: AuthType.EMAIL, }, }); From 649d1a1017628ec4b5f812e429e777a941eeecb4 Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Tue, 18 Jun 2024 18:30:14 +0530 Subject: [PATCH 4/7] add tests --- src/user/user.e2e.spec.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/user/user.e2e.spec.ts b/src/user/user.e2e.spec.ts index 5f6f5b9..aa8ae29 100644 --- a/src/user/user.e2e.spec.ts +++ b/src/user/user.e2e.spec.ts @@ -54,6 +54,30 @@ describe('User Controller Tests', () => { expect(prisma).toBeDefined(); }); + it('should not be able to update itself if onboarding is not finished', async () => { + await prisma.user.update({ + where: { + email: 'johndoe@example.com', + }, + data: { + onboarded: false, + }, + }); + + const response = await app.inject({ + method: 'PUT', + url: '/user', + headers: { + 'x-e2e-user-email': 'johndoe@example.com', + }, + payload: { + name: 'Jane Doe', + }, + }); + + expect(response.statusCode).toBe(401); + }); + it('should be able to get the current user', async () => { const response = await app.inject({ method: 'GET', From 3994c6523a1637d54cb13f875c96101a66bc51ec Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Mon, 24 Jun 2024 20:08:22 +0530 Subject: [PATCH 5/7] Implemented PR suggestions --- src/auth/controller/auth.controller.ts | 4 ++- src/auth/service/auth.service.ts | 42 ++++++-------------------- src/user/controller/user.controller.ts | 2 ++ src/user/dto/user-response.dto.ts | 14 +++++++++ 4 files changed, 28 insertions(+), 34 deletions(-) create mode 100644 src/user/dto/user-response.dto.ts diff --git a/src/auth/controller/auth.controller.ts b/src/auth/controller/auth.controller.ts index a150f33..fb2bff2 100644 --- a/src/auth/controller/auth.controller.ts +++ b/src/auth/controller/auth.controller.ts @@ -35,6 +35,7 @@ import { LowercasePipe } from '../../common/pipes/lowercase.pipe'; import { GithubOAuthStrategyFactory } from '../../oauth/factory/github/github-strategy.factory'; import { CurrentUser } from '../../decorators/current-user.decorator'; import { SocialAccountType, User } from '@prisma/client'; +import { BypassOnboardingCheck } from '../../decorators/bypass-onboarding.decorator'; @Controller('auth') @ApiTags('Auth Controller') @@ -311,7 +312,7 @@ export class AuthController { } @Public() - @Post('send-verification-email') + @Post('email') @ApiOperation({ summary: 'Sign in or sign up with email', description: 'Sign in or sign up with email', @@ -378,6 +379,7 @@ export class AuthController { return await this.authService.verifyEmail(dto); } + @BypassOnboardingCheck() @Get('/social-accounts') async getSocialAccounts(@CurrentUser() user: User) { return this.authService.getSocialAccounts(user.id); diff --git a/src/auth/service/auth.service.ts b/src/auth/service/auth.service.ts index af1e4da..f4cd8b9 100644 --- a/src/auth/service/auth.service.ts +++ b/src/auth/service/auth.service.ts @@ -10,6 +10,8 @@ import { AuthType, SocialAccountType, User } from '@prisma/client'; import { UserDetailsDto } from '../dto/user-details.dto'; import { MailService } from '../../mail/mail.service'; import { EmailVerificationDto } from '../dto/email-verification.dto'; +import { plainToClass } from 'class-transformer'; +import UserResponseDto from '../../user/dto/user-response.dto'; @Injectable() export class AuthService { @@ -20,14 +22,11 @@ export class AuthService { ) {} async sendVerificationCode(dto: UserDetailsDto) { - const user = await this.createUserIfNotExists( - dto.email, - AuthType.EMAIL, - null, - null, - ); + await this.createUserIfNotExists(dto.email, AuthType.EMAIL, null, null); - return user; + return { + status: 'success', + }; } async handleGoogleOAuthLogin(req: any) { @@ -279,17 +278,10 @@ export class AuthService { create: {}, }, }, - select: { - id: true, - email: true, - name: true, - profilePictureUrl: true, - authType: true, - isEmailVerified: true, - }, }); - } else { + } else if (!user.onboarded) { // And if it exists, we need to update the user data + // only if the user is not onboarded yet user = await this.prisma.user.update({ where: { email, @@ -298,14 +290,6 @@ export class AuthService { name, profilePictureUrl, }, - select: { - id: true, - email: true, - name: true, - profilePictureUrl: true, - authType: true, - isEmailVerified: true, - }, }); } @@ -313,7 +297,7 @@ export class AuthService { await this.sendEmailVerificationCode(email); } - return user; + return plainToClass(UserResponseDto, user); } private async generateToken(user: Partial) { @@ -331,14 +315,6 @@ export class AuthService { where: { email, }, - select: { - id: true, - email: true, - name: true, - profilePictureUrl: true, - authType: true, - isEmailVerified: true, - }, }); } diff --git a/src/user/controller/user.controller.ts b/src/user/controller/user.controller.ts index f011993..3e22156 100644 --- a/src/user/controller/user.controller.ts +++ b/src/user/controller/user.controller.ts @@ -58,6 +58,7 @@ export class UserController { return this.userService.getSelf(user); } + @BypassOnboardingCheck() @Put() @ApiOperation({ summary: 'Update current user', @@ -77,6 +78,7 @@ export class UserController { return this.userService.updateSelf(user, dto); } + @BypassOnboardingCheck() @Put('/profile-picture') @ApiOperation({ summary: 'Upload profile picture encoded in base64', diff --git a/src/user/dto/user-response.dto.ts b/src/user/dto/user-response.dto.ts new file mode 100644 index 0000000..fe71586 --- /dev/null +++ b/src/user/dto/user-response.dto.ts @@ -0,0 +1,14 @@ +import { AuthType } from '@prisma/client'; +import { Exclude } from 'class-transformer'; + +@Exclude() +export default class UserResponseDto { + email: string; + name: string; + location: string; + onboarded: boolean; + profilePictureUrl: string; + authType: AuthType; + isEmailVerified: boolean; + headline: string; +} From ca6b4118dbec88975eef4b59aa300f9194c2d10b Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Mon, 24 Jun 2024 20:10:41 +0530 Subject: [PATCH 6/7] Updated user service class --- src/user/service/user.service.ts | 47 ++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/user/service/user.service.ts b/src/user/service/user.service.ts index 7fc7817..9fdeb29 100644 --- a/src/user/service/user.service.ts +++ b/src/user/service/user.service.ts @@ -16,6 +16,8 @@ import { Redis } from 'ioredis'; import { v4 } from 'uuid'; import { getMimeType } from '../../utils/image'; import { UpdateUserSettingsDto } from '../dto/update-user-settings.dto'; +import { plainToClass } from 'class-transformer'; +import UserResponseDto from '../dto/user-response.dto'; @Injectable() export class UserService { @@ -32,14 +34,17 @@ export class UserService { } async updateSelf(user: User, dto: UpdateUserDto) { - return await this.prisma.user.update({ - where: { id: user.id }, - data: { - name: dto.name, - headline: dto.headline, - location: dto.location, - }, - }); + return plainToClass( + UserResponseDto, + await this.prisma.user.update({ + where: { id: user.id }, + data: { + name: dto.name, + headline: dto.headline, + location: dto.location, + }, + }), + ); } async updateProfilePicture(user: User, file: string) { @@ -66,12 +71,15 @@ export class UserService { await this.s3.send(putObjectRequest); this.logger.log('Profile picture uploaded'); - return await this.prisma.user.update({ - where: { id: user.id }, - data: { - profilePictureUrl: `https://${process.env.AWS_S3_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`, - }, - }); + return plainToClass( + UserResponseDto, + await this.prisma.user.update({ + where: { id: user.id }, + data: { + profilePictureUrl: `https://${process.env.AWS_S3_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`, + }, + }), + ); } catch (err) { throw new InternalServerErrorException( 'Failed to upload profile picture', @@ -144,10 +152,13 @@ export class UserService { throw new ConflictException('User has already been onboarded'); } - await this.prisma.user.update({ - where: { id: user.id }, - data: { onboarded: true }, - }); + return plainToClass( + UserResponseDto, + await this.prisma.user.update({ + where: { id: user.id }, + data: { onboarded: true }, + }), + ); } updateSettings(id: string, data: UpdateUserSettingsDto) { From b43e34eaed5620f9e1b40429a39e0deebf582b96 Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Mon, 24 Jun 2024 20:24:20 +0530 Subject: [PATCH 7/7] Fixed breaking dto --- src/auth/auth.e2e.spec.ts | 10 ++++++---- src/auth/service/auth.service.ts | 27 +++++++++++---------------- src/user/dto/user-response.dto.ts | 20 +++++++++++++++++++- src/user/service/user.service.ts | 2 +- src/user/user.e2e.spec.ts | 11 +++++++++-- 5 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/auth/auth.e2e.spec.ts b/src/auth/auth.e2e.spec.ts index 435d451..eb9fade 100644 --- a/src/auth/auth.e2e.spec.ts +++ b/src/auth/auth.e2e.spec.ts @@ -41,20 +41,22 @@ describe('Auth Controller Tests', () => { it('should be able to sign up using email', async () => { const response = await app.inject({ method: 'POST', - url: '/auth/send-verification-email', + url: '/auth/email', payload: { email: 'jane@example.com', }, }); expect(response.statusCode).toEqual(201); - expect(response.json().email).toEqual('jane@example.com'); + expect(response.json()).toEqual({ + status: 'success', + }); }); it('should send verification code to email on sign up', async () => { await app.inject({ method: 'POST', - url: '/auth/send-verification-email', + url: '/auth/email', payload: { email: 'jane@example.com', }, @@ -101,7 +103,7 @@ describe('Auth Controller Tests', () => { // Sign up await app.inject({ method: 'POST', - url: '/auth/send-verification-email', + url: '/auth/email', payload: { email: 'jane@example.com', }, diff --git a/src/auth/service/auth.service.ts b/src/auth/service/auth.service.ts index f4cd8b9..0149465 100644 --- a/src/auth/service/auth.service.ts +++ b/src/auth/service/auth.service.ts @@ -220,22 +220,17 @@ export class AuthService { throw new NotFoundException('User not found'); } - const updatedUser = await this.prisma.user.update({ - where: { - email, - }, - data: { - isEmailVerified: true, - }, - select: { - id: true, - email: true, - name: true, - profilePictureUrl: true, - authType: true, - isEmailVerified: true, - }, - }); + const updatedUser = plainToClass( + UserResponseDto, + await this.prisma.user.update({ + where: { + email, + }, + data: { + isEmailVerified: true, + }, + }), + ); await this.prisma.verificationCode.delete({ where: { diff --git a/src/user/dto/user-response.dto.ts b/src/user/dto/user-response.dto.ts index fe71586..3b98c59 100644 --- a/src/user/dto/user-response.dto.ts +++ b/src/user/dto/user-response.dto.ts @@ -1,14 +1,32 @@ import { AuthType } from '@prisma/client'; -import { Exclude } from 'class-transformer'; +import { Exclude, Expose } from 'class-transformer'; @Exclude() export default class UserResponseDto { + @Expose() + id: string; + + @Expose() email: string; + + @Expose() name: string; + + @Expose() location: string; + + @Expose() onboarded: boolean; + + @Expose() profilePictureUrl: string; + + @Expose() authType: AuthType; + + @Expose() isEmailVerified: boolean; + + @Expose() headline: string; } diff --git a/src/user/service/user.service.ts b/src/user/service/user.service.ts index 9fdeb29..84f858d 100644 --- a/src/user/service/user.service.ts +++ b/src/user/service/user.service.ts @@ -30,7 +30,7 @@ export class UserService { ) {} async getSelf(user: User) { - return user; + return plainToClass(UserResponseDto, user); } async updateSelf(user: User, dto: UpdateUserDto) { diff --git a/src/user/user.e2e.spec.ts b/src/user/user.e2e.spec.ts index aa8ae29..b294fc4 100644 --- a/src/user/user.e2e.spec.ts +++ b/src/user/user.e2e.spec.ts @@ -54,7 +54,7 @@ describe('User Controller Tests', () => { expect(prisma).toBeDefined(); }); - it('should not be able to update itself if onboarding is not finished', async () => { + it('should be able to update itself if onboarding is not finished', async () => { await prisma.user.update({ where: { email: 'johndoe@example.com', @@ -75,7 +75,14 @@ describe('User Controller Tests', () => { }, }); - expect(response.statusCode).toBe(401); + const updatedUser = await prisma.user.findUnique({ + where: { + email: 'johndoe@example.com', + }, + }); + expect(updatedUser.name).toBe('Jane Doe'); + + expect(response.statusCode).toBe(200); }); it('should be able to get the current user', async () => {