11import type { Request , Response , NextFunction } from "express" ;
22import bcrypt from "bcryptjs" ;
3- import jwt from "jsonwebtoken" ; // <-- ADDED
3+ import jwt from "jsonwebtoken" ;
44import User , { type IUser } from "../models/userModel.js" ;
55import {
6- generateAccessToken , // <-- RENAMED/UPDATED
7- generateRefreshToken , // <-- ADDED
6+ generateToken ,
7+ generateRefreshToken ,
88} from "../utils/generateToken.js" ;
99import { userSchema , loginSchema } from "../utils/validateInputs.js" ;
1010import dotenv from "dotenv" ;
11- import jwt from "jsonwebtoken" ;
1211import { Session } from "../models/sessionModel.js" ;
1312
1413dotenv . config ( ) ;
14+
1515const asTypedUser = ( user : any ) : IUser & { _id : string } =>
1616 user as IUser & { _id : string } ;
1717
18- // A helper function to send tokens
19- const sendTokens = ( res : Response , user : IUser & { _id : string } ) => {
20- const accessToken = generateAccessToken ( user . _id . toString ( ) ) ;
21- const newRefreshToken = generateRefreshToken ( user . _id . toString ( ) ) ;
22-
23- // Update user's refresh tokens in DB
24- // Only one refresh token per user is supported (single device).
25- user . refreshTokens = [ newRefreshToken ] ;
26-
27- // Set refresh token in secure httpOnly cookie
28- res . cookie ( "jwt" , newRefreshToken , {
29- httpOnly : true ,
30- secure : process . env . NODE_ENV !== "development" ,
31- sameSite : "strict" ,
32- maxAge : 7 * 24 * 60 * 60 * 1000 , // 7 days (matches token expiry)
33- } ) ;
18+ const cookieOptions = {
19+ httpOnly : true ,
20+ secure : process . env . NODE_ENV !== "development" ,
21+ sameSite : "strict" as const ,
22+ maxAge : 7 * 24 * 60 * 60 * 1000 , // 7 days
23+ } ;
24+
25+ async function createSessionForAccessToken ( userId : string , token : string ) {
26+ const decoded = jwt . decode ( token ) as { exp ?: number } | null ;
27+ const expiresAt = decoded ?. exp
28+ ? new Date ( decoded . exp * 1000 )
29+ : new Date ( Date . now ( ) + 15 * 60 * 1000 ) ;
3430
35- // Send access token in response body
36- res . json ( {
37- success : true ,
38- message : "Login successful" ,
39- accessToken : accessToken ,
31+ await Session . create ( {
32+ userId,
33+ token,
34+ expiresAt,
4035 } ) ;
41- } ;
36+ }
4237
43- // ✅ SIGNUP CONTROLLER (Updated)
38+ //signup controller
4439export const registerUser = async (
4540 req : Request ,
4641 res : Response ,
@@ -69,31 +64,29 @@ export const registerUser = async (
6964 const newUser = await User . create ( { name, email, password : hashedPassword } ) ;
7065 const typedUser = asTypedUser ( newUser ) ;
7166
72- const token = generateToken ( typedUser . _id . toString ( ) ) ;
73- const decoded = jwt . decode ( token ) as { exp ?: number } | null ;
67+ // Generate tokens
68+ const accessToken = generateToken ( typedUser . _id . toString ( ) ) ;
69+ const refreshToken = generateRefreshToken ( typedUser . _id . toString ( ) ) ;
7470
75- if ( ! decoded || ! decoded . exp ) {
76- throw new Error ( "Invalid token format or missing expiration" ) ;
77- }
71+ typedUser . refreshTokens = [ refreshToken ] ;
72+ await typedUser . save ( ) ;
7873
79- const expiresAt = new Date ( decoded . exp * 1000 ) ;
80- await Session . create ( {
81- userId : typedUser . _id ,
82- token,
83- expiresAt,
84- } ) ;
74+ res . cookie ( "jwt" , refreshToken , cookieOptions ) ;
75+
76+ //a session document
77+ await createSessionForAccessToken ( typedUser . _id . toString ( ) , accessToken ) ;
8578
8679 res . status ( 201 ) . json ( {
8780 success : true ,
8881 message : "User registered successfully" ,
89- token ,
82+ accessToken ,
9083 } ) ;
9184 } catch ( err ) {
9285 next ( err ) ;
9386 }
9487} ;
9588
96- // ✅ LOGIN CONTROLLER (Updated)
89+ //login controller
9790export const loginUser = async (
9891 req : Request ,
9992 res : Response ,
@@ -131,40 +124,31 @@ export const loginUser = async (
131124
132125 const typedUser = asTypedUser ( foundUser ) ;
133126
134- const token = generateToken ( typedUser . _id . toString ( ) ) ;
135- const decoded = jwt . verify ( token , process . env . JWT_SECRET ! ) as { exp ?: number } ;
136-
137- if ( ! decoded . exp ) {
138- throw new Error ( "Token missing expiration claim" ) ;
139- }
127+ // Generate tokens
128+ const accessToken = generateToken ( typedUser . _id . toString ( ) ) ;
129+ const refreshToken = generateRefreshToken ( typedUser . _id . toString ( ) ) ;
140130
141- const expiresAt = new Date ( decoded . exp * 1000 ) ;
131+ // Store refresh token
132+ typedUser . refreshTokens = [ refreshToken ] ;
133+ await typedUser . save ( ) ;
142134
143- await Session . create ( {
144- userId : typedUser . _id ,
145- token,
146- expiresAt,
147- } ) ;
135+ // Set cookie
136+ res . cookie ( "jwt" , refreshToken , cookieOptions ) ;
148137
138+ // Create session
139+ await createSessionForAccessToken ( typedUser . _id . toString ( ) , accessToken ) ;
149140
150- res . json ( {
141+ return res . json ( {
151142 success : true ,
152143 message : "Login successful" ,
153- token ,
144+ accessToken ,
154145 } ) ;
155-
156- // Redirect to the frontend without passing the token in the URL
157- const redirectUrl = `${ process . env . FRONTEND_URL } /auth-success` ;
158- res . redirect ( redirectUrl ) ;
159-
160146 } catch ( err ) {
161147 next ( err ) ;
162148 }
163149} ;
164150
165- // ... (keep all other functions as they are)
166-
167- // ✅ REFRESH TOKEN CONTROLLER (Updated with fixes)
151+ //refresh token controller
168152export const handleRefreshToken = async (
169153 req : Request ,
170154 res : Response ,
@@ -180,76 +164,64 @@ export const handleRefreshToken = async (
180164
181165 const refreshToken = cookies . jwt ;
182166 // Clear the old cookie immediately
183- res . clearCookie ( "jwt" , { httpOnly : true , sameSite : "strict" , secure : process . env . NODE_ENV !== "development" } ) ;
167+ res . clearCookie ( "jwt" , {
168+ httpOnly : true ,
169+ sameSite : "strict" ,
170+ secure : process . env . NODE_ENV !== "development" ,
171+ } ) ;
184172
185173 const foundUser = await User . findOne ( { refreshTokens : refreshToken } ) ;
186174
187- // Detected refresh token reuse!
188175 if ( ! foundUser ) {
189176 try {
190177 const decoded = jwt . verify (
191178 refreshToken ,
192179 process . env . JWT_REFRESH_SECRET as string
193180 ) as { id : string } ;
194181
195- // We know who the user is, now we hack-proof them
196- // by deleting all their refresh tokens
197182 const compromisedUser = await User . findById ( decoded . id ) ;
198183 if ( compromisedUser ) {
199184 compromisedUser . refreshTokens = [ ] ;
200185 await compromisedUser . save ( ) ;
201186 }
202187 } catch ( err ) {
203- // Token was invalid in the first place
188+ // nothing to do
204189 } finally {
205190 return res
206191 . status ( 403 )
207192 . json ( { success : false , message : "Forbidden, token reuse" } ) ;
208193 }
209194 }
210195
211- // Valid token, let's rotate it
212196 const typedUser = asTypedUser ( foundUser ) ;
213197
214198 try {
215- // Verify the token is still valid
199+ // Verify
216200 jwt . verify ( refreshToken , process . env . JWT_REFRESH_SECRET as string ) as {
217201 id : string ;
218202 } ;
219203
220204 // Generate new tokens
221- const newAccessToken = generateAccessToken ( typedUser . _id . toString ( ) ) ;
205+ const newAccessToken = generateToken ( typedUser . _id . toString ( ) ) ;
222206 const newRefreshToken = generateRefreshToken ( typedUser . _id . toString ( ) ) ;
223207
224- // ==================
225- // FIX #1
226- // ==================
227- // Filter out the old token and default to an empty array
228208 const otherRefreshTokens =
229209 typedUser . refreshTokens ?. filter ( ( rt ) => rt !== refreshToken ) || [ ] ;
230210
231- // Assign the new array (filtered list + new token)
232211 typedUser . refreshTokens = [ ...otherRefreshTokens , newRefreshToken ] ;
233212 await typedUser . save ( ) ;
234213
235- // Send new tokens
236- res . cookie ( "jwt" , newRefreshToken , {
237- httpOnly : true ,
238- secure : process . env . NODE_ENV !== "development" ,
239- sameSite : "strict" ,
240- maxAge : 7 * 24 * 60 * 60 * 1000 ,
241- } ) ;
214+ //new refresh cookie
215+ res . cookie ( "jwt" , newRefreshToken , cookieOptions ) ;
216+
217+ //new session
218+ await createSessionForAccessToken ( typedUser . _id . toString ( ) , newAccessToken ) ;
242219
243- res . json ( {
220+ return res . json ( {
244221 success : true ,
245222 accessToken : newAccessToken ,
246223 } ) ;
247224 } catch ( err ) {
248- // Token expired or invalid
249- // ==================
250- // FIX #2
251- // ==================
252- // Clear out the bad token, default to an empty array
253225 typedUser . refreshTokens =
254226 typedUser . refreshTokens ?. filter ( ( rt ) => rt !== refreshToken ) || [ ] ;
255227 await typedUser . save ( ) ;
@@ -263,49 +235,47 @@ export const handleRefreshToken = async (
263235 }
264236} ;
265237
266-
267- // ✅ LOGOUT CONTROLLER (Updated with fix)
238+ //logout controller
268239export const logoutUser = async (
269240 req : Request ,
270241 res : Response ,
271242 next : NextFunction
272243) => {
273244 try {
274245 const cookies = req . cookies ;
275- if ( ! cookies ?. jwt ) {
276- return res . sendStatus ( 204 ) ; // No cookie, already logged out
277- }
278-
279- const refreshToken = cookies . jwt ;
280-
281- // Find user and remove this specific refresh token
282- const foundUser = await User . findOne ( { refreshTokens : refreshToken } ) ;
283- if ( foundUser ) {
284- foundUser . refreshTokens =
285- foundUser . refreshTokens ?. filter ( ( rt ) => rt !== refreshToken ) || [ ] ;
246+ const authHeader = req . headers . authorization ;
247+ const accessToken = authHeader ?. split ( " " ) [ 1 ] ;
248+
249+ // If cookie exists- remove refresh token
250+ if ( cookies ?. jwt ) {
251+ const refreshToken = cookies . jwt ;
252+ const foundUser = await User . findOne ( { refreshTokens : refreshToken } ) ;
253+ if ( foundUser ) {
254+ foundUser . refreshTokens =
255+ foundUser . refreshTokens ?. filter ( ( rt ) => rt !== refreshToken ) || [ ] ;
256+ await foundUser . save ( ) ;
257+ }
286258
287- await foundUser . save ( ) ;
259+ res . clearCookie ( "jwt" , {
260+ httpOnly : true ,
261+ sameSite : "strict" ,
262+ secure : process . env . NODE_ENV !== "development" ,
263+ } ) ;
264+ }
265+ if ( accessToken ) {
266+ await Session . deleteOne ( { token : accessToken } ) ;
288267 }
289268
290- // Clear the cookie
291- res . clearCookie ( "jwt" , {
292- httpOnly : true ,
293- sameSite : "strict" ,
294- secure : process . env . NODE_ENV !== "development" ,
295- } ) ;
296-
297- res . status ( 200 ) . json ( { success : true , message : "Logged out successfully" } ) ;
269+ return res . status ( 200 ) . json ( { success : true , message : "Logged out successfully" } ) ;
298270 } catch ( err ) {
299271 next ( err ) ;
300272 }
301273} ;
302274
303- // ✅ GET PROFILE CONTROLLER (Unchanged, but for completeness)
275+ //get profile controller
304276export const getUserProfile = async ( req : Request , res : Response ) => {
305277 try {
306- // req.userId comes from the 'protect' middleware
307- // @ts -ignore
308- const user = await User . findById ( req . userId ) . select ( "-password -refreshTokens" ) ;
278+ const user = await User . findById ( ( req as any ) . userId ) . select ( "-password -refreshTokens" ) ;
309279
310280 if ( ! user ) {
311281 return res . status ( 404 ) . json ( {
0 commit comments