Skip to content

Commit 8887500

Browse files
committed
feat: add terminatedManually field for session reactivation
New behavior: - Session timeout (cleanup job) sets terminatedManually: false - Manual logout sets terminatedManually: true - On request with inactive session: - If terminatedManually: true → BLOCK (user logged out intentionally) - If terminatedManually: false → REACTIVATE session + update lastActive This allows users to resume their session after timeout without needing to login again, while still enforcing manual logouts. Changes: - Add terminatedManually field to session schema (default: false) - Update cleanup job to set terminatedManually: false - Update terminateSession to set terminatedManually: true - Update JWT verify, middleware, policy with reactivation logic
1 parent 4e14750 commit 8887500

5 files changed

Lines changed: 123 additions & 66 deletions

File tree

server/src/bootstrap.js

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ async function registerSessionAwareAuthStrategy(strapi, log) {
631631
return decoded;
632632
}
633633

634-
// Check for active sessions
634+
// Check for active sessions first
635635
const activeSessions = await strapi.documents(SESSION_UID).findMany({
636636
filters: {
637637
user: { documentId: userDocId },
@@ -645,48 +645,55 @@ async function registerSessionAwareAuthStrategy(strapi, log) {
645645
return decoded;
646646
}
647647

648-
// No active session found - check if ANY sessions exist (including inactive)
649-
const allSessions = await strapi.documents(SESSION_UID).findMany({
650-
filters: { user: { documentId: userDocId } },
648+
// No active session - check for inactive sessions
649+
const inactiveSessions = await strapi.documents(SESSION_UID).findMany({
650+
filters: {
651+
user: { documentId: userDocId },
652+
isActive: false,
653+
},
651654
limit: 5,
652-
fields: ['isActive', 'lastActive'],
655+
fields: ['documentId', 'terminatedManually', 'lastActive'],
656+
sort: [{ lastActive: 'desc' }],
653657
});
654658

655-
const totalSessions = allSessions?.length || 0;
656-
const hasInactiveSessions = allSessions?.some(s => s.isActive === false);
657-
658-
// DECISION LOGIC:
659-
// 1. User has inactive sessions = explicitly logged out → ALWAYS BLOCK
660-
// 2. No sessions exist at all = session never created (bug/race condition) → ALLOW
661-
// 3. strictMode controls whether missing sessions block or just warn
662-
663-
if (hasInactiveSessions) {
664-
// User was explicitly logged out → ALWAYS BLOCK (this is intentional!)
665-
strapi.log.info(
666-
`[magic-sessionmanager] [JWT-BLOCKED] User ${userDocId.substring(0, 8)}... was logged out (${totalSessions} inactive sessions)`
667-
);
668-
return null; // Block - user was explicitly logged out
669-
}
670-
671-
// No sessions exist at all - session was never created (bug/race condition)
672-
if (totalSessions === 0) {
673-
if (strictMode) {
674-
// Strict mode: Block even if no sessions (could be security risk)
659+
if (inactiveSessions && inactiveSessions.length > 0) {
660+
// Check if ANY session was manually terminated
661+
const manuallyTerminated = inactiveSessions.find(s => s.terminatedManually === true);
662+
663+
if (manuallyTerminated) {
664+
// User was explicitly logged out → BLOCK
675665
strapi.log.info(
676-
`[magic-sessionmanager] [JWT-BLOCKED] No sessions exist for user ${userDocId.substring(0, 8)}... (strictMode enabled)`
666+
`[magic-sessionmanager] [JWT-BLOCKED] User ${userDocId.substring(0, 8)}... was manually logged out`
677667
);
678668
return null;
679669
}
680-
// Non-strict mode: Allow through but warn
681-
strapi.log.warn(
682-
`[magic-sessionmanager] [JWT-WARN] No session found for user ${userDocId.substring(0, 8)}... (allowing - session may not have been created)`
670+
671+
// Session was deactivated by timeout → REACTIVATE most recent one
672+
const sessionToReactivate = inactiveSessions[0];
673+
await strapi.documents(SESSION_UID).update({
674+
documentId: sessionToReactivate.documentId,
675+
data: {
676+
isActive: true,
677+
lastActive: new Date(),
678+
},
679+
});
680+
strapi.log.info(
681+
`[magic-sessionmanager] [JWT-REACTIVATED] Session reactivated for user ${userDocId.substring(0, 8)}...`
683682
);
684683
return decoded;
685684
}
686685

687-
// Edge case: sessions exist but none are active or inactive (shouldn't happen)
686+
// No sessions exist at all - session was never created (bug/race condition)
687+
if (strictMode) {
688+
strapi.log.info(
689+
`[magic-sessionmanager] [JWT-BLOCKED] No sessions exist for user ${userDocId.substring(0, 8)}... (strictMode)`
690+
);
691+
return null;
692+
}
693+
694+
// Non-strict mode: Allow through but warn
688695
strapi.log.warn(
689-
`[magic-sessionmanager] [JWT-ALLOW] Unexpected session state for user ${userDocId.substring(0, 8)}... (allowing)`
696+
`[magic-sessionmanager] [JWT-WARN] No session for user ${userDocId.substring(0, 8)}... (allowing)`
690697
);
691698
return decoded;
692699

server/src/content-types/session/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
"default": true,
7272
"required": true
7373
},
74+
"terminatedManually": {
75+
"type": "boolean",
76+
"default": false,
77+
"required": false
78+
},
7479
"geoLocation": {
7580
"type": "json"
7681
},

server/src/middlewares/last-seen.js

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -113,29 +113,48 @@ module.exports = ({ strapi, sessionService }) => {
113113

114114
// If user has NO active sessions
115115
if (!activeSessions || activeSessions.length === 0) {
116-
// Check if user has ANY sessions at all
117-
const allSessions = await strapi.documents(SESSION_UID).findMany({
118-
filters: { user: { documentId: userDocId } },
119-
limit: 1,
120-
fields: ['isActive'],
116+
// Check for inactive sessions
117+
const inactiveSessions = await strapi.documents(SESSION_UID).findMany({
118+
filters: {
119+
user: { documentId: userDocId },
120+
isActive: false,
121+
},
122+
limit: 5,
123+
fields: ['documentId', 'terminatedManually', 'lastActive'],
124+
sort: [{ lastActive: 'desc' }],
121125
});
122126

123-
const hasInactiveSessions = allSessions?.some(s => s.isActive === false);
124-
125-
if (hasInactiveSessions) {
126-
// User was explicitly logged out → ALWAYS BLOCK
127-
strapi.log.info(`[magic-sessionmanager] [BLOCKED] Session terminated (user: ${userDocId.substring(0, 8)}...)`);
128-
return ctx.unauthorized('Session has been terminated. Please login again.');
129-
}
130-
131-
// No sessions exist at all - session was never created
132-
if (strictMode) {
133-
strapi.log.info(`[magic-sessionmanager] [BLOCKED] No session exists (user: ${userDocId.substring(0, 8)}..., strictMode)`);
134-
return ctx.unauthorized('No valid session. Please login again.');
127+
if (inactiveSessions && inactiveSessions.length > 0) {
128+
// Check if ANY session was manually terminated
129+
const manuallyTerminated = inactiveSessions.find(s => s.terminatedManually === true);
130+
131+
if (manuallyTerminated) {
132+
// User was explicitly logged out → BLOCK
133+
strapi.log.info(`[magic-sessionmanager] [BLOCKED] User ${userDocId.substring(0, 8)}... was manually logged out`);
134+
return ctx.unauthorized('Session has been terminated. Please login again.');
135+
}
136+
137+
// Session was deactivated by timeout → REACTIVATE most recent one
138+
const sessionToReactivate = inactiveSessions[0];
139+
await strapi.documents(SESSION_UID).update({
140+
documentId: sessionToReactivate.documentId,
141+
data: {
142+
isActive: true,
143+
lastActive: new Date(),
144+
},
145+
});
146+
strapi.log.info(`[magic-sessionmanager] [REACTIVATED] Session reactivated for user ${userDocId.substring(0, 8)}...`);
147+
// Continue - session is now active
148+
} else {
149+
// No sessions exist at all - session was never created
150+
if (strictMode) {
151+
strapi.log.info(`[magic-sessionmanager] [BLOCKED] No session exists (user: ${userDocId.substring(0, 8)}..., strictMode)`);
152+
return ctx.unauthorized('No valid session. Please login again.');
153+
}
154+
155+
// Non-strict mode: Allow but log warning
156+
strapi.log.warn(`[magic-sessionmanager] [WARN] No session for user ${userDocId.substring(0, 8)}... (allowing)`);
135157
}
136-
137-
// Non-strict mode: Allow but log warning
138-
strapi.log.warn(`[magic-sessionmanager] [WARN] No session for user ${userDocId.substring(0, 8)}... (allowing)`);
139158
}
140159

141160
// Store documentId for later use

server/src/policies/session-required.js

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,42 @@ module.exports = async (policyContext, config, { strapi }) => {
5757
return true;
5858
}
5959

60-
// No active session - check if user was explicitly logged out
61-
const allSessions = await strapi.documents(SESSION_UID).findMany({
62-
filters: { user: { documentId: userDocId } },
63-
limit: 1,
64-
fields: ['isActive'],
60+
// No active session - check for inactive sessions
61+
const inactiveSessions = await strapi.documents(SESSION_UID).findMany({
62+
filters: {
63+
user: { documentId: userDocId },
64+
isActive: false,
65+
},
66+
limit: 5,
67+
fields: ['documentId', 'terminatedManually', 'lastActive'],
68+
sort: [{ lastActive: 'desc' }],
6569
});
6670

67-
const hasInactiveSessions = allSessions?.some(s => s.isActive === false);
68-
69-
if (hasInactiveSessions) {
70-
// User was explicitly logged out → ALWAYS BLOCK
71+
if (inactiveSessions && inactiveSessions.length > 0) {
72+
// Check if ANY session was manually terminated
73+
const manuallyTerminated = inactiveSessions.find(s => s.terminatedManually === true);
74+
75+
if (manuallyTerminated) {
76+
// User was explicitly logged out → BLOCK
77+
strapi.log.info(
78+
`[magic-sessionmanager] [POLICY-BLOCKED] User ${userDocId.substring(0, 8)}... was manually logged out`
79+
);
80+
throw new errors.UnauthorizedError('Session terminated. Please login again.');
81+
}
82+
83+
// Session was deactivated by timeout → REACTIVATE most recent one
84+
const sessionToReactivate = inactiveSessions[0];
85+
await strapi.documents(SESSION_UID).update({
86+
documentId: sessionToReactivate.documentId,
87+
data: {
88+
isActive: true,
89+
lastActive: new Date(),
90+
},
91+
});
7192
strapi.log.info(
72-
`[magic-sessionmanager] [POLICY-BLOCKED] Session terminated (user: ${userDocId.substring(0, 8)}...)`
93+
`[magic-sessionmanager] [POLICY-REACTIVATED] Session reactivated for user ${userDocId.substring(0, 8)}...`
7394
);
74-
throw new errors.UnauthorizedError('Session terminated. Please login again.');
95+
return true;
7596
}
7697

7798
// No sessions exist at all - session was never created

server/src/services/session.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,12 @@ module.exports = ({ strapi }) => {
109109
documentId: sessionId,
110110
data: {
111111
isActive: false,
112+
terminatedManually: true,
112113
logoutTime: now,
113114
},
114115
});
115116

116-
log.info(`Session ${sessionId} terminated`);
117+
log.info(`Session ${sessionId} terminated (manual)`);
117118
} else if (userId) {
118119
// Strapi v5: If numeric id provided, look up documentId first
119120
// NOTE: entityService is deprecated, but required here for numeric ID -> documentId conversion
@@ -133,18 +134,19 @@ module.exports = ({ strapi }) => {
133134
},
134135
});
135136

136-
// Terminate all active sessions
137+
// Terminate all active sessions (manual logout)
137138
for (const session of activeSessions) {
138139
await strapi.documents(SESSION_UID).update({
139140
documentId: session.documentId,
140141
data: {
141142
isActive: false,
143+
terminatedManually: true,
142144
logoutTime: now,
143145
},
144146
});
145147
}
146148

147-
log.info(`All sessions terminated for user ${userDocumentId}`);
149+
log.info(`All sessions terminated (manual) for user ${userDocumentId}`);
148150
}
149151
} catch (err) {
150152
log.error('Error terminating session:', err);
@@ -568,7 +570,10 @@ module.exports = ({ strapi }) => {
568570
if (lastActiveTime < cutoffTime) {
569571
await strapi.documents(SESSION_UID).update({
570572
documentId: session.documentId,
571-
data: { isActive: false },
573+
data: {
574+
isActive: false,
575+
terminatedManually: false, // Timeout, not manual - can be reactivated
576+
},
572577
});
573578
deactivatedCount++;
574579
}

0 commit comments

Comments
 (0)