From c312e7901ea566f7ec776905230beefbe12e5f44 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 3 Jul 2026 16:24:57 +0200 Subject: [PATCH] fix(auth): normalize email to lowercase on register/login + rollback orphaned org L'email n'etait pas normalise, or la contrainte chk_users_email exige des minuscules. Un email avec majuscules faisait echouer l'INSERT utilisateur APRES la creation de l'organisation (flux non transactionnel), laissant une org orpheline -> 'An organization with this name already exists' aux tentatives suivantes. - register/login: email.trim().toLowerCase() - register: rollback de l'organisation nouvellement creee si la creation de l'utilisateur echoue (anti-orphelin) Co-Authored-By: Claude Opus 4.8 --- .../src/application/auth/auth.service.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/application/auth/auth.service.ts b/apps/backend/src/application/auth/auth.service.ts index 040ff1b..17f20f8 100644 --- a/apps/backend/src/application/auth/auth.service.ts +++ b/apps/backend/src/application/auth/auth.service.ts @@ -70,6 +70,12 @@ export class AuthService { organizationData?: RegisterOrganizationDto, invitationRole?: string ): Promise<{ accessToken: string; refreshToken: string; user: any }> { + // Emails are stored lowercase (enforced by the chk_users_email DB CHECK + // constraint), so normalize before any lookup or insert. Without this, an + // address containing uppercase letters fails the user INSERT after the + // organization has already been created — leaving an orphaned organization. + email = email.trim().toLowerCase(); + this.logger.log(`Registering new user: ${email}`); const existingUser = await this.userRepository.findByEmail(email); @@ -90,6 +96,9 @@ export class AuthService { // 2. If organizationData is provided (new user), create a new organization // 3. Otherwise, use default organization const finalOrganizationId = await this.resolveOrganizationId(organizationId, organizationData); + // Track whether a brand-new organization was created for this registration, + // so it can be rolled back if the subsequent user creation fails. + const createdNewOrg = !organizationId && !!organizationData; // Determine role: // - If invitation role is provided (invited user), use it @@ -121,7 +130,28 @@ export class AuthService { role: userRole, }); - const savedUser = await this.userRepository.save(user); + let savedUser: User; + try { + savedUser = await this.userRepository.save(user); + } catch (err) { + // The organization is created before the user (non-atomic flow). If the + // user INSERT fails, roll the organization back so a retry isn't blocked + // by an orphaned organization ("name already exists"). + if (createdNewOrg) { + try { + await this.organizationRepository.deleteById(finalOrganizationId); + this.logger.warn( + `Rolled back orphaned organization ${finalOrganizationId} after failed user creation` + ); + } catch (cleanupErr) { + this.logger.error( + `Failed to roll back organization ${finalOrganizationId}`, + cleanupErr as Error + ); + } + } + throw err; + } // Allocate a license for the new user try { @@ -158,6 +188,7 @@ export class AuthService { password: string, rememberMe = false ): Promise<{ accessToken: string; refreshToken: string; user: any }> { + email = email.trim().toLowerCase(); this.logger.log(`Login attempt for: ${email}`); const user = await this.userRepository.findByEmail(email);