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:
parent
64de600292
commit
c312e7901e
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user