@@ -83,77 +83,118 @@ export const registerUser = async (req, res) => {
8383 return res . status ( 400 ) . json ( { error : "Username must start with letters and contain only lowercase letters, numbers, and underscores" } ) ;
8484 }
8585
86- // Use single query to check all unique constraints atomically
87- const [ existingUser , existingUsername , existingRegNo ] = await Promise . all ( [
88- User . findOne ( { email : trimmedEmail } ) ,
89- User . findOne ( { username : trimmedUsername } ) ,
90- User . findOne ( { RegistrationNumber : trimmedRegNo } )
91- ] ) ;
92-
93- if ( existingUser && existingUser . isVerified ) {
94- return res . status ( 400 ) . json ( { error : "Account already exists, please login" } ) ;
95- }
86+ // Single query to check all unique constraints atomically within transaction
87+ const session = await mongoose . startSession ( ) ;
88+ session . startTransaction ( ) ;
89+
90+ try {
91+ const [ existingUser , existingUsername , existingRegNo ] = await Promise . all ( [
92+ User . findOne ( { email : trimmedEmail } ) . session ( session ) ,
93+ User . findOne ( { username : trimmedUsername } ) . session ( session ) ,
94+ User . findOne ( { RegistrationNumber : trimmedRegNo } ) . session ( session )
95+ ] ) ;
96+
97+ if ( existingUser && existingUser . isVerified ) {
98+ await session . abortTransaction ( ) ;
99+ session . endSession ( ) ;
100+ return res . status ( 400 ) . json ( { error : "Account already exists, please login" } ) ;
101+ }
96102
97- if ( existingUsername && existingUsername . isVerified ) {
98- return res . status ( 400 ) . json ( { error : "Username already exists, please choose another" } ) ;
99- }
103+ if ( existingUsername && existingUsername . isVerified ) {
104+ await session . abortTransaction ( ) ;
105+ session . endSession ( ) ;
106+ return res . status ( 400 ) . json ( { error : "Username already exists, please choose another" } ) ;
107+ }
100108
101- if ( existingRegNo && existingRegNo . isVerified ) {
102- return res . status ( 400 ) . json ( { error : "Registration number already exists" } ) ;
103- }
109+ if ( existingRegNo && existingRegNo . isVerified ) {
110+ await session . abortTransaction ( ) ;
111+ session . endSession ( ) ;
112+ return res . status ( 400 ) . json ( { error : "Registration number already exists" } ) ;
113+ }
104114
105- const salt = await bcrypt . genSalt ( 10 ) ;
106- const hashedPassword = await bcrypt . hash ( password , salt ) ;
115+ const salt = await bcrypt . genSalt ( 10 ) ;
116+ const hashedPassword = await bcrypt . hash ( password , salt ) ;
117+
118+ // Generate a 6-digit OTP
119+ const otp = crypto . randomInt ( 100000 , 999999 ) . toString ( ) ;
120+ const nowIST = new Date ( new Date ( ) . toLocaleString ( 'en-US' , { timeZone : 'Asia/Kolkata' } ) ) ;
121+ const otpExpires = nowIST . getTime ( ) + 5 * 60 * 1000 ; // OTP expires in 5 minutes
122+
123+ // Use upsert with atomic operation to prevent race conditions
124+ await User . findOneAndUpdate (
125+ { email : trimmedEmail } ,
126+ {
127+ $set : {
128+ email : trimmedEmail ,
129+ name : trimmedName ,
130+ username : trimmedUsername ,
131+ password : hashedPassword ,
132+ RegistrationNumber : trimmedRegNo ,
133+ branch : trimmedBranch ,
134+ collegeName : trimmedCollege ,
135+ isAffiliate : isAffiliate || false ,
136+ otp,
137+ otpExpires,
138+ isVerified : false ,
139+ }
140+ } ,
141+ {
142+ upsert : true ,
143+ new : true ,
144+ setDefaultsOnInsert : true ,
145+ session
146+ }
147+ ) ;
107148
108- // Generate a 6-digit OTP
109- const otp = crypto . randomInt ( 100000 , 999999 ) . toString ( ) ;
110- const nowIST = new Date ( new Date ( ) . toLocaleString ( 'en-US' , { timeZone : 'Asia/Kolkata' } ) ) ;
111- const otpExpires = nowIST . getTime ( ) + 5 * 60 * 1000 ; // OTP expires in 5 minutes
149+ await session . commitTransaction ( ) ;
150+ session . endSession ( ) ;
112151
113- // Use upsert with atomic operation to prevent race conditions
114- await User . findOneAndUpdate (
115- { email : trimmedEmail } ,
116- {
117- $set : {
118- email : trimmedEmail ,
119- name : trimmedName ,
120- username : trimmedUsername ,
121- password : hashedPassword ,
122- RegistrationNumber : trimmedRegNo ,
123- branch : trimmedBranch ,
124- collegeName : trimmedCollege ,
125- isAffiliate : isAffiliate || false ,
126- otp,
127- otpExpires,
128- isVerified : false ,
129- }
130- } ,
131- {
132- upsert : true ,
133- new : true ,
134- // Prevent duplicate key errors during concurrent registrations
135- setDefaultsOnInsert : true
136- }
137- ) ;
152+ // Send OTP to user's email
153+ await sendOTPEmail ( trimmedEmail , otp ) ;
138154
139- // Send OTP to user's email
140- await sendOTPEmail ( trimmedEmail , otp ) ;
155+ auditService . authEvent ( 'registration_otp_sent' , {
156+ requestId : req . auditContext ?. requestId ,
157+ email : trimmedEmail ,
158+ username : trimmedUsername ,
159+ collegeName : trimmedCollege ,
160+ branch : trimmedBranch
161+ } ) ;
141162
142- auditService . authEvent ( 'registration_otp_sent' , {
143- requestId : req . auditContext ?. requestId ,
144- email : trimmedEmail ,
145- username : trimmedUsername ,
146- collegeName : trimmedCollege ,
147- branch : trimmedBranch
148- } ) ;
163+ audit . complete ( {
164+ email : trimmedEmail ,
165+ username : trimmedUsername ,
166+ otpSent : true
167+ } ) ;
149168
150- audit . complete ( {
151- email : trimmedEmail ,
152- username : trimmedUsername ,
153- otpSent : true
154- } ) ;
169+ res . status ( 201 ) . json ( { message : "OTP sent to email. Verify to complete registration." } ) ;
170+
171+ } catch ( error ) {
172+ await session . abortTransaction ( ) ;
173+ session . endSession ( ) ;
174+
175+ if ( error . code === 11000 ) { // MongoDB duplicate key error
176+ const field = Object . keys ( error . keyPattern || { } ) [ 0 ] ;
177+ const fieldName = field === 'username' ? 'Username' :
178+ field === 'RegistrationNumber' ? 'Registration number' : 'Email' ;
179+ auditService . authEvent ( 'registration_duplicate_error' , {
180+ requestId : req . auditContext ?. requestId ,
181+ email : trimmedEmail ,
182+ duplicateField : field ,
183+ error : error . message
184+ } ) ;
185+ return res . status ( 400 ) . json ( { error : `${ fieldName } already exists` } ) ;
186+ }
187+
188+ auditService . error ( 'Registration failed' , error , {
189+ requestId : req . auditContext ?. requestId ,
190+ email : req . body . email ,
191+ username : req . body . username ,
192+ ip : req . ip
193+ } ) ;
155194
156- res . status ( 201 ) . json ( { message : "OTP sent to email. Verify to complete registration." } ) ;
195+ audit . error ( error ) ;
196+ res . status ( 500 ) . json ( { error : error . message } ) ;
197+ }
157198 } catch ( error ) {
158199 auditService . error ( 'Registration failed' , error , {
159200 requestId : req . auditContext ?. requestId ,
0 commit comments