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 <noreply@anthropic.com>
This commit is contained in:
David 2026-07-03 16:24:57 +02:00
parent 64de600292
commit c312e7901e

View File

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