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