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,
|
organizationData?: RegisterOrganizationDto,
|
||||||
invitationRole?: string
|
invitationRole?: string
|
||||||
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
): 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}`);
|
this.logger.log(`Registering new user: ${email}`);
|
||||||
|
|
||||||
const existingUser = await this.userRepository.findByEmail(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
|
// 2. If organizationData is provided (new user), create a new organization
|
||||||
// 3. Otherwise, use default organization
|
// 3. Otherwise, use default organization
|
||||||
const finalOrganizationId = await this.resolveOrganizationId(organizationId, organizationData);
|
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:
|
// Determine role:
|
||||||
// - If invitation role is provided (invited user), use it
|
// - If invitation role is provided (invited user), use it
|
||||||
@ -121,7 +130,28 @@ export class AuthService {
|
|||||||
role: userRole,
|
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
|
// Allocate a license for the new user
|
||||||
try {
|
try {
|
||||||
@ -158,6 +188,7 @@ export class AuthService {
|
|||||||
password: string,
|
password: string,
|
||||||
rememberMe = false
|
rememberMe = false
|
||||||
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
||||||
|
email = email.trim().toLowerCase();
|
||||||
this.logger.log(`Login attempt for: ${email}`);
|
this.logger.log(`Login attempt for: ${email}`);
|
||||||
|
|
||||||
const user = await this.userRepository.findByEmail(email);
|
const user = await this.userRepository.findByEmail(email);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user