From aa8deadf1fc99da80213d1c7531a39906c6924da Mon Sep 17 00:00:00 2001 From: SauravDhakal Date: Sat, 11 Apr 2026 07:57:28 +0545 Subject: [PATCH] feat: Change auth flow --- common/keys.ts | 1 + package.json | 1 + .../migration.sql | 35 ++++ .../migration.sql | 20 +++ prisma/models/user.prisma | 56 ++++-- src/auth/auth.controller.ts | 39 +++- src/auth/auth.module.ts | 2 + src/auth/auth.service.ts | 170 +++++++++++++++--- src/auth/decorators/index.ts | 1 + src/auth/decorators/isTemp.decorator.ts | 4 + src/auth/dto/complete-setup.dto.ts | 42 +++++ src/auth/dto/index.ts | 2 + src/auth/dto/register-user.dto.ts | 41 ----- src/auth/dto/validate-otp.dto.ts | 22 +++ src/auth/guards/auth.guard.ts | 19 +- src/auth/types/jwt.ts | 3 +- src/auth/types/token.ts | 11 +- .../organization-membership.service.ts | 15 +- src/prisma/prisma.service.ts | 7 +- src/user/dtos/user.dto.ts | 12 +- src/user/user.service.ts | 62 +++++-- 21 files changed, 442 insertions(+), 123 deletions(-) create mode 100644 prisma/migrations/20260405132201_update_user_model_and_added_additional_info/migration.sql create mode 100644 prisma/migrations/20260405145322_added_status_and_some_changes/migration.sql create mode 100644 src/auth/decorators/isTemp.decorator.ts create mode 100644 src/auth/dto/complete-setup.dto.ts create mode 100644 src/auth/dto/validate-otp.dto.ts diff --git a/common/keys.ts b/common/keys.ts index 0546e23..5752227 100644 --- a/common/keys.ts +++ b/common/keys.ts @@ -3,3 +3,4 @@ export const ROLE_KEY = '__ROLE_KEY__'; export const ORG_ROLE_KEY = '__ORG_ROLE_KEY__' export const ORG_ROLES_ALL_KEY = '__ORG_ROLE_ALL_KEY__'; export const CAN_PERFORM_KEY = '__CAN_PERFORM_KEY__'; +export const TEMP_TOKEN_KEY = '__TEMP_TOKEN_KEY__'; diff --git a/package.json b/package.json index adc20c2..2ece958 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", + "dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", diff --git a/prisma/migrations/20260405132201_update_user_model_and_added_additional_info/migration.sql b/prisma/migrations/20260405132201_update_user_model_and_added_additional_info/migration.sql new file mode 100644 index 0000000..354e56d --- /dev/null +++ b/prisma/migrations/20260405132201_update_user_model_and_added_additional_info/migration.sql @@ -0,0 +1,35 @@ +/* + Warnings: + + - You are about to drop the column `firstName` on the `user` table. All the data in the column will be lost. + - You are about to drop the column `lastName` on the `user` table. All the data in the column will be lost. + - You are about to drop the column `middleName` on the `user` table. All the data in the column will be lost. + - You are about to drop the column `profilePicture` on the `user` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "user" DROP COLUMN "firstName", +DROP COLUMN "lastName", +DROP COLUMN "middleName", +DROP COLUMN "profilePicture"; + +-- CreateTable +CREATE TABLE "UserAdditionalInformation" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "middleName" TEXT, + "lastName" TEXT NOT NULL, + "profilePicture" TEXT, + "address" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UserAdditionalInformation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserAdditionalInformation_userId_key" ON "UserAdditionalInformation"("userId"); + +-- AddForeignKey +ALTER TABLE "UserAdditionalInformation" ADD CONSTRAINT "UserAdditionalInformation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260405145322_added_status_and_some_changes/migration.sql b/prisma/migrations/20260405145322_added_status_and_some_changes/migration.sql new file mode 100644 index 0000000..61bc7f7 --- /dev/null +++ b/prisma/migrations/20260405145322_added_status_and_some_changes/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - You are about to drop the column `isVerified` on the `user` table. All the data in the column will be lost. + - You are about to drop the column `generatedOn` on the `user_otp` table. All the data in the column will be lost. + - Added the required column `expiresAt` to the `user_otp` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "USER_ACCOUNT_STATUS" AS ENUM ('pending', 'active', 'suspended', 'deleted'); + +-- AlterTable +ALTER TABLE "user" DROP COLUMN "isVerified", +ADD COLUMN "status" "USER_ACCOUNT_STATUS" NOT NULL DEFAULT 'pending', +ALTER COLUMN "password" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "user_otp" DROP COLUMN "generatedOn", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL; diff --git a/prisma/models/user.prisma b/prisma/models/user.prisma index 48f1f89..0ae1f5d 100644 --- a/prisma/models/user.prisma +++ b/prisma/models/user.prisma @@ -1,30 +1,45 @@ model User { - id String @id @default(uuid()) - firstName String - middleName String? - lastName String - email String @unique - password String - role USER_ROLE @default(user) - isVerified Boolean? @default(false) // TODO: Email using queue - refreshToken String? - profilePicture String? - isDeleted Boolean? @default(false) - deletedAt DateTime? + id String @id @default(uuid()) + email String @unique + password String? + role USER_ROLE @default(user) + refreshToken String? + status USER_ACCOUNT_STATUS @default(pending) + isDeleted Boolean? @default(false) + deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - organizations OrganizationUserJoinTable[] - organizationsRequested OrganizationJoinRequest[] + organizations OrganizationUserJoinTable[] + organizationsRequested OrganizationJoinRequest[] + userAdditionalInformation UserAdditionalInformation? @@map("user") } +model UserAdditionalInformation { + id String @id @default(uuid()) + userId String @unique + firstName String + middleName String? + lastName String + profilePicture String? + address String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + model UserOTP { - email String @unique - otp Int - generatedOn DateTime + email String @unique + otp Int + // ExipresAt is also saved so its easier to check and also + // run cron job to remove expired OTPs + createdAt DateTime @default(now()) + expiresAt DateTime @@map("user_otp") } @@ -33,3 +48,10 @@ enum USER_ROLE { superadmin user } + +enum USER_ACCOUNT_STATUS { + pending + active + suspended + deleted +} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 1bfcac4..19c702e 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -5,17 +5,21 @@ import { HttpStatus, Post, Res, + UseGuards, } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { ApiOperation } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { + CompleteProfileSetupRequestDTO, LoginUserRequestDTO, LoginUserResponseDTO, RegisterUserRequestDTO, + ValidateUserRegisterOTPRequestDTO, } from './dto'; import { Response } from 'express'; import { DataResponse } from 'common/http'; -import { Public } from './decorators'; +import { IsTempToken, Public } from './decorators'; +import { AuthGuard } from './guards/auth.guard'; @Controller('auth') @Public() @@ -41,7 +45,6 @@ export class AuthController { } @ApiOperation({ summary: 'User register' }) - @HttpCode(HttpStatus.CREATED) @Post('/register') async register(@Body() body: RegisterUserRequestDTO): Promise { await this.authService.register(body); @@ -49,6 +52,36 @@ export class AuthController { return 'Check your email for OTP'; } + + // TODO: Assign Temp Token + @ApiOperation({ summary: 'Validate OTP' }) + @Post('/validate-otp') + async validateOTP(@Body() body: ValidateUserRegisterOTPRequestDTO) { + const { accessToken, refreshToken } = await this.authService.validateOtp(body); + + return { + message: 'Continue with the rest of profile setup', + data: { + accessToken, + refreshToken + } + } + + } + + // TODO: Assign Temp Token + @ApiOperation({ summary: 'Complete Profile' }) + @ApiBearerAuth("access-token") + @Public(false) + @IsTempToken() + @UseGuards(AuthGuard) + @Post('/complete-profile') + async completeUserProfile(@Body() body: CompleteProfileSetupRequestDTO): Promise { + await this.authService.completeProfileSetup(body); + + return 'Welcome'; + } + logout() { } forgotPassword() { } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 5fcec87..33cc37b 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -7,6 +7,7 @@ import { UserModule } from 'src/user/user.module'; import { JwtModule } from '@nestjs/jwt'; import { RequestContextModule } from 'core/als/request-context.module'; import { BullModule } from '@nestjs/bullmq'; +import { PrismaModule } from 'src/prisma/prisma.module'; @Global() @Module({ @@ -25,6 +26,7 @@ import { BullModule } from '@nestjs/bullmq'; UserModule, JwtModule, RequestContextModule, + PrismaModule ], }) export class AuthModule { } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 8867104..b1b7ff8 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,12 +1,25 @@ -import { ConflictException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { + BadRequestException, + ConflictException, + Injectable, + InternalServerErrorException, + UnauthorizedException +} from '@nestjs/common'; import { Public } from './decorators'; -import { LoginUserRequestDTO, RegisterUserRequestDTO } from './dto'; +import { + CompleteProfileSetupRequestDTO, + LoginUserRequestDTO, + RegisterUserRequestDTO, + ValidateUserRegisterOTPRequestDTO +} from './dto'; import * as bcrypt from 'bcrypt'; import { UserService } from 'src/user/user.service'; -import { TokenInputType } from './types'; +import { OTPTokenInputType, TokenInputType } from './types'; import { JwtService } from '@nestjs/jwt'; import { Queue } from 'bullmq'; import { InjectQueue } from '@nestjs/bullmq'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { RequestContextService } from 'core/als/request-context.service'; @Injectable() @Public() @@ -14,6 +27,8 @@ export class AuthService { constructor( private readonly userService: UserService, private readonly jwtService: JwtService, + private readonly prismaService: PrismaService, + private readonly requestContext: RequestContextService, @InjectQueue('mail') private readonly mailQueue: Queue ) { } @@ -32,16 +47,19 @@ export class AuthService { * Else, do nothing * */ const now = Number(new Date()) / 1000; - const generatedOn = Number(otpExists.generatedOn) / 1000; + const generatedPlusTwoMin = (Number(otpExists.createdAt) / 1000) + 60 * 2; - if (generatedOn + (60 * 2) > now) { + if (generatedPlusTwoMin > now) { return; } } const otp = this.genOtp() - await this.userService.updateOTPByEmail(dto.email, otp); + await this.prismaService.$transaction(async (tx) => { + this.requestContext.tx = tx; + await this.userService.updateOTPByEmail(dto.email, otp); + }) this.mailQueue.add('send-register-otp-email', { email: dto.email, @@ -57,40 +75,120 @@ export class AuthService { }) return true; - // const hashedPassword = await bcrypt.hash(dto.password, 10); - // await this.userService.createUserWithPassword({ - // ...dto, - // password: hashedPassword, - // }); - // - // this.mailQueue.add('send-welcome-email', { - // email: dto.email - // }, { - // attempts: 3, - // backoff: { - // type: "exponential", - // delay: 3000, - // }, - // removeOnComplete: true, // clean up Redis after success - // removeOnFail: false, - // }) - // - // return true; } // Validate OTP - async validateOtp() { + async validateOtp(dto: ValidateUserRegisterOTPRequestDTO) { + const otpExists = await this.userService.findByEmailInOTP(dto.email) + const now = Number(new Date()) / 1000; + if (!otpExists) + throw new BadRequestException("No user found") + else if (otpExists.otp !== dto.otp) + throw new BadRequestException("Invalid OTP") + else if ((Number(otpExists.expiresAt) / 1000 < now)) { + await this.userService.removeByEmailInOTP(dto.email); + throw new BadRequestException("OTP has expired") + } + + const user = await this.prismaService.$transaction(async (tx) => { + this.requestContext.tx = tx; + await this.userService.removeByEmailInOTP(dto.email); + return await this.userService.initializeUserWithEmail(dto.email) + }) + + if (!user) + throw new InternalServerErrorException() + + const token = { + userId: user.id, + email: user.email, + status: user.status + } + + const tokens = this.genSignedTempToken(token) + + return tokens; } // Complete rest of singup process - async completeSignup() { + async completeProfileSetup(dto: CompleteProfileSetupRequestDTO) { + const user = this.requestContext.user; + if (!user) + throw new UnauthorizedException("User") + const hashedPassword = await bcrypt.hash(dto.password, 10); + + const { + newUser, + userAdditionalInfo: _ + } = await this.prismaService.$transaction(async (tx) => { + this.requestContext.tx = tx; + + const newUser = await this.userService.createUserWithPassword( + user.email, + hashedPassword, + ); + + if (!newUser) + throw new UnauthorizedException() + + const userAdditionalInfo = await this.userService.createUserAdditionalInformation( + newUser?.id, + dto + ) + + return { + newUser, + userAdditionalInfo, + } + }) + + this.mailQueue.add('send-welcome-email', { + email: user.email, + }, { + attempts: 3, + backoff: { + type: "exponential", + delay: 3000, + }, + removeOnComplete: true, // clean up Redis after success + removeOnFail: false, + }) + + const token = { + userId: newUser.id, + email: newUser.email, + role: newUser.role, + status: newUser.status + }; + + const { + accessToken, + refreshToken + } = await this.genSignedTokens(token) + + return { + accessToken, + refreshToken, + user: newUser + }; } async login(dto: LoginUserRequestDTO) { const user = await this.userService.findUserForAuth(dto.email); if (!user) throw new UnauthorizedException('Invalid credentials.'); + else if (!user.password) { + const token = { + userId: user.id, + email: user.email, + status: user.status + } + + const { accessToken, refreshToken } = await this.genSignedTempToken(token) + + return { accessToken, refreshToken, user }; + } const passwordMatch = await bcrypt.compare(dto.password, user.password); if (!passwordMatch) throw new UnauthorizedException('Invalid credentials.'); @@ -99,6 +197,7 @@ export class AuthService { userId: user.id, email: user.email, role: user.role, + status: user.status }; // TODO: Store more info: orgId, orgRole, etc @@ -136,6 +235,23 @@ export class AuthService { return { accessToken, refreshToken }; } + private async genSignedTempToken(token: OTPTokenInputType) { + const accessToken = await this.jwtService.signAsync(token, { + secret: 'demo', + }); + + const refreshToken = await this.jwtService.signAsync( + { + userId: token.userId, + }, + { + secret: 'demo', + }, + ); + + return { accessToken, refreshToken }; + } + private genOtp() { return 123456; } diff --git a/src/auth/decorators/index.ts b/src/auth/decorators/index.ts index 1fac1b8..8faf855 100644 --- a/src/auth/decorators/index.ts +++ b/src/auth/decorators/index.ts @@ -1,3 +1,4 @@ export * from './public.decorator'; export * from './role.decorator'; export * from './authorization.decorator'; +export * from './isTemp.decorator' diff --git a/src/auth/decorators/isTemp.decorator.ts b/src/auth/decorators/isTemp.decorator.ts new file mode 100644 index 0000000..e6a72cb --- /dev/null +++ b/src/auth/decorators/isTemp.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from "@nestjs/common"; +import { TEMP_TOKEN_KEY } from "common/keys"; + +export const IsTempToken = () => SetMetadata(TEMP_TOKEN_KEY, true) diff --git a/src/auth/dto/complete-setup.dto.ts b/src/auth/dto/complete-setup.dto.ts new file mode 100644 index 0000000..36568a5 --- /dev/null +++ b/src/auth/dto/complete-setup.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsNotEmpty, IsOptional, IsString, MinLength } from "class-validator"; + +export class CompleteProfileSetupRequestDTO { + @ApiProperty({ + description: "User's firstName", + example: 'John', + type: 'string', + }) + @IsString() + @IsNotEmpty() + firstName: string; + + @ApiPropertyOptional({ + description: "User's middleName", + example: 'Kumar', + type: 'string', + }) + @IsString() + @IsOptional() + middleName?: string; + + @ApiProperty({ + description: "User's lastName", + example: 'Doe', + type: 'string', + }) + @IsString() + @IsNotEmpty() + lastName: string; + + @ApiProperty({ + description: "User's password", + example: '123456', + type: 'string', + minLength: 6, + }) + @IsString() + @IsNotEmpty() + @MinLength(6) + password: string; +} diff --git a/src/auth/dto/index.ts b/src/auth/dto/index.ts index 8dbf399..cf98c43 100644 --- a/src/auth/dto/index.ts +++ b/src/auth/dto/index.ts @@ -1,3 +1,5 @@ export * from './register-user.dto'; export * from './login-user.dto'; export * from './login-response.dto'; +export * from "./validate-otp.dto"; +export * from "./complete-setup.dto"; diff --git a/src/auth/dto/register-user.dto.ts b/src/auth/dto/register-user.dto.ts index aee295b..09f006c 100644 --- a/src/auth/dto/register-user.dto.ts +++ b/src/auth/dto/register-user.dto.ts @@ -2,39 +2,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, - IsOptional, - IsString, - MinLength, } from 'class-validator'; export class RegisterUserRequestDTO { - @ApiProperty({ - description: "User's firstName", - example: 'John', - type: 'string', - }) - @IsString() - @IsNotEmpty() - firstName: string; - - @ApiPropertyOptional({ - description: "User's middleName", - example: 'Kumar', - type: 'string', - }) - @IsString() - @IsOptional() - middleName?: string; - - @ApiProperty({ - description: "User's lastName", - example: 'Doe', - type: 'string', - }) - @IsString() - @IsNotEmpty() - lastName: string; - @ApiProperty({ description: "User's email", example: 'user@example.com', @@ -43,15 +13,4 @@ export class RegisterUserRequestDTO { @IsEmail() @IsNotEmpty() email: string; - - @ApiProperty({ - description: "User's password", - example: '123456', - type: 'string', - minLength: 6, - }) - @IsString() - @IsNotEmpty() - @MinLength(6) - password: string; } diff --git a/src/auth/dto/validate-otp.dto.ts b/src/auth/dto/validate-otp.dto.ts new file mode 100644 index 0000000..94c6dce --- /dev/null +++ b/src/auth/dto/validate-otp.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsNotEmpty, IsNumber } from "class-validator"; + +export class ValidateUserRegisterOTPRequestDTO { + @ApiProperty({ + description: "Register OTP", + example: 123456, + type: 'number', + }) + @IsNumber() + @IsNotEmpty() + otp: number + + @ApiProperty({ + description: "User's email", + example: 'user@example.com', + type: 'string', + }) + @IsEmail() + @IsNotEmpty() + email: string; +} diff --git a/src/auth/guards/auth.guard.ts b/src/auth/guards/auth.guard.ts index f1fffba..4ea5e4a 100644 --- a/src/auth/guards/auth.guard.ts +++ b/src/auth/guards/auth.guard.ts @@ -1,6 +1,7 @@ import { CanActivate, ExecutionContext, + ForbiddenException, Injectable, UnauthorizedException, } from '@nestjs/common'; @@ -9,8 +10,9 @@ import { JwtService } from '@nestjs/jwt'; import { JwtPayload } from '../types'; import { Request } from 'express'; import { Reflector } from '@nestjs/core'; -import { PUBLIC_KEY } from 'common/keys'; +import { PUBLIC_KEY, TEMP_TOKEN_KEY } from 'common/keys'; import { UserService } from 'src/user/user.service'; +import { USER_ACCOUNT_STATUS } from 'prisma/generated/prisma/enums'; @Injectable() export class AuthGuard implements CanActivate { @@ -19,7 +21,7 @@ export class AuthGuard implements CanActivate { private readonly jwtService: JwtService, private readonly requestContext: RequestContextService, private readonly userService: UserService, - ) {} + ) { } async canActivate(context: ExecutionContext) { const request = context.switchToHttp().getRequest(); @@ -30,6 +32,11 @@ export class AuthGuard implements CanActivate { ); if (isPublicRoute) return true; + const isTempToken = this.reflector.getAllAndOverride( + TEMP_TOKEN_KEY, + [context.getHandler(), context.getClass()], + ) + const token = this.extractTokenFromHeader(request); if (!token) throw new UnauthorizedException(); @@ -38,9 +45,17 @@ export class AuthGuard implements CanActivate { secret: 'demo', }); + // TODO: Redis + Org too, blacklist token const userExists = await this.userService.findById(payload.userId); if (!userExists) throw new UnauthorizedException(); + + // NOTE: Add more checks here (other account status) + if (userExists.status !== USER_ACCOUNT_STATUS.active) { + if (userExists.status === USER_ACCOUNT_STATUS.pending && isTempToken === undefined) + throw new ForbiddenException() + } + this.requestContext.set('user', payload); return true; diff --git a/src/auth/types/jwt.ts b/src/auth/types/jwt.ts index 7cd9cd4..2c3fae4 100644 --- a/src/auth/types/jwt.ts +++ b/src/auth/types/jwt.ts @@ -1,10 +1,11 @@ -import { ORG_ROLE, USER_ROLE } from 'prisma/generated/prisma/enums'; +import { ORG_ROLE, USER_ACCOUNT_STATUS, USER_ROLE } from 'prisma/generated/prisma/enums'; export interface JwtPayload { iat?: number; exp?: number; orgId?: string; orgRole?: ORG_ROLE; + status: USER_ACCOUNT_STATUS; userId: string; email: string; role: USER_ROLE; diff --git a/src/auth/types/token.ts b/src/auth/types/token.ts index 37900d2..2745499 100644 --- a/src/auth/types/token.ts +++ b/src/auth/types/token.ts @@ -1,10 +1,19 @@ +import { USER_ACCOUNT_STATUS } from "prisma/generated/prisma/enums"; + export interface TokenInputType { userId: string; email: string; role: string; + status: USER_ACCOUNT_STATUS; } -export interface AccessTokenPayloadType extends TokenInputType {} +export interface OTPTokenInputType { + userId: string; + email: string; + status: USER_ACCOUNT_STATUS +} + +export interface AccessTokenPayloadType extends TokenInputType { } export interface RefreshTokenPayloadType { userId: string; diff --git a/src/organization-membership/organization-membership.service.ts b/src/organization-membership/organization-membership.service.ts index 3044536..97632fa 100644 --- a/src/organization-membership/organization-membership.service.ts +++ b/src/organization-membership/organization-membership.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, - ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; @@ -12,8 +11,6 @@ import { ORGANIZATION_JOIN_REQUEST, ORGANIZATION_JOIN_REQUEST_TYPE, } from 'prisma/generated/prisma/enums'; -import { AuthorizationService } from 'src/authorization/authorization.service'; -import { USER_ORGANIZATION_OPERATIONS } from 'src/authorization/operations'; import { Prisma } from 'prisma/generated/prisma/client'; import { JoinRequestToOrganizationRequestDTO } from './dto/join-request.dto'; import { USER_ORG_ACCEPT_REJECT_ACTION } from './constants'; @@ -24,7 +21,6 @@ export class OrganizationMembershipService { private readonly userService: UserService, private readonly orgService: OrganizationService, private readonly prisma: PrismaService, - private readonly authorization: AuthorizationService, ) { } /* * @@ -121,7 +117,12 @@ export class OrganizationMembershipService { requestType: joinReqType, status: ORGANIZATION_JOIN_REQUEST.PENDING }, - include: { user: { select: { firstName: true, email: true } } } + include: { + user: { + include: { userAdditionalInformation: { select: { firstName: true } } }, + select: { email: true } + }, + } }) } @@ -442,9 +443,9 @@ export class OrganizationMembershipService { include: { user: { select: { - firstName: true, email: true - } + }, + include: { userAdditionalInformation: { select: { firstName: true } } }, } }, } diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 8cf1de6..12972e9 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -4,11 +4,12 @@ import { OnModuleDestroy, OnModuleInit, } from '@nestjs/common'; -import { PrismaClient } from 'prisma/generated/prisma/client'; +import { Prisma, PrismaClient } from 'prisma/generated/prisma/client'; import { Pool } from 'pg'; import { PrismaPg } from '@prisma/adapter-pg'; import { RequestContextService } from 'core/als/request-context.service'; import { ConfigService } from '@nestjs/config'; +import { TransactionClient } from 'prisma/generated/prisma/internal/prismaNamespace'; @Global() @Injectable() @@ -49,7 +50,7 @@ export class PrismaService * For shared transaction across services. If present, return the transaction. * Else returns itself. * */ - get client() { - return this.ctx.get().tx ?? this; + get client(): TransactionClient { + return this.ctx.tx ?? this; } } diff --git a/src/user/dtos/user.dto.ts b/src/user/dtos/user.dto.ts index cf6b33d..dc1feda 100644 --- a/src/user/dtos/user.dto.ts +++ b/src/user/dtos/user.dto.ts @@ -1,21 +1,15 @@ -import { User } from 'prisma/generated/prisma/client'; +import { User, USER_ACCOUNT_STATUS } from 'prisma/generated/prisma/client'; export class UserDTO { readonly id: string; readonly email: string; - readonly firstName: string; - readonly middleName: string | null; - readonly lastName: string; readonly role: string; - readonly profilePicture: string | null; + readonly status: USER_ACCOUNT_STATUS; constructor(user: User) { this.id = user.id; this.email = user.email; - this.firstName = user.firstName; - this.lastName = user.lastName; - this.middleName = user.middleName; this.role = user.role; - this.profilePicture = user.profilePicture; + this.status = user.status } } diff --git a/src/user/user.service.ts b/src/user/user.service.ts index d4f2ffa..927137a 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -1,16 +1,50 @@ import { ConflictException, Injectable } from '@nestjs/common'; -import { Prisma } from 'prisma/generated/prisma/client'; -import { RegisterUserRequestDTO } from 'src/auth/dto'; +import { Prisma, USER_ACCOUNT_STATUS } from 'prisma/generated/prisma/client'; +import { CompleteProfileSetupRequestDTO } from 'src/auth/dto'; import { PrismaService } from 'src/prisma/prisma.service'; @Injectable() export class UserService { - constructor(private readonly prisma: PrismaService) { } + constructor( + private readonly prisma: PrismaService, + ) { } - async createUserWithPassword(dto: RegisterUserRequestDTO) { + async initializeUserWithEmail(email: string) { try { - return await this.prisma.user.create({ - data: dto, + return await this.prisma.client.user.create({ + data: { + email, + }, + }); + } catch (err) { + if (err instanceof Prisma.PrismaClientKnownRequestError) { + if (err.code === 'P2002') { + throw new ConflictException('User already exists'); + } + } else throw err; + } + } + + async createUserWithPassword(email: string, password: string) { + return await this.prisma.client.user.update({ + where: { + email, + }, + data: { + password, + status: USER_ACCOUNT_STATUS.active + }, + }); + } + + async createUserAdditionalInformation(userId: string, dto: CompleteProfileSetupRequestDTO) { + const { password, ...rest } = dto; + try { + return await this.prisma.client.userAdditionalInformation.create({ + data: { + userId, + ...rest + }, }); } catch (err) { if (err instanceof Prisma.PrismaClientKnownRequestError) { @@ -37,7 +71,8 @@ export class UserService { } async findByEmail(email: string) { - return await this.prisma.user.findUnique({ + const client = this.prisma.client.user ?? this.prisma.user; + return await client.findUnique({ where: { email: email, }, @@ -55,7 +90,8 @@ export class UserService { * USER OTP SERVICES * */ async findByEmailInOTP(email: string) { - return await this.prisma.userOTP.findUnique({ + const client = this.prisma.client.userOTP ?? this.prisma.userOTP; + return await client.findUnique({ where: { email, }, @@ -63,7 +99,7 @@ export class UserService { } async removeByEmailInOTP(email: string) { - return await this.prisma.userOTP.delete({ + return await this.prisma.client.userOTP.delete({ where: { email } @@ -71,18 +107,20 @@ export class UserService { } async updateOTPByEmail(email: string, otp: number) { - return await this.prisma.userOTP.upsert({ + return await this.prisma.client.userOTP.upsert({ where: { email }, create: { email, otp, - generatedOn: new Date() + createdAt: new Date(), + expiresAt: new Date(Date.now() + 5 * 60 * 1000) }, update: { otp, - generatedOn: new Date() + createdAt: new Date(), + expiresAt: new Date(Date.now() + 5 * 60 * 1000) } }) }