feat: Change auth flow

This commit is contained in:
SauravDhakal
2026-04-11 07:57:28 +05:45
parent ab8b2ef353
commit aa8deadf1f
21 changed files with 442 additions and 123 deletions

View File

@@ -3,3 +3,4 @@ export const ROLE_KEY = '__ROLE_KEY__';
export const ORG_ROLE_KEY = '__ORG_ROLE_KEY__' export const ORG_ROLE_KEY = '__ORG_ROLE_KEY__'
export const ORG_ROLES_ALL_KEY = '__ORG_ROLE_ALL_KEY__'; export const ORG_ROLES_ALL_KEY = '__ORG_ROLE_ALL_KEY__';
export const CAN_PERFORM_KEY = '__CAN_PERFORM_KEY__'; export const CAN_PERFORM_KEY = '__CAN_PERFORM_KEY__';
export const TEMP_TOKEN_KEY = '__TEMP_TOKEN_KEY__';

View File

@@ -10,6 +10,7 @@
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start", "start": "nest start",
"start:dev": "nest start --watch", "start:dev": "nest start --watch",
"dev": "nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,30 +1,45 @@
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())
firstName String email String @unique
middleName String? password String?
lastName String role USER_ROLE @default(user)
email String @unique refreshToken String?
password String status USER_ACCOUNT_STATUS @default(pending)
role USER_ROLE @default(user) isDeleted Boolean? @default(false)
isVerified Boolean? @default(false) // TODO: Email using queue deletedAt DateTime?
refreshToken String?
profilePicture String?
isDeleted Boolean? @default(false)
deletedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
organizations OrganizationUserJoinTable[] organizations OrganizationUserJoinTable[]
organizationsRequested OrganizationJoinRequest[] organizationsRequested OrganizationJoinRequest[]
userAdditionalInformation UserAdditionalInformation?
@@map("user") @@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 { model UserOTP {
email String @unique email String @unique
otp Int otp Int
generatedOn DateTime // 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") @@map("user_otp")
} }
@@ -33,3 +48,10 @@ enum USER_ROLE {
superadmin superadmin
user user
} }
enum USER_ACCOUNT_STATUS {
pending
active
suspended
deleted
}

View File

@@ -5,17 +5,21 @@ import {
HttpStatus, HttpStatus,
Post, Post,
Res, Res,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { ApiOperation } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { import {
CompleteProfileSetupRequestDTO,
LoginUserRequestDTO, LoginUserRequestDTO,
LoginUserResponseDTO, LoginUserResponseDTO,
RegisterUserRequestDTO, RegisterUserRequestDTO,
ValidateUserRegisterOTPRequestDTO,
} from './dto'; } from './dto';
import { Response } from 'express'; import { Response } from 'express';
import { DataResponse } from 'common/http'; import { DataResponse } from 'common/http';
import { Public } from './decorators'; import { IsTempToken, Public } from './decorators';
import { AuthGuard } from './guards/auth.guard';
@Controller('auth') @Controller('auth')
@Public() @Public()
@@ -41,7 +45,6 @@ export class AuthController {
} }
@ApiOperation({ summary: 'User register' }) @ApiOperation({ summary: 'User register' })
@HttpCode(HttpStatus.CREATED)
@Post('/register') @Post('/register')
async register(@Body() body: RegisterUserRequestDTO): Promise<string> { async register(@Body() body: RegisterUserRequestDTO): Promise<string> {
await this.authService.register(body); await this.authService.register(body);
@@ -49,6 +52,36 @@ export class AuthController {
return 'Check your email for OTP'; 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<string> {
await this.authService.completeProfileSetup(body);
return 'Welcome';
}
logout() { } logout() { }
forgotPassword() { } forgotPassword() { }

View File

@@ -7,6 +7,7 @@ import { UserModule } from 'src/user/user.module';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { RequestContextModule } from 'core/als/request-context.module'; import { RequestContextModule } from 'core/als/request-context.module';
import { BullModule } from '@nestjs/bullmq'; import { BullModule } from '@nestjs/bullmq';
import { PrismaModule } from 'src/prisma/prisma.module';
@Global() @Global()
@Module({ @Module({
@@ -25,6 +26,7 @@ import { BullModule } from '@nestjs/bullmq';
UserModule, UserModule,
JwtModule, JwtModule,
RequestContextModule, RequestContextModule,
PrismaModule
], ],
}) })
export class AuthModule { } export class AuthModule { }

View File

@@ -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 { Public } from './decorators';
import { LoginUserRequestDTO, RegisterUserRequestDTO } from './dto'; import {
CompleteProfileSetupRequestDTO,
LoginUserRequestDTO,
RegisterUserRequestDTO,
ValidateUserRegisterOTPRequestDTO
} from './dto';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import { TokenInputType } from './types'; import { OTPTokenInputType, TokenInputType } from './types';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq'; import { InjectQueue } from '@nestjs/bullmq';
import { PrismaService } from 'src/prisma/prisma.service';
import { RequestContextService } from 'core/als/request-context.service';
@Injectable() @Injectable()
@Public() @Public()
@@ -14,6 +27,8 @@ export class AuthService {
constructor( constructor(
private readonly userService: UserService, private readonly userService: UserService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly prismaService: PrismaService,
private readonly requestContext: RequestContextService,
@InjectQueue('mail') private readonly mailQueue: Queue @InjectQueue('mail') private readonly mailQueue: Queue
) { } ) { }
@@ -32,16 +47,19 @@ export class AuthService {
* Else, do nothing * Else, do nothing
* */ * */
const now = Number(new Date()) / 1000; 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; return;
} }
} }
const otp = this.genOtp() 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', { this.mailQueue.add('send-register-otp-email', {
email: dto.email, email: dto.email,
@@ -57,40 +75,120 @@ export class AuthService {
}) })
return true; 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 // 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 // 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) { async login(dto: LoginUserRequestDTO) {
const user = await this.userService.findUserForAuth(dto.email); const user = await this.userService.findUserForAuth(dto.email);
if (!user) throw new UnauthorizedException('Invalid credentials.'); 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); const passwordMatch = await bcrypt.compare(dto.password, user.password);
if (!passwordMatch) throw new UnauthorizedException('Invalid credentials.'); if (!passwordMatch) throw new UnauthorizedException('Invalid credentials.');
@@ -99,6 +197,7 @@ export class AuthService {
userId: user.id, userId: user.id,
email: user.email, email: user.email,
role: user.role, role: user.role,
status: user.status
}; };
// TODO: Store more info: orgId, orgRole, etc // TODO: Store more info: orgId, orgRole, etc
@@ -136,6 +235,23 @@ export class AuthService {
return { accessToken, refreshToken }; 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() { private genOtp() {
return 123456; return 123456;
} }

View File

@@ -1,3 +1,4 @@
export * from './public.decorator'; export * from './public.decorator';
export * from './role.decorator'; export * from './role.decorator';
export * from './authorization.decorator'; export * from './authorization.decorator';
export * from './isTemp.decorator'

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from "@nestjs/common";
import { TEMP_TOKEN_KEY } from "common/keys";
export const IsTempToken = () => SetMetadata(TEMP_TOKEN_KEY, true)

View File

@@ -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;
}

View File

@@ -1,3 +1,5 @@
export * from './register-user.dto'; export * from './register-user.dto';
export * from './login-user.dto'; export * from './login-user.dto';
export * from './login-response.dto'; export * from './login-response.dto';
export * from "./validate-otp.dto";
export * from "./complete-setup.dto";

View File

@@ -2,39 +2,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { import {
IsEmail, IsEmail,
IsNotEmpty, IsNotEmpty,
IsOptional,
IsString,
MinLength,
} from 'class-validator'; } from 'class-validator';
export class RegisterUserRequestDTO { 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({ @ApiProperty({
description: "User's email", description: "User's email",
example: 'user@example.com', example: 'user@example.com',
@@ -43,15 +13,4 @@ export class RegisterUserRequestDTO {
@IsEmail() @IsEmail()
@IsNotEmpty() @IsNotEmpty()
email: string; email: string;
@ApiProperty({
description: "User's password",
example: '123456',
type: 'string',
minLength: 6,
})
@IsString()
@IsNotEmpty()
@MinLength(6)
password: string;
} }

View File

@@ -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;
}

View File

@@ -1,6 +1,7 @@
import { import {
CanActivate, CanActivate,
ExecutionContext, ExecutionContext,
ForbiddenException,
Injectable, Injectable,
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
@@ -9,8 +10,9 @@ import { JwtService } from '@nestjs/jwt';
import { JwtPayload } from '../types'; import { JwtPayload } from '../types';
import { Request } from 'express'; import { Request } from 'express';
import { Reflector } from '@nestjs/core'; 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 { UserService } from 'src/user/user.service';
import { USER_ACCOUNT_STATUS } from 'prisma/generated/prisma/enums';
@Injectable() @Injectable()
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
@@ -19,7 +21,7 @@ export class AuthGuard implements CanActivate {
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly requestContext: RequestContextService, private readonly requestContext: RequestContextService,
private readonly userService: UserService, private readonly userService: UserService,
) {} ) { }
async canActivate(context: ExecutionContext) { async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
@@ -30,6 +32,11 @@ export class AuthGuard implements CanActivate {
); );
if (isPublicRoute) return true; if (isPublicRoute) return true;
const isTempToken = this.reflector.getAllAndOverride<boolean>(
TEMP_TOKEN_KEY,
[context.getHandler(), context.getClass()],
)
const token = this.extractTokenFromHeader(request); const token = this.extractTokenFromHeader(request);
if (!token) throw new UnauthorizedException(); if (!token) throw new UnauthorizedException();
@@ -38,9 +45,17 @@ export class AuthGuard implements CanActivate {
secret: 'demo', secret: 'demo',
}); });
// TODO: Redis + Org too, blacklist token // TODO: Redis + Org too, blacklist token
const userExists = await this.userService.findById(payload.userId); const userExists = await this.userService.findById(payload.userId);
if (!userExists) throw new UnauthorizedException(); 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); this.requestContext.set('user', payload);
return true; return true;

View File

@@ -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 { export interface JwtPayload {
iat?: number; iat?: number;
exp?: number; exp?: number;
orgId?: string; orgId?: string;
orgRole?: ORG_ROLE; orgRole?: ORG_ROLE;
status: USER_ACCOUNT_STATUS;
userId: string; userId: string;
email: string; email: string;
role: USER_ROLE; role: USER_ROLE;

View File

@@ -1,10 +1,19 @@
import { USER_ACCOUNT_STATUS } from "prisma/generated/prisma/enums";
export interface TokenInputType { export interface TokenInputType {
userId: string; userId: string;
email: string; email: string;
role: 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 { export interface RefreshTokenPayloadType {
userId: string; userId: string;

View File

@@ -1,6 +1,5 @@
import { import {
BadRequestException, BadRequestException,
ForbiddenException,
Injectable, Injectable,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
@@ -12,8 +11,6 @@ import {
ORGANIZATION_JOIN_REQUEST, ORGANIZATION_JOIN_REQUEST,
ORGANIZATION_JOIN_REQUEST_TYPE, ORGANIZATION_JOIN_REQUEST_TYPE,
} from 'prisma/generated/prisma/enums'; } 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 { Prisma } from 'prisma/generated/prisma/client';
import { JoinRequestToOrganizationRequestDTO } from './dto/join-request.dto'; import { JoinRequestToOrganizationRequestDTO } from './dto/join-request.dto';
import { USER_ORG_ACCEPT_REJECT_ACTION } from './constants'; import { USER_ORG_ACCEPT_REJECT_ACTION } from './constants';
@@ -24,7 +21,6 @@ export class OrganizationMembershipService {
private readonly userService: UserService, private readonly userService: UserService,
private readonly orgService: OrganizationService, private readonly orgService: OrganizationService,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly authorization: AuthorizationService,
) { } ) { }
/* * /* *
@@ -121,7 +117,12 @@ export class OrganizationMembershipService {
requestType: joinReqType, requestType: joinReqType,
status: ORGANIZATION_JOIN_REQUEST.PENDING 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: { include: {
user: { user: {
select: { select: {
firstName: true,
email: true email: true
} },
include: { userAdditionalInformation: { select: { firstName: true } } },
} }
}, },
} }

View File

@@ -4,11 +4,12 @@ import {
OnModuleDestroy, OnModuleDestroy,
OnModuleInit, OnModuleInit,
} from '@nestjs/common'; } from '@nestjs/common';
import { PrismaClient } from 'prisma/generated/prisma/client'; import { Prisma, PrismaClient } from 'prisma/generated/prisma/client';
import { Pool } from 'pg'; import { Pool } from 'pg';
import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaPg } from '@prisma/adapter-pg';
import { RequestContextService } from 'core/als/request-context.service'; import { RequestContextService } from 'core/als/request-context.service';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { TransactionClient } from 'prisma/generated/prisma/internal/prismaNamespace';
@Global() @Global()
@Injectable() @Injectable()
@@ -49,7 +50,7 @@ export class PrismaService
* For shared transaction across services. If present, return the transaction. * For shared transaction across services. If present, return the transaction.
* Else returns itself. * Else returns itself.
* */ * */
get client() { get client(): TransactionClient {
return this.ctx.get().tx ?? this; return this.ctx.tx ?? this;
} }
} }

View File

@@ -1,21 +1,15 @@
import { User } from 'prisma/generated/prisma/client'; import { User, USER_ACCOUNT_STATUS } from 'prisma/generated/prisma/client';
export class UserDTO { export class UserDTO {
readonly id: string; readonly id: string;
readonly email: string; readonly email: string;
readonly firstName: string;
readonly middleName: string | null;
readonly lastName: string;
readonly role: string; readonly role: string;
readonly profilePicture: string | null; readonly status: USER_ACCOUNT_STATUS;
constructor(user: User) { constructor(user: User) {
this.id = user.id; this.id = user.id;
this.email = user.email; this.email = user.email;
this.firstName = user.firstName;
this.lastName = user.lastName;
this.middleName = user.middleName;
this.role = user.role; this.role = user.role;
this.profilePicture = user.profilePicture; this.status = user.status
} }
} }

View File

@@ -1,16 +1,50 @@
import { ConflictException, Injectable } from '@nestjs/common'; import { ConflictException, Injectable } from '@nestjs/common';
import { Prisma } from 'prisma/generated/prisma/client'; import { Prisma, USER_ACCOUNT_STATUS } from 'prisma/generated/prisma/client';
import { RegisterUserRequestDTO } from 'src/auth/dto'; import { CompleteProfileSetupRequestDTO } from 'src/auth/dto';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
@Injectable() @Injectable()
export class UserService { export class UserService {
constructor(private readonly prisma: PrismaService) { } constructor(
private readonly prisma: PrismaService,
) { }
async createUserWithPassword(dto: RegisterUserRequestDTO) { async initializeUserWithEmail(email: string) {
try { try {
return await this.prisma.user.create({ return await this.prisma.client.user.create({
data: dto, 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) { } catch (err) {
if (err instanceof Prisma.PrismaClientKnownRequestError) { if (err instanceof Prisma.PrismaClientKnownRequestError) {
@@ -37,7 +71,8 @@ export class UserService {
} }
async findByEmail(email: string) { async findByEmail(email: string) {
return await this.prisma.user.findUnique({ const client = this.prisma.client.user ?? this.prisma.user;
return await client.findUnique({
where: { where: {
email: email, email: email,
}, },
@@ -55,7 +90,8 @@ export class UserService {
* USER OTP SERVICES * USER OTP SERVICES
* */ * */
async findByEmailInOTP(email: string) { async findByEmailInOTP(email: string) {
return await this.prisma.userOTP.findUnique({ const client = this.prisma.client.userOTP ?? this.prisma.userOTP;
return await client.findUnique({
where: { where: {
email, email,
}, },
@@ -63,7 +99,7 @@ export class UserService {
} }
async removeByEmailInOTP(email: string) { async removeByEmailInOTP(email: string) {
return await this.prisma.userOTP.delete({ return await this.prisma.client.userOTP.delete({
where: { where: {
email email
} }
@@ -71,18 +107,20 @@ export class UserService {
} }
async updateOTPByEmail(email: string, otp: number) { async updateOTPByEmail(email: string, otp: number) {
return await this.prisma.userOTP.upsert({ return await this.prisma.client.userOTP.upsert({
where: { where: {
email email
}, },
create: { create: {
email, email,
otp, otp,
generatedOn: new Date() createdAt: new Date(),
expiresAt: new Date(Date.now() + 5 * 60 * 1000)
}, },
update: { update: {
otp, otp,
generatedOn: new Date() createdAt: new Date(),
expiresAt: new Date(Date.now() + 5 * 60 * 1000)
} }
}) })
} }