@@ -5,14 +5,53 @@ const bcrypt = require("bcryptjs");
55const z = require ( "zod" ) ;
66const jwt = require ( "jsonwebtoken" ) ;
77const sendOtp = require ( "../utils/emailService" ) ;
8+ const crypto = require ( "crypto" ) ;
89const {
910 loginSchema,
1011 changePasswordSchema,
1112 deleteAccountSchema,
12- onlyEmailSchema
13+ onlyEmailSchema,
14+ resetPasswordSchema,
15+ verifyOtpSchema
1316} = require ( "../utils/input.validation" ) ;
1417
18+ const JWT_EXPIRES_IN = '7d' ;
19+ const OTP_MAX_ATTEMPTS = 5 ;
1520
21+ async function createAndStoreOtp ( userId ) {
22+ const otp = crypto . randomInt ( 100000 , 1000000 ) . toString ( ) ;
23+
24+ // Delete any existing OTP for this user
25+ await otpSchema . deleteOne ( { userId } ) ;
26+
27+ // Hash OTP before storing
28+ const salt = await bcrypt . genSalt ( 10 ) ;
29+ const hashedOtp = await bcrypt . hash ( otp , salt ) ;
30+
31+ await new otpSchema ( { userId, otp : hashedOtp } ) . save ( ) ;
32+ return otp ;
33+ }
34+
35+ // HELPER: Validate OTP with attempt tracking
36+ async function validateOtp ( userId , passedOtp ) {
37+ const otpDoc = await otpSchema . findOne ( { userId } ) ;
38+ if ( ! otpDoc ) throw { status : 400 , message : "No OTP found. Please request a new one." } ;
39+
40+ if ( otpDoc . attempts >= OTP_MAX_ATTEMPTS ) {
41+ await otpDoc . deleteOne ( ) ;
42+ throw { status : 429 , message : "Too many incorrect attempts. Please request a new OTP." } ;
43+ }
44+
45+ const isMatch = await bcrypt . compare ( passedOtp . toString ( ) , otpDoc . otp ) ;
46+ if ( ! isMatch ) {
47+ otpDoc . attempts += 1 ;
48+ await otpDoc . save ( ) ;
49+ const remaining = OTP_MAX_ATTEMPTS - otpDoc . attempts ;
50+ throw { status : 400 , message : `Incorrect OTP. ${ remaining } attempt(s) remaining.` } ;
51+ }
52+
53+ return otpDoc ;
54+ }
1655
1756module . exports . register = async ( req , res ) => {
1857 try {
@@ -47,19 +86,20 @@ module.exports.login = async (req, res) => {
4786 const validPass = await bcrypt . compare ( password , dev . password ) ;
4887 if ( ! validPass ) return res . status ( 400 ) . json ( { error : "Invalid password" } ) ;
4988
50- const token = jwt . sign ( { _id : dev . _id , isVerified : dev . isVerified } , process . env . JWT_SECRET ) ;
89+ // FIX 1: JWT now expires in 7 days
90+ const token = jwt . sign (
91+ { _id : dev . _id , isVerified : dev . isVerified } ,
92+ process . env . JWT_SECRET ,
93+ { expiresIn : JWT_EXPIRES_IN }
94+ ) ;
5195 res . json ( { token } ) ;
5296 } catch ( err ) {
53- console . log ( "LOGIN ERROR CAUGHT:" , err . message ) ;
54- console . log ( "Is ZodError?" , err instanceof z . ZodError ) ;
55-
5697 if ( err instanceof z . ZodError ) {
5798 return res . status ( 400 ) . json ( {
5899 error : "Validation Failed" ,
59100 details : err . issues
60101 } ) ;
61102 }
62-
63103 console . error ( "Server Error:" , err ) ;
64104 res . status ( 500 ) . json ( { error : "Internal Server Error" } ) ;
65105 }
@@ -114,55 +154,95 @@ module.exports.deleteAccount = async (req, res) => {
114154module . exports . sendOtp = async ( req , res ) => {
115155 try {
116156 const { email } = onlyEmailSchema . parse ( req . body ) ;
117- const otp = Math . floor ( 100000 + Math . random ( ) * 900000 ) ;
118- // Secure: OTP logging removed
157+
119158 const existingUser = await Developer . findOne ( { email } ) ;
120159 if ( ! existingUser ) return res . status ( 400 ) . json ( { error : "User not found" } ) ;
121160
122161 if ( existingUser . isVerified ) return res . status ( 400 ) . json ( { error : "User already verified" } ) ;
123162
124- const existingOtp = await otpSchema . findOne ( { userId : existingUser . _id } ) ;
125- if ( existingOtp ) {
126- await existingOtp . deleteOne ( ) ;
127- }
128- const newOtp = new otpSchema (
129- {
130- userId : existingUser . _id ,
131- otp
132- } ) ;
133- newOtp . save ( ) ;
134- await sendOtp ( email , otp ) ;
163+ const otp = await createAndStoreOtp ( existingUser . _id ) ;
164+
165+ await sendOtp ( email , otp ) ; // Send raw OTP to user's email
135166 res . json ( { message : "OTP sent successfully" } ) ;
136167 } catch ( err ) {
168+ if ( err instanceof z . ZodError ) return res . status ( 400 ) . json ( { error : err . errors } ) ;
137169 console . error ( err ) ;
138170 res . status ( 500 ) . json ( { error : "Internal Server Error" } ) ;
139171 }
140172}
141173
142174
143-
144175module . exports . verifyOtp = async ( req , res ) => {
145176 try {
146- const { email } = onlyEmailSchema . parse ( req . body ) ;
147- const { otp } = req . body ;
148- const existingUser = await Developer . findOne ( { email } ) . sort ( { createdAt : - 1 } ) ;
149- if ( ! existingUser ) return res . status ( 400 ) . json ( { error : "User not found" } ) ;
177+ const { email, otp } = verifyOtpSchema . parse ( req . body ) ;
150178
151- const existingOtp = await otpSchema . findOne ( { userId : existingUser . _id } ) ;
152- if ( ! existingOtp ) return res . status ( 400 ) . json ( { error : "You havn't requested an OTP " } ) ;
179+ const existingUser = await Developer . findOne ( { email } ) ;
180+ if ( ! existingUser ) return res . status ( 400 ) . json ( { error : "User not found " } ) ;
153181
154- // Secure: OTP verification logging removed
155- if ( existingOtp . otp !== otp ) return res . status ( 400 ) . json ( { error : "Incorrect OTP" } ) ;
182+ const otpDoc = await validateOtp ( existingUser . _id , otp ) ;
156183
157- await existingOtp . deleteOne ( ) ;
184+ await otpDoc . deleteOne ( ) ;
158185 existingUser . isVerified = true ;
159186 await existingUser . save ( ) ;
160187
161- // Generate new token with isVerified: true
162- const token = jwt . sign ( { _id : existingUser . _id , isVerified : existingUser . isVerified } , process . env . JWT_SECRET ) ;
188+ // FIX 1: JWT with expiry
189+ const token = jwt . sign (
190+ { _id : existingUser . _id , isVerified : true } ,
191+ process . env . JWT_SECRET ,
192+ { expiresIn : JWT_EXPIRES_IN }
193+ ) ;
163194
164195 res . status ( 200 ) . json ( { message : "OTP verified successfully" , token } ) ;
165196 } catch ( err ) {
197+ if ( err . status ) return res . status ( err . status ) . json ( { error : err . message } ) ;
198+ if ( err instanceof z . ZodError ) return res . status ( 400 ) . json ( { error : err . errors } ) ;
199+ console . error ( err ) ;
200+ res . status ( 500 ) . json ( { error : "Internal Server Error" } ) ;
201+ }
202+ }
203+
204+
205+ // FIX 5: Forgot Password — generate + send reset OTP
206+ module . exports . forgotPassword = async ( req , res ) => {
207+ try {
208+ const { email } = onlyEmailSchema . parse ( req . body ) ;
209+
210+ const dev = await Developer . findOne ( { email } ) ;
211+ // Return same message regardless of whether email exists (prevents user enumeration)
212+ if ( ! dev ) return res . status ( 200 ) . json ( { message : "If this email is registered, an OTP has been sent." } ) ;
213+
214+ const otp = await createAndStoreOtp ( dev . _id ) ;
215+
216+ await sendOtp ( email , otp , { subject : "Password Reset OTP — urBackend" } ) ;
217+ res . status ( 200 ) . json ( { message : "If this email is registered, an OTP has been sent." } ) ;
218+ } catch ( err ) {
219+ if ( err instanceof z . ZodError ) return res . status ( 400 ) . json ( { error : err . errors } ) ;
220+ console . error ( err ) ;
221+ res . status ( 500 ) . json ( { error : "Internal Server Error" } ) ;
222+ }
223+ }
224+
225+
226+ // FIX 5: Reset Password — verify OTP then set new password
227+ module . exports . resetPassword = async ( req , res ) => {
228+ try {
229+ const { email, otp, newPassword } = resetPasswordSchema . parse ( req . body ) ;
230+
231+ const dev = await Developer . findOne ( { email } ) ;
232+ if ( ! dev ) return res . status ( 400 ) . json ( { error : "User not found" } ) ;
233+
234+ const otpDoc = await validateOtp ( dev . _id , otp ) ;
235+
236+ // OTP matched — update password
237+ await otpDoc . deleteOne ( ) ;
238+ const salt = await bcrypt . genSalt ( 10 ) ;
239+ dev . password = await bcrypt . hash ( newPassword , salt ) ;
240+ await dev . save ( ) ;
241+
242+ res . status ( 200 ) . json ( { message : "Password reset successfully. Please log in with your new password." } ) ;
243+ } catch ( err ) {
244+ if ( err . status ) return res . status ( err . status ) . json ( { error : err . message } ) ;
245+ if ( err instanceof z . ZodError ) return res . status ( 400 ) . json ( { error : err . errors } ) ;
166246 console . error ( err ) ;
167247 res . status ( 500 ) . json ( { error : "Internal Server Error" } ) ;
168248 }
0 commit comments