From d0cfef55e332cea211d42dcfe8b844400285de0f Mon Sep 17 00:00:00 2001 From: anshifmonz Date: Tue, 20 May 2025 17:30:04 +0530 Subject: [PATCH] fix(auth): fix race condition using DB-level uniqueness check Previously, we checked for existing email and username before user creation. This opened a race condition where simultaneous requests could bypass checks and insert conflicting records. Now we rely on Prisma's unique constraints and remove the manual pre-checks. If a duplicate is attempted, we catch the P2002 error and return proper validation. This also reduces DB queries from 3 to 1, improving performance and atomicity. --- src/app/routes/auth/auth.service.ts | 86 ++++++++++++----------------- 1 file changed, 34 insertions(+), 52 deletions(-) diff --git a/src/app/routes/auth/auth.service.ts b/src/app/routes/auth/auth.service.ts index d09a28d3d..bfbe8f6bb 100644 --- a/src/app/routes/auth/auth.service.ts +++ b/src/app/routes/auth/auth.service.ts @@ -6,35 +6,6 @@ import { RegisteredUser } from './registered-user.model'; import generateToken from './token.utils'; import { User } from './user.model'; -const checkUserUniqueness = async (email: string, username: string) => { - const existingUserByEmail = await prisma.user.findUnique({ - where: { - email, - }, - select: { - id: true, - }, - }); - - const existingUserByUsername = await prisma.user.findUnique({ - where: { - username, - }, - select: { - id: true, - }, - }); - - if (existingUserByEmail || existingUserByUsername) { - throw new HttpException(422, { - errors: { - ...(existingUserByEmail ? { email: ['has already been taken'] } : {}), - ...(existingUserByUsername ? { username: ['has already been taken'] } : {}), - }, - }); - } -}; - export const createUser = async (input: RegisterInput): Promise => { const email = input.email?.trim(); const username = input.username?.trim(); @@ -53,32 +24,43 @@ export const createUser = async (input: RegisterInput): Promise throw new HttpException(422, { errors: { password: ["can't be blank"] } }); } - await checkUserUniqueness(email, username); + try { + const hashedPassword = await bcrypt.hash(password, 10); + + const user = await prisma.user.create({ + data: { + username, + email, + password: hashedPassword, + ...(image ? { image } : {}), + ...(bio ? { bio } : {}), + ...(demo ? { demo } : {}), + }, + select: { + id: true, + email: true, + username: true, + bio: true, + image: true, + }, + }); - const hashedPassword = await bcrypt.hash(password, 10); + return { + ...user, + token: generateToken(user.id), + }; + } catch (err: any) { + if (err.code === "P2002" && Array.isArray(err.meta?.target)) { + const errors: Record = {}; - const user = await prisma.user.create({ - data: { - username, - email, - password: hashedPassword, - ...(image ? { image } : {}), - ...(bio ? { bio } : {}), - ...(demo ? { demo } : {}), - }, - select: { - id: true, - email: true, - username: true, - bio: true, - image: true, - }, - }); + for (const field of err.meta.target as string[]) { + errors[field] = ["has already been taken"]; + } - return { - ...user, - token: generateToken(user.id), - }; + throw new HttpException(422, { errors }); + } + throw err; + } }; export const login = async (userPayload: any) => {