1010 * even though JWT tokens are stateless and cannot be invalidated directly.
1111 *
1212 * [SUCCESS] Migrated to strapi.documents() API (Strapi v5 Best Practice)
13+ * [FIX] Now updates activity on ALL requests with valid JWT, not just when ctx.state.user is set
1314 */
1415
1516const SESSION_UID = 'plugin::magic-sessionmanager.session' ;
1617const { decryptToken } = require ( '../utils/encryption' ) ;
1718
19+ // In-memory cache for rate limiting (per session)
20+ const lastTouchCache = new Map ( ) ;
21+
1822module . exports = ( { strapi, sessionService } ) => {
1923 return async ( ctx , next ) => {
24+ // Get JWT token from Authorization header
25+ const currentToken = ctx . request . headers . authorization ?. replace ( 'Bearer ' , '' ) ;
26+
27+ // Skip if no token provided
28+ if ( ! currentToken ) {
29+ await next ( ) ;
30+ return ;
31+ }
32+
33+ // Skip internal/admin routes that don't need session tracking
34+ const skipPaths = [ '/admin' , '/_health' , '/favicon.ico' ] ;
35+ if ( skipPaths . some ( p => ctx . path . startsWith ( p ) ) ) {
36+ await next ( ) ;
37+ return ;
38+ }
39+
40+ let matchingSession = null ;
41+ let userId = null ;
42+
2043 // BEFORE processing request: Validate the SPECIFIC session is still active
21- // Strapi v5: Use documentId instead of numeric id for Document Service API
22- if ( ctx . state . user && ctx . state . user . documentId ) {
23- try {
24- const userId = ctx . state . user . documentId ;
25- const currentToken = ctx . request . headers . authorization ?. replace ( 'Bearer ' , '' ) ;
26-
27- if ( ! currentToken ) {
28- // No token provided, let Strapi handle auth
29- await next ( ) ;
30- return ;
31- }
44+ try {
45+ // Try to get userId from ctx.state.user first (if already authenticated)
46+ if ( ctx . state . user && ctx . state . user . documentId ) {
47+ userId = ctx . state . user . documentId ;
3248
3349 // Get all active sessions for this user
3450 const activeSessions = await strapi . documents ( SESSION_UID ) . findMany ( {
@@ -45,7 +61,6 @@ module.exports = ({ strapi, sessionService }) => {
4561 }
4662
4763 // Find the session that matches this specific JWT token
48- let matchingSession = null ;
4964 for ( const session of activeSessions ) {
5065 if ( ! session . token ) continue ;
5166 try {
@@ -64,28 +79,65 @@ module.exports = ({ strapi, sessionService }) => {
6479 strapi . log . info ( `[magic-sessionmanager] [BLOCKED] Session for user ${ userId } has been terminated` ) ;
6580 return ctx . unauthorized ( 'This session has been terminated. Please login again.' ) ;
6681 }
82+ } else {
83+ // User not yet authenticated by Strapi - find session directly by token
84+ // This handles cases where JWT is valid but ctx.state.user isn't set yet
85+ const allActiveSessions = await strapi . documents ( SESSION_UID ) . findMany ( {
86+ filters : { isActive : true } ,
87+ populate : { user : { fields : [ 'documentId' ] } } ,
88+ limit : 500 , // Reasonable limit for performance
89+ } ) ;
6790
68- // Store the matching session ID for later use (touch, etc.)
91+ for ( const session of allActiveSessions ) {
92+ if ( ! session . token ) continue ;
93+ try {
94+ const decrypted = decryptToken ( session . token ) ;
95+ if ( decrypted === currentToken ) {
96+ matchingSession = session ;
97+ userId = session . user ?. documentId ;
98+ break ;
99+ }
100+ } catch ( err ) {
101+ // Ignore decryption errors
102+ }
103+ }
104+ }
105+
106+ // Store the matching session for later use
107+ if ( matchingSession ) {
69108 ctx . state . sessionId = matchingSession . documentId ;
70109 ctx . state . currentSession = matchingSession ;
71-
72- } catch ( err ) {
73- strapi . log . debug ( '[magic-sessionmanager] Error checking session:' , err . message ) ;
74- // On error, allow request to continue (fail-open for availability)
75110 }
111+
112+ } catch ( err ) {
113+ strapi . log . debug ( '[magic-sessionmanager] Error checking session:' , err . message ) ;
114+ // On error, allow request to continue (fail-open for availability)
76115 }
77116
78117 // Process request
79118 await next ( ) ;
80119
81- // AFTER response: Update activity timestamps if user is authenticated
82- if ( ctx . state . user && ctx . state . user . documentId && ctx . state . sessionId ) {
120+ // AFTER response: Update activity timestamps if we found a valid session
121+ if ( matchingSession ) {
83122 try {
84- // Call touch with rate limiting using the validated session ID
85- await sessionService . touch ( {
86- userId : ctx . state . user . documentId ,
87- sessionId : ctx . state . sessionId ,
88- } ) ;
123+ // Rate limiting: Check in-memory cache first (faster than DB)
124+ const config = strapi . config . get ( 'plugin::magic-sessionmanager' ) || { } ;
125+ const rateLimit = config . lastSeenRateLimit || 30000 ; // 30 seconds default
126+ const now = Date . now ( ) ;
127+ const lastTouch = lastTouchCache . get ( matchingSession . documentId ) || 0 ;
128+
129+ if ( now - lastTouch > rateLimit ) {
130+ // Update cache
131+ lastTouchCache . set ( matchingSession . documentId , now ) ;
132+
133+ // Update database
134+ await strapi . documents ( SESSION_UID ) . update ( {
135+ documentId : matchingSession . documentId ,
136+ data : { lastActive : new Date ( ) } ,
137+ } ) ;
138+
139+ strapi . log . debug ( `[magic-sessionmanager] [TOUCH] Session ${ matchingSession . documentId } activity updated` ) ;
140+ }
89141 } catch ( err ) {
90142 strapi . log . debug ( '[magic-sessionmanager] Error updating lastSeen:' , err . message ) ;
91143 }
0 commit comments