strapi-plugin-magic-sessionmanager 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -349,7 +349,7 @@ Log: "Cleaned up X inactive sessions"
349
349
  2. Inactivity timeout triggers cleanup
350
350
  3. Admin terminates the session
351
351
 
352
- #### Refresh Tokens ⚠️ **Critical Limitation**
352
+ #### Refresh Tokens **SOLVED!**
353
353
 
354
354
  **What are Refresh Tokens?**
355
355
  Refresh tokens allow users to get new Access Tokens (JWTs) without re-entering credentials. This enables longer sessions:
@@ -367,78 +367,100 @@ Strapi issues new JWT
367
367
  User continues without re-login
368
368
  ```
369
369
 
370
- **The Problem:**
371
- - **Stored:** NO - Refresh tokens are NOT tracked by this plugin
372
- - **Reason:** Strapi v5 handles refresh tokens in `users-permissions` plugin
373
- - **Impact:** User can bypass session termination! ⚠️
370
+ **The Solution (v3.2+):**
371
+ - **Stored:** YES - Refresh tokens are encrypted and stored with sessions ✅
372
+ - **Tracked:** YES - Middleware intercepts `/api/auth/refresh-token` requests ✅
373
+ - **Validated:** YES - Checks if session is still active before issuing new tokens ✅
374
+
375
+ **How It Works:**
374
376
 
375
- **Scenario:**
376
377
  ```
377
- Admin terminates user's session
378
+ Login: User gets JWT + Refresh Token
379
+
380
+ Both tokens encrypted and stored in session
381
+
382
+ Admin terminates session
383
+
384
+ Session: isActive = false ❌
385
+
386
+ User tries to refresh token:
387
+ POST /api/auth/refresh-token
388
+ { refreshToken: "..." }
378
389
 
379
- Session Manager: isActive = false ❌
390
+ [Refresh Token Middleware]
380
391
 
381
- User's JWT still valid OR
382
- User has refresh token
392
+ Decrypt all active session refresh tokens
383
393
 
384
- User gets new JWT via refresh token
394
+ Find matching session
385
395
 
386
- Plugin creates NEW session automatically! ⚠️
396
+ Session found but isActive = false?
397
+ → BLOCK! Return 401 Unauthorized ❌
398
+ → Message: "Session terminated. Please login again."
387
399
 
388
- User is "logged in" again despite termination
400
+ Session found and isActive = true?
401
+ → ALLOW! ✅
402
+ → Strapi issues new tokens
403
+ → Session updated with new encrypted tokens
389
404
  ```
390
405
 
391
- **Current Limitation:**
392
- This plugin **cannot prevent** users with valid refresh tokens from getting new JWTs. The session termination only affects the current JWT token.
406
+ **Security Benefits:**
393
407
 
394
- **Workarounds:**
408
+ **Session termination is FINAL** - User cannot get new tokens
409
+ ✅ **Refresh tokens tracked** - Encrypted & stored securely
410
+ ✅ **Token rotation** - New tokens automatically updated in session
411
+ ✅ **Admin control** - Force logout works even with refresh tokens
412
+
413
+ **Configuration:**
414
+
415
+ Enable refresh tokens in Strapi:
395
416
 
396
- **Option 1: Disable Refresh Tokens (Strict Control)**
397
417
  ```typescript
398
418
  // src/config/plugins.ts
399
419
  export default () => ({
400
420
  'users-permissions': {
401
421
  config: {
402
- jwt: {
403
- expiresIn: '30m',
404
- // Don't issue refresh tokens
422
+ jwtManagement: 'refresh', // Enable refresh tokens
423
+ sessions: {
424
+ accessTokenLifespan: 3600, // 1 hour (in seconds)
425
+ maxRefreshTokenLifespan: 2592000, // 30 days
426
+ idleRefreshTokenLifespan: 604800, // 7 days idle
405
427
  },
406
428
  },
407
429
  },
408
430
  'magic-sessionmanager': {
431
+ enabled: true,
409
432
  config: {
410
- inactivityTimeout: 15 * 60 * 1000, // < JWT expiration
433
+ inactivityTimeout: 15 * 60 * 1000, // 15 minutes
411
434
  },
412
435
  },
413
436
  });
414
437
  ```
415
438
 
416
- **Option 2: Short Refresh Token Expiry**
417
- ```typescript
418
- 'users-permissions': {
419
- config: {
420
- jwt: {
421
- expiresIn: '15m', // Short Access Token
422
- refreshExpiresIn: '30m', // Short Refresh Token
423
- },
424
- },
425
- }
426
- ```
439
+ **Testing Refresh Token Blocking:**
427
440
 
428
- **Option 3: Accept the Limitation**
429
- Understand that:
430
- - Session termination stops the **current** JWT
431
- - Users with refresh tokens can get **new** JWTs
432
- - New JWTs create **new** sessions
433
- - Use session analytics to detect unusual patterns
441
+ ```bash
442
+ # 1. Login and get tokens
443
+ curl -X POST http://localhost:1337/api/auth/local \
444
+ -H "Content-Type: application/json" \
445
+ -d '{"identifier":"user@example.com","password":"pass"}'
434
446
 
435
- **Future Enhancement:**
436
- To fully block users, the plugin would need to:
437
- 1. Track refresh tokens (complex)
438
- 2. Hook into Strapi's token refresh endpoint
439
- 3. Validate against active sessions before issuing new JWTs
447
+ # Save both tokens:
448
+ ACCESS_TOKEN="eyJhbGci..."
449
+ REFRESH_TOKEN="abc123..."
450
+
451
+ # 2. Admin terminates session
452
+ # Go to Admin → Sessions → Find session → Terminate
453
+
454
+ # 3. Try to refresh token
455
+ curl -X POST http://localhost:1337/api/auth/refresh-token \
456
+ -H "Content-Type: application/json" \
457
+ -d "{\"refreshToken\":\"$REFRESH_TOKEN\"}"
458
+
459
+ # Expected: 401 Unauthorized
460
+ # "Session terminated. Please login again."
461
+ ```
440
462
 
441
- This is planned for a future version.
463
+ **This completely solves the refresh token security gap!** 🔒
442
464
 
443
465
  ### Multi-Login Behavior
444
466
 
@@ -350,8 +350,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
350
350
  userId: user.id,
351
351
  ip,
352
352
  userAgent,
353
- token: ctx.body.jwt
354
- // Store JWT token reference
353
+ token: ctx.body.jwt,
354
+ // Store Access Token (encrypted)
355
+ refreshToken: ctx.body.refreshToken
356
+ // Store Refresh Token (encrypted) if exists
355
357
  });
356
358
  strapi2.log.info(`[magic-sessionmanager] ✅ Session created for user ${user.id} (IP: ${ip})`);
357
359
  if (geoData && (config2.enableEmailAlerts || config2.enableWebhooks)) {
@@ -396,6 +398,84 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
396
398
  }
397
399
  });
398
400
  strapi2.log.info("[magic-sessionmanager] ✅ Login/Logout interceptor middleware mounted");
401
+ strapi2.server.use(async (ctx, next) => {
402
+ const isRefreshToken = ctx.path === "/api/auth/refresh-token" && ctx.method === "POST";
403
+ if (isRefreshToken) {
404
+ try {
405
+ const refreshToken = ctx.request.body?.refreshToken;
406
+ if (refreshToken) {
407
+ const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
408
+ filters: {
409
+ isActive: true
410
+ }
411
+ });
412
+ const matchingSession = allSessions.find((session2) => {
413
+ if (!session2.refreshToken) return false;
414
+ try {
415
+ const decrypted = decryptToken$2(session2.refreshToken);
416
+ return decrypted === refreshToken;
417
+ } catch (err) {
418
+ return false;
419
+ }
420
+ });
421
+ if (!matchingSession) {
422
+ strapi2.log.warn("[magic-sessionmanager] 🚫 Blocked refresh token request - no active session");
423
+ ctx.status = 401;
424
+ ctx.body = {
425
+ error: {
426
+ status: 401,
427
+ message: "Session terminated. Please login again.",
428
+ name: "UnauthorizedError"
429
+ }
430
+ };
431
+ return;
432
+ }
433
+ strapi2.log.info(`[magic-sessionmanager] ✅ Refresh token allowed for session ${matchingSession.id}`);
434
+ }
435
+ } catch (err) {
436
+ strapi2.log.error("[magic-sessionmanager] Error checking refresh token:", err);
437
+ }
438
+ }
439
+ await next();
440
+ if (isRefreshToken && ctx.status === 200 && ctx.body && ctx.body.jwt) {
441
+ try {
442
+ const oldRefreshToken = ctx.request.body?.refreshToken;
443
+ const newAccessToken = ctx.body.jwt;
444
+ const newRefreshToken = ctx.body.refreshToken;
445
+ if (oldRefreshToken) {
446
+ const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
447
+ filters: {
448
+ isActive: true
449
+ }
450
+ });
451
+ const matchingSession = allSessions.find((session2) => {
452
+ if (!session2.refreshToken) return false;
453
+ try {
454
+ const decrypted = decryptToken$2(session2.refreshToken);
455
+ return decrypted === oldRefreshToken;
456
+ } catch (err) {
457
+ return false;
458
+ }
459
+ });
460
+ if (matchingSession) {
461
+ const encryptedToken = newAccessToken ? encryptToken$1(newAccessToken) : matchingSession.token;
462
+ const encryptedRefreshToken = newRefreshToken ? encryptToken$1(newRefreshToken) : matchingSession.refreshToken;
463
+ await strapi2.entityService.update("plugin::magic-sessionmanager.session", matchingSession.id, {
464
+ data: {
465
+ token: encryptedToken,
466
+ refreshToken: encryptedRefreshToken,
467
+ lastActive: /* @__PURE__ */ new Date()
468
+ }
469
+ });
470
+ strapi2.log.info(`[magic-sessionmanager] 🔄 Tokens refreshed for session ${matchingSession.id}`);
471
+ }
472
+ }
473
+ } catch (err) {
474
+ strapi2.log.error("[magic-sessionmanager] Error updating refreshed tokens:", err);
475
+ }
476
+ }
477
+ });
478
+ strapi2.log.info("[magic-sessionmanager] ✅ Refresh Token interceptor middleware mounted");
399
479
  strapi2.server.use(
400
480
  lastSeen({ strapi: strapi2, sessionService })
401
481
  );
@@ -480,6 +560,10 @@ const attributes = {
480
560
  type: "text",
481
561
  "private": true
482
562
  },
563
+ refreshToken: {
564
+ type: "text",
565
+ "private": true
566
+ },
483
567
  loginTime: {
484
568
  type: "datetime",
485
569
  required: true
@@ -760,14 +844,16 @@ var session$3 = {
760
844
  },
761
845
  /**
762
846
  * Get user's sessions
763
- * GET /magic-sessionmanager/user/:userId/sessions
764
- * SECURITY: User can only access their own sessions
847
+ * GET /magic-sessionmanager/user/:userId/sessions (Admin API)
848
+ * GET /api/magic-sessionmanager/user/:userId/sessions (Content API)
849
+ * SECURITY: Admins can view any user, Content API users only their own
765
850
  */
766
851
  async getUserSessions(ctx) {
767
852
  try {
768
853
  const { userId } = ctx.params;
854
+ const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
769
855
  const requestingUserId = ctx.state.user?.id;
770
- if (requestingUserId && String(requestingUserId) !== String(userId)) {
856
+ if (!isAdminRequest && requestingUserId && String(requestingUserId) !== String(userId)) {
771
857
  strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserId} tried to access sessions of user ${userId}`);
772
858
  return ctx.forbidden("You can only access your own sessions");
773
859
  }
@@ -1315,14 +1401,15 @@ const { encryptToken, decryptToken, generateSessionId } = encryption;
1315
1401
  var session$1 = ({ strapi: strapi2 }) => ({
1316
1402
  /**
1317
1403
  * Create a new session record
1318
- * @param {Object} params - { userId, ip, userAgent, token }
1404
+ * @param {Object} params - { userId, ip, userAgent, token, refreshToken }
1319
1405
  * @returns {Promise<Object>} Created session
1320
1406
  */
1321
- async createSession({ userId, ip = "unknown", userAgent = "unknown", token }) {
1407
+ async createSession({ userId, ip = "unknown", userAgent = "unknown", token, refreshToken }) {
1322
1408
  try {
1323
1409
  const now = /* @__PURE__ */ new Date();
1324
1410
  const sessionId = generateSessionId(userId);
1325
1411
  const encryptedToken = token ? encryptToken(token) : null;
1412
+ const encryptedRefreshToken = refreshToken ? encryptToken(refreshToken) : null;
1326
1413
  const session2 = await strapi2.entityService.create("plugin::magic-sessionmanager.session", {
1327
1414
  data: {
1328
1415
  user: userId,
@@ -1332,7 +1419,9 @@ var session$1 = ({ strapi: strapi2 }) => ({
1332
1419
  lastActive: now,
1333
1420
  isActive: true,
1334
1421
  token: encryptedToken,
1335
- // ✅ Encrypted JWT for security
1422
+ // ✅ Encrypted Access Token
1423
+ refreshToken: encryptedRefreshToken,
1424
+ // ✅ Encrypted Refresh Token
1336
1425
  sessionId
1337
1426
  // ✅ Unique identifier
1338
1427
  }
@@ -346,8 +346,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
346
346
  userId: user.id,
347
347
  ip,
348
348
  userAgent,
349
- token: ctx.body.jwt
350
- // Store JWT token reference
349
+ token: ctx.body.jwt,
350
+ // Store Access Token (encrypted)
351
+ refreshToken: ctx.body.refreshToken
352
+ // Store Refresh Token (encrypted) if exists
351
353
  });
352
354
  strapi2.log.info(`[magic-sessionmanager] ✅ Session created for user ${user.id} (IP: ${ip})`);
353
355
  if (geoData && (config2.enableEmailAlerts || config2.enableWebhooks)) {
@@ -392,6 +394,84 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
392
394
  }
393
395
  });
394
396
  strapi2.log.info("[magic-sessionmanager] ✅ Login/Logout interceptor middleware mounted");
397
+ strapi2.server.use(async (ctx, next) => {
398
+ const isRefreshToken = ctx.path === "/api/auth/refresh-token" && ctx.method === "POST";
399
+ if (isRefreshToken) {
400
+ try {
401
+ const refreshToken = ctx.request.body?.refreshToken;
402
+ if (refreshToken) {
403
+ const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
404
+ filters: {
405
+ isActive: true
406
+ }
407
+ });
408
+ const matchingSession = allSessions.find((session2) => {
409
+ if (!session2.refreshToken) return false;
410
+ try {
411
+ const decrypted = decryptToken$2(session2.refreshToken);
412
+ return decrypted === refreshToken;
413
+ } catch (err) {
414
+ return false;
415
+ }
416
+ });
417
+ if (!matchingSession) {
418
+ strapi2.log.warn("[magic-sessionmanager] 🚫 Blocked refresh token request - no active session");
419
+ ctx.status = 401;
420
+ ctx.body = {
421
+ error: {
422
+ status: 401,
423
+ message: "Session terminated. Please login again.",
424
+ name: "UnauthorizedError"
425
+ }
426
+ };
427
+ return;
428
+ }
429
+ strapi2.log.info(`[magic-sessionmanager] ✅ Refresh token allowed for session ${matchingSession.id}`);
430
+ }
431
+ } catch (err) {
432
+ strapi2.log.error("[magic-sessionmanager] Error checking refresh token:", err);
433
+ }
434
+ }
435
+ await next();
436
+ if (isRefreshToken && ctx.status === 200 && ctx.body && ctx.body.jwt) {
437
+ try {
438
+ const oldRefreshToken = ctx.request.body?.refreshToken;
439
+ const newAccessToken = ctx.body.jwt;
440
+ const newRefreshToken = ctx.body.refreshToken;
441
+ if (oldRefreshToken) {
442
+ const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
443
+ filters: {
444
+ isActive: true
445
+ }
446
+ });
447
+ const matchingSession = allSessions.find((session2) => {
448
+ if (!session2.refreshToken) return false;
449
+ try {
450
+ const decrypted = decryptToken$2(session2.refreshToken);
451
+ return decrypted === oldRefreshToken;
452
+ } catch (err) {
453
+ return false;
454
+ }
455
+ });
456
+ if (matchingSession) {
457
+ const encryptedToken = newAccessToken ? encryptToken$1(newAccessToken) : matchingSession.token;
458
+ const encryptedRefreshToken = newRefreshToken ? encryptToken$1(newRefreshToken) : matchingSession.refreshToken;
459
+ await strapi2.entityService.update("plugin::magic-sessionmanager.session", matchingSession.id, {
460
+ data: {
461
+ token: encryptedToken,
462
+ refreshToken: encryptedRefreshToken,
463
+ lastActive: /* @__PURE__ */ new Date()
464
+ }
465
+ });
466
+ strapi2.log.info(`[magic-sessionmanager] 🔄 Tokens refreshed for session ${matchingSession.id}`);
467
+ }
468
+ }
469
+ } catch (err) {
470
+ strapi2.log.error("[magic-sessionmanager] Error updating refreshed tokens:", err);
471
+ }
472
+ }
473
+ });
474
+ strapi2.log.info("[magic-sessionmanager] ✅ Refresh Token interceptor middleware mounted");
395
475
  strapi2.server.use(
396
476
  lastSeen({ strapi: strapi2, sessionService })
397
477
  );
@@ -476,6 +556,10 @@ const attributes = {
476
556
  type: "text",
477
557
  "private": true
478
558
  },
559
+ refreshToken: {
560
+ type: "text",
561
+ "private": true
562
+ },
479
563
  loginTime: {
480
564
  type: "datetime",
481
565
  required: true
@@ -756,14 +840,16 @@ var session$3 = {
756
840
  },
757
841
  /**
758
842
  * Get user's sessions
759
- * GET /magic-sessionmanager/user/:userId/sessions
760
- * SECURITY: User can only access their own sessions
843
+ * GET /magic-sessionmanager/user/:userId/sessions (Admin API)
844
+ * GET /api/magic-sessionmanager/user/:userId/sessions (Content API)
845
+ * SECURITY: Admins can view any user, Content API users only their own
761
846
  */
762
847
  async getUserSessions(ctx) {
763
848
  try {
764
849
  const { userId } = ctx.params;
850
+ const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
765
851
  const requestingUserId = ctx.state.user?.id;
766
- if (requestingUserId && String(requestingUserId) !== String(userId)) {
852
+ if (!isAdminRequest && requestingUserId && String(requestingUserId) !== String(userId)) {
767
853
  strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserId} tried to access sessions of user ${userId}`);
768
854
  return ctx.forbidden("You can only access your own sessions");
769
855
  }
@@ -1311,14 +1397,15 @@ const { encryptToken, decryptToken, generateSessionId } = encryption;
1311
1397
  var session$1 = ({ strapi: strapi2 }) => ({
1312
1398
  /**
1313
1399
  * Create a new session record
1314
- * @param {Object} params - { userId, ip, userAgent, token }
1400
+ * @param {Object} params - { userId, ip, userAgent, token, refreshToken }
1315
1401
  * @returns {Promise<Object>} Created session
1316
1402
  */
1317
- async createSession({ userId, ip = "unknown", userAgent = "unknown", token }) {
1403
+ async createSession({ userId, ip = "unknown", userAgent = "unknown", token, refreshToken }) {
1318
1404
  try {
1319
1405
  const now = /* @__PURE__ */ new Date();
1320
1406
  const sessionId = generateSessionId(userId);
1321
1407
  const encryptedToken = token ? encryptToken(token) : null;
1408
+ const encryptedRefreshToken = refreshToken ? encryptToken(refreshToken) : null;
1322
1409
  const session2 = await strapi2.entityService.create("plugin::magic-sessionmanager.session", {
1323
1410
  data: {
1324
1411
  user: userId,
@@ -1328,7 +1415,9 @@ var session$1 = ({ strapi: strapi2 }) => ({
1328
1415
  lastActive: now,
1329
1416
  isActive: true,
1330
1417
  token: encryptedToken,
1331
- // ✅ Encrypted JWT for security
1418
+ // ✅ Encrypted Access Token
1419
+ refreshToken: encryptedRefreshToken,
1420
+ // ✅ Encrypted Refresh Token
1332
1421
  sessionId
1333
1422
  // ✅ Unique identifier
1334
1423
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.2.0",
2
+ "version": "3.3.0",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",
@@ -235,7 +235,8 @@ module.exports = async ({ strapi }) => {
235
235
  userId: user.id,
236
236
  ip,
237
237
  userAgent,
238
- token: ctx.body.jwt, // Store JWT token reference
238
+ token: ctx.body.jwt, // Store Access Token (encrypted)
239
+ refreshToken: ctx.body.refreshToken, // Store Refresh Token (encrypted) if exists
239
240
  });
240
241
 
241
242
  strapi.log.info(`[magic-sessionmanager] ✅ Session created for user ${user.id} (IP: ${ip})`);
@@ -293,6 +294,107 @@ module.exports = async ({ strapi }) => {
293
294
 
294
295
  strapi.log.info('[magic-sessionmanager] ✅ Login/Logout interceptor middleware mounted');
295
296
 
297
+ // Middleware to block refresh token requests for terminated sessions
298
+ strapi.server.use(async (ctx, next) => {
299
+ // Check if this is a refresh token request
300
+ const isRefreshToken = ctx.path === '/api/auth/refresh-token' && ctx.method === 'POST';
301
+
302
+ if (isRefreshToken) {
303
+ try {
304
+ const refreshToken = ctx.request.body?.refreshToken;
305
+
306
+ if (refreshToken) {
307
+ // Find session with this refresh token
308
+ const allSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
309
+ filters: {
310
+ isActive: true,
311
+ },
312
+ });
313
+
314
+ // Find matching session by decrypting and comparing refresh tokens
315
+ const matchingSession = allSessions.find(session => {
316
+ if (!session.refreshToken) return false;
317
+ try {
318
+ const decrypted = decryptToken(session.refreshToken);
319
+ return decrypted === refreshToken;
320
+ } catch (err) {
321
+ return false;
322
+ }
323
+ });
324
+
325
+ if (!matchingSession) {
326
+ // No active session with this refresh token → Block!
327
+ strapi.log.warn('[magic-sessionmanager] 🚫 Blocked refresh token request - no active session');
328
+ ctx.status = 401;
329
+ ctx.body = {
330
+ error: {
331
+ status: 401,
332
+ message: 'Session terminated. Please login again.',
333
+ name: 'UnauthorizedError'
334
+ }
335
+ };
336
+ return; // Don't continue
337
+ }
338
+
339
+ strapi.log.info(`[magic-sessionmanager] ✅ Refresh token allowed for session ${matchingSession.id}`);
340
+ }
341
+ } catch (err) {
342
+ strapi.log.error('[magic-sessionmanager] Error checking refresh token:', err);
343
+ // On error, allow request to continue (fail-open for availability)
344
+ }
345
+ }
346
+
347
+ // Continue with request
348
+ await next();
349
+
350
+ // AFTER: If refresh token response was successful, update session with new tokens
351
+ if (isRefreshToken && ctx.status === 200 && ctx.body && ctx.body.jwt) {
352
+ try {
353
+ const oldRefreshToken = ctx.request.body?.refreshToken;
354
+ const newAccessToken = ctx.body.jwt;
355
+ const newRefreshToken = ctx.body.refreshToken;
356
+
357
+ if (oldRefreshToken) {
358
+ // Find session and update with new tokens
359
+ const allSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
360
+ filters: {
361
+ isActive: true,
362
+ },
363
+ });
364
+
365
+ const matchingSession = allSessions.find(session => {
366
+ if (!session.refreshToken) return false;
367
+ try {
368
+ const decrypted = decryptToken(session.refreshToken);
369
+ return decrypted === oldRefreshToken;
370
+ } catch (err) {
371
+ return false;
372
+ }
373
+ });
374
+
375
+ if (matchingSession) {
376
+ const encryptedToken = newAccessToken ? encryptToken(newAccessToken) : matchingSession.token;
377
+ const encryptedRefreshToken = newRefreshToken ? encryptToken(newRefreshToken) : matchingSession.refreshToken;
378
+
379
+ await strapi.entityService.update('plugin::magic-sessionmanager.session', matchingSession.id, {
380
+ data: {
381
+ token: encryptedToken,
382
+ refreshToken: encryptedRefreshToken,
383
+ lastActive: new Date(),
384
+ },
385
+ });
386
+
387
+ strapi.log.info(`[magic-sessionmanager] 🔄 Tokens refreshed for session ${matchingSession.id}`);
388
+ }
389
+ }
390
+ } catch (err) {
391
+ strapi.log.error('[magic-sessionmanager] Error updating refreshed tokens:', err);
392
+ }
393
+ }
394
+ });
395
+
396
+ strapi.log.info('[magic-sessionmanager] ✅ Refresh Token interceptor middleware mounted');
397
+
296
398
  // Mount lastSeen update middleware
297
399
  strapi.server.use(
298
400
  require('./middlewares/last-seen')({ strapi, sessionService })
@@ -43,6 +43,10 @@
43
43
  "type": "text",
44
44
  "private": true
45
45
  },
46
+ "refreshToken": {
47
+ "type": "text",
48
+ "private": true
49
+ },
46
50
  "loginTime": {
47
51
  "type": "datetime",
48
52
  "required": true
@@ -57,16 +57,21 @@ module.exports = {
57
57
 
58
58
  /**
59
59
  * Get user's sessions
60
- * GET /magic-sessionmanager/user/:userId/sessions
61
- * SECURITY: User can only access their own sessions
60
+ * GET /magic-sessionmanager/user/:userId/sessions (Admin API)
61
+ * GET /api/magic-sessionmanager/user/:userId/sessions (Content API)
62
+ * SECURITY: Admins can view any user, Content API users only their own
62
63
  */
63
64
  async getUserSessions(ctx) {
64
65
  try {
65
66
  const { userId } = ctx.params;
67
+
68
+ // Check if this is an admin request
69
+ const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
66
70
  const requestingUserId = ctx.state.user?.id;
67
71
 
68
- // SECURITY CHECK: User can only see their own sessions
69
- if (requestingUserId && String(requestingUserId) !== String(userId)) {
72
+ // SECURITY CHECK: Content API users can only see their own sessions
73
+ // Admins can see any user's sessions
74
+ if (!isAdminRequest && requestingUserId && String(requestingUserId) !== String(userId)) {
70
75
  strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserId} tried to access sessions of user ${userId}`);
71
76
  return ctx.forbidden('You can only access your own sessions');
72
77
  }
@@ -17,18 +17,19 @@ const { encryptToken, decryptToken, generateSessionId } = require('../utils/encr
17
17
  module.exports = ({ strapi }) => ({
18
18
  /**
19
19
  * Create a new session record
20
- * @param {Object} params - { userId, ip, userAgent, token }
20
+ * @param {Object} params - { userId, ip, userAgent, token, refreshToken }
21
21
  * @returns {Promise<Object>} Created session
22
22
  */
23
- async createSession({ userId, ip = 'unknown', userAgent = 'unknown', token }) {
23
+ async createSession({ userId, ip = 'unknown', userAgent = 'unknown', token, refreshToken }) {
24
24
  try {
25
25
  const now = new Date();
26
26
 
27
27
  // Generate unique session ID
28
28
  const sessionId = generateSessionId(userId);
29
29
 
30
- // Encrypt JWT token before storing
30
+ // Encrypt JWT tokens before storing (both access and refresh)
31
31
  const encryptedToken = token ? encryptToken(token) : null;
32
+ const encryptedRefreshToken = refreshToken ? encryptToken(refreshToken) : null;
32
33
 
33
34
  const session = await strapi.entityService.create('plugin::magic-sessionmanager.session', {
34
35
  data: {
@@ -38,8 +39,9 @@ module.exports = ({ strapi }) => ({
38
39
  loginTime: now,
39
40
  lastActive: now,
40
41
  isActive: true,
41
- token: encryptedToken, // ✅ Encrypted JWT for security
42
- sessionId: sessionId, // ✅ Unique identifier
42
+ token: encryptedToken, // ✅ Encrypted Access Token
43
+ refreshToken: encryptedRefreshToken, // ✅ Encrypted Refresh Token
44
+ sessionId: sessionId, // ✅ Unique identifier
43
45
  },
44
46
  });
45
47