strapi-plugin-magic-sessionmanager 4.5.3 → 4.5.4
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/dist/server/index.js +163 -35
- package/dist/server/index.mjs +163 -35
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -9981,17 +9981,28 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9981
9981
|
});
|
|
9982
9982
|
log.info("[SUCCESS] Failed-login lockout middleware mounted");
|
|
9983
9983
|
}
|
|
9984
|
+
function isJwtIssuingPath(path2, method) {
|
|
9985
|
+
if (!path2) return false;
|
|
9986
|
+
const get2 = method === "GET";
|
|
9987
|
+
const post = method === "POST";
|
|
9988
|
+
if (post && path2 === "/api/auth/local") return true;
|
|
9989
|
+
if (post && path2 === "/api/auth/local/register") return true;
|
|
9990
|
+
if (post && path2 === "/api/auth/reset-password") return true;
|
|
9991
|
+
if ((get2 || post) && path2 === "/api/auth/email-confirmation") return true;
|
|
9992
|
+
if (get2 && /^\/api\/auth\/[a-z0-9-]+\/callback$/i.test(path2)) return true;
|
|
9993
|
+
if ((get2 || post) && path2.startsWith("/api/magic-link/login")) return true;
|
|
9994
|
+
if (post && path2 === "/api/magic-link/verify-mfa-totp") return true;
|
|
9995
|
+
if (post && path2 === "/api/magic-link/otp/verify") return true;
|
|
9996
|
+
if (post && path2 === "/api/magic-link/login-totp") return true;
|
|
9997
|
+
if ((get2 || post) && path2.startsWith("/api/passwordless/")) return true;
|
|
9998
|
+
return false;
|
|
9999
|
+
}
|
|
9984
10000
|
function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
9985
10001
|
strapi2.server.use(async (ctx, next) => {
|
|
9986
10002
|
await next();
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
const isMagicLinkOTP = ctx.path.includes("/magic-link/otp/verify") && ctx.method === "POST";
|
|
9991
|
-
const isMagicLink = isMagicLinkLogin || isMagicLinkMFA || isMagicLinkOTP;
|
|
9992
|
-
if (!((isAuthLocal || isMagicLink) && ctx.status === 200 && ctx.body && ctx.body.jwt && ctx.body.user)) {
|
|
9993
|
-
return;
|
|
9994
|
-
}
|
|
10003
|
+
if (!isJwtIssuingPath(ctx.path, ctx.method)) return;
|
|
10004
|
+
if (ctx.status !== 200) return;
|
|
10005
|
+
if (!ctx.body || !ctx.body.jwt || !ctx.body.user) return;
|
|
9995
10006
|
try {
|
|
9996
10007
|
const user = ctx.body.user;
|
|
9997
10008
|
const ip = getClientIp(ctx);
|
|
@@ -10192,11 +10203,19 @@ function mountRefreshTokenInterceptor({ strapi: strapi2, log }) {
|
|
|
10192
10203
|
log.info("[SUCCESS] Refresh token interceptor middleware mounted");
|
|
10193
10204
|
}
|
|
10194
10205
|
async function ensureContentApiPermissions(strapi2, log) {
|
|
10206
|
+
const PERMISSIONS_VERSION = 2;
|
|
10195
10207
|
try {
|
|
10196
10208
|
const pluginStore = strapi2.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
10197
|
-
const
|
|
10198
|
-
if (
|
|
10199
|
-
|
|
10209
|
+
const storedVersion = await pluginStore.get({ key: "contentApiPermissionsVersion" });
|
|
10210
|
+
if (storedVersion === void 0) {
|
|
10211
|
+
const legacyFlag = await pluginStore.get({ key: "contentApiPermissionsInitialized" });
|
|
10212
|
+
if (legacyFlag === true) {
|
|
10213
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: 1 });
|
|
10214
|
+
}
|
|
10215
|
+
}
|
|
10216
|
+
const effectiveVersion = await pluginStore.get({ key: "contentApiPermissionsVersion" });
|
|
10217
|
+
if (effectiveVersion >= PERMISSIONS_VERSION) {
|
|
10218
|
+
log.debug("Content-API permissions already at current version (skipping auto-setup)");
|
|
10200
10219
|
return;
|
|
10201
10220
|
}
|
|
10202
10221
|
const ROLE_UID = "plugin::users-permissions.role";
|
|
@@ -10214,6 +10233,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10214
10233
|
const requiredActions = [
|
|
10215
10234
|
"plugin::magic-sessionmanager.session.logout",
|
|
10216
10235
|
"plugin::magic-sessionmanager.session.logoutAll",
|
|
10236
|
+
"plugin::magic-sessionmanager.session.logoutOthers",
|
|
10217
10237
|
"plugin::magic-sessionmanager.session.getOwnSessions",
|
|
10218
10238
|
"plugin::magic-sessionmanager.session.getUserSessions",
|
|
10219
10239
|
"plugin::magic-sessionmanager.session.getCurrentSession",
|
|
@@ -10230,7 +10250,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10230
10250
|
const existingActions = existingPermissions.map((p) => p.action);
|
|
10231
10251
|
const missingActions = requiredActions.filter((action) => !existingActions.includes(action));
|
|
10232
10252
|
if (missingActions.length === 0) {
|
|
10233
|
-
await pluginStore.set({ key: "
|
|
10253
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: PERMISSIONS_VERSION });
|
|
10234
10254
|
log.debug("Content-API permissions already configured");
|
|
10235
10255
|
return;
|
|
10236
10256
|
}
|
|
@@ -10240,7 +10260,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10240
10260
|
});
|
|
10241
10261
|
log.info(`[PERMISSION] Enabled ${action} for authenticated users`);
|
|
10242
10262
|
}
|
|
10243
|
-
await pluginStore.set({ key: "
|
|
10263
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: PERMISSIONS_VERSION });
|
|
10244
10264
|
log.info("[SUCCESS] Content-API permissions configured for authenticated users");
|
|
10245
10265
|
} catch (err) {
|
|
10246
10266
|
log.warn("Could not auto-configure permissions:", err.message);
|
|
@@ -10699,7 +10719,17 @@ var contentApi$1 = {
|
|
|
10699
10719
|
config: {
|
|
10700
10720
|
auth: { strategies: ["users-permissions"] },
|
|
10701
10721
|
middlewares: writeRateLimit,
|
|
10702
|
-
description: "Logout from
|
|
10722
|
+
description: "Logout from ALL devices including the current one (requires JWT)"
|
|
10723
|
+
}
|
|
10724
|
+
},
|
|
10725
|
+
{
|
|
10726
|
+
method: "POST",
|
|
10727
|
+
path: "/logout-others",
|
|
10728
|
+
handler: "session.logoutOthers",
|
|
10729
|
+
config: {
|
|
10730
|
+
auth: { strategies: ["users-permissions"] },
|
|
10731
|
+
middlewares: writeRateLimit,
|
|
10732
|
+
description: "Logout from all OTHER devices, keep current session alive (requires JWT)"
|
|
10703
10733
|
}
|
|
10704
10734
|
},
|
|
10705
10735
|
// ================== SESSION QUERIES ==================
|
|
@@ -11251,7 +11281,14 @@ var session$3 = {
|
|
|
11251
11281
|
}
|
|
11252
11282
|
},
|
|
11253
11283
|
/**
|
|
11254
|
-
* Terminates
|
|
11284
|
+
* Terminates EVERY session of the authenticated user — including the
|
|
11285
|
+
* current one. After this call the caller will also be logged out on
|
|
11286
|
+
* the next request (their own JWT is rejected by the JWT-verify wrapper
|
|
11287
|
+
* with reason=manual).
|
|
11288
|
+
*
|
|
11289
|
+
* For the "log me out everywhere ELSE but keep me here" flow, use
|
|
11290
|
+
* `/logout-others` below.
|
|
11291
|
+
*
|
|
11255
11292
|
* @route POST /api/magic-sessionmanager/logout-all
|
|
11256
11293
|
*/
|
|
11257
11294
|
async logoutAll(ctx) {
|
|
@@ -11261,14 +11298,82 @@ var session$3 = {
|
|
|
11261
11298
|
return ctx.unauthorized("Authentication required");
|
|
11262
11299
|
}
|
|
11263
11300
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11264
|
-
await sessionService.terminateSession({
|
|
11265
|
-
|
|
11266
|
-
|
|
11301
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11302
|
+
userId: userDocId,
|
|
11303
|
+
reason: "manual"
|
|
11304
|
+
});
|
|
11305
|
+
strapi.log.info(
|
|
11306
|
+
`[magic-sessionmanager] User ${userDocId} logged out from all devices (${terminatedCount})`
|
|
11307
|
+
);
|
|
11308
|
+
ctx.body = {
|
|
11309
|
+
message: "Logged out from all devices successfully",
|
|
11310
|
+
terminatedCount
|
|
11311
|
+
};
|
|
11267
11312
|
} catch (err) {
|
|
11268
11313
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
11269
11314
|
ctx.throw(500, "Error during logout");
|
|
11270
11315
|
}
|
|
11271
11316
|
},
|
|
11317
|
+
/**
|
|
11318
|
+
* Terminates every session of the authenticated user EXCEPT the current
|
|
11319
|
+
* one. This is the "kick everyone else off my account" flow — the
|
|
11320
|
+
* caller stays logged in on the device they are using.
|
|
11321
|
+
*
|
|
11322
|
+
* If no "current session" record can be located for the caller's JWT
|
|
11323
|
+
* (edge case: user logged in before the session-manager was installed,
|
|
11324
|
+
* or right at the end of the grace window) we fall back to terminating
|
|
11325
|
+
* ALL sessions so the user still gets the safety effect they asked for,
|
|
11326
|
+
* and we report `fellBackToLogoutAll: true` so the client can adjust
|
|
11327
|
+
* its success message.
|
|
11328
|
+
*
|
|
11329
|
+
* @route POST /api/magic-sessionmanager/logout-others
|
|
11330
|
+
*/
|
|
11331
|
+
async logoutOthers(ctx) {
|
|
11332
|
+
try {
|
|
11333
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11334
|
+
const token = extractBearerToken$1(ctx);
|
|
11335
|
+
if (!userDocId || !token) {
|
|
11336
|
+
return ctx.unauthorized("Authentication required");
|
|
11337
|
+
}
|
|
11338
|
+
const currentTokenHash = hashToken$2(token);
|
|
11339
|
+
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11340
|
+
filters: { user: { documentId: userDocId }, tokenHash: currentTokenHash },
|
|
11341
|
+
fields: ["documentId"]
|
|
11342
|
+
});
|
|
11343
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11344
|
+
if (!currentSession) {
|
|
11345
|
+
const { terminatedCount: terminatedCount2 } = await sessionService.terminateSession({
|
|
11346
|
+
userId: userDocId,
|
|
11347
|
+
reason: "manual"
|
|
11348
|
+
});
|
|
11349
|
+
strapi.log.warn(
|
|
11350
|
+
`[magic-sessionmanager] logoutOthers fell back to logoutAll for user ${userDocId} (no current session match)`
|
|
11351
|
+
);
|
|
11352
|
+
ctx.body = {
|
|
11353
|
+
message: "All sessions terminated (could not preserve current session)",
|
|
11354
|
+
terminatedCount: terminatedCount2,
|
|
11355
|
+
fellBackToLogoutAll: true
|
|
11356
|
+
};
|
|
11357
|
+
return;
|
|
11358
|
+
}
|
|
11359
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11360
|
+
userId: userDocId,
|
|
11361
|
+
exceptSessionId: currentSession.documentId,
|
|
11362
|
+
reason: "manual"
|
|
11363
|
+
});
|
|
11364
|
+
strapi.log.info(
|
|
11365
|
+
`[magic-sessionmanager] User ${userDocId} logged out ${terminatedCount} other device(s)`
|
|
11366
|
+
);
|
|
11367
|
+
ctx.body = {
|
|
11368
|
+
message: terminatedCount === 0 ? "No other active sessions to terminate" : `${terminatedCount} other session(s) terminated`,
|
|
11369
|
+
terminatedCount,
|
|
11370
|
+
currentSessionPreserved: true
|
|
11371
|
+
};
|
|
11372
|
+
} catch (err) {
|
|
11373
|
+
strapi.log.error("[magic-sessionmanager] Logout-others error:", err);
|
|
11374
|
+
ctx.throw(500, "Error terminating other sessions");
|
|
11375
|
+
}
|
|
11376
|
+
},
|
|
11272
11377
|
/**
|
|
11273
11378
|
* Returns the session associated with the current JWT.
|
|
11274
11379
|
*
|
|
@@ -12123,9 +12228,9 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12123
12228
|
}
|
|
12124
12229
|
},
|
|
12125
12230
|
/**
|
|
12126
|
-
* Terminates a single session
|
|
12127
|
-
* reason so the JWT-verify wrapper
|
|
12128
|
-
* communicate the cause to the client
|
|
12231
|
+
* Terminates a single session, all sessions of a user, or all sessions
|
|
12232
|
+
* of a user EXCEPT one with a typed reason so the JWT-verify wrapper
|
|
12233
|
+
* can communicate the cause to the client.
|
|
12129
12234
|
*
|
|
12130
12235
|
* Supported reasons:
|
|
12131
12236
|
* - 'manual': user clicked logout, or admin terminated a session
|
|
@@ -12139,12 +12244,18 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12139
12244
|
* to work, while new code relies on `terminationReason`.
|
|
12140
12245
|
*
|
|
12141
12246
|
* @param {Object} params
|
|
12142
|
-
* @param {string} [params.sessionId]
|
|
12143
|
-
* @param {string|number} [params.userId]
|
|
12247
|
+
* @param {string} [params.sessionId] Terminate exactly this session
|
|
12248
|
+
* @param {string|number} [params.userId] Terminate every active session
|
|
12249
|
+
* of this user …
|
|
12250
|
+
* @param {string} [params.exceptSessionId] … except for this one. Only
|
|
12251
|
+
* meaningful together with
|
|
12252
|
+
* `userId`. Used by
|
|
12253
|
+
* /logout-other-devices so
|
|
12254
|
+
* the caller stays logged in.
|
|
12144
12255
|
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
12145
|
-
* @returns {Promise<
|
|
12256
|
+
* @returns {Promise<{terminatedCount: number}>}
|
|
12146
12257
|
*/
|
|
12147
|
-
async terminateSession({ sessionId, userId, reason = "manual" }) {
|
|
12258
|
+
async terminateSession({ sessionId, userId, exceptSessionId = null, reason = "manual" }) {
|
|
12148
12259
|
try {
|
|
12149
12260
|
const now = /* @__PURE__ */ new Date();
|
|
12150
12261
|
const validReasons = ["manual", "idle", "expired", "blocked"];
|
|
@@ -12162,29 +12273,46 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12162
12273
|
});
|
|
12163
12274
|
if (!existing) {
|
|
12164
12275
|
log.warn(`Session ${sessionId} not found for termination`);
|
|
12165
|
-
return;
|
|
12276
|
+
return { terminatedCount: 0 };
|
|
12166
12277
|
}
|
|
12167
12278
|
await strapi2.documents(SESSION_UID$1).update({
|
|
12168
12279
|
documentId: sessionId,
|
|
12169
12280
|
data: updateData
|
|
12170
12281
|
});
|
|
12171
12282
|
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
12172
|
-
|
|
12283
|
+
return { terminatedCount: 1 };
|
|
12284
|
+
}
|
|
12285
|
+
if (userId) {
|
|
12173
12286
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
12174
|
-
if (!userDocumentId) return;
|
|
12287
|
+
if (!userDocumentId) return { terminatedCount: 0 };
|
|
12288
|
+
const filters2 = { user: { documentId: userDocumentId }, isActive: true };
|
|
12289
|
+
if (exceptSessionId) {
|
|
12290
|
+
filters2.documentId = { $ne: exceptSessionId };
|
|
12291
|
+
}
|
|
12175
12292
|
const activeSessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
12176
|
-
filters:
|
|
12293
|
+
filters: filters2,
|
|
12177
12294
|
fields: ["documentId"],
|
|
12178
12295
|
limit: MAX_SESSIONS_QUERY
|
|
12179
12296
|
});
|
|
12297
|
+
let terminatedCount = 0;
|
|
12180
12298
|
for (const session2 of activeSessions) {
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12299
|
+
try {
|
|
12300
|
+
await strapi2.documents(SESSION_UID$1).update({
|
|
12301
|
+
documentId: session2.documentId,
|
|
12302
|
+
data: updateData
|
|
12303
|
+
});
|
|
12304
|
+
terminatedCount++;
|
|
12305
|
+
} catch (err) {
|
|
12306
|
+
log.debug(`Failed to terminate session ${session2.documentId}:`, err.message);
|
|
12307
|
+
}
|
|
12185
12308
|
}
|
|
12186
|
-
|
|
12309
|
+
const label = exceptSessionId ? "OTHER sessions" : "ALL sessions";
|
|
12310
|
+
log.info(
|
|
12311
|
+
`${label} terminated for user ${userDocumentId} (reason: ${finalReason}, count: ${terminatedCount})`
|
|
12312
|
+
);
|
|
12313
|
+
return { terminatedCount };
|
|
12187
12314
|
}
|
|
12315
|
+
return { terminatedCount: 0 };
|
|
12188
12316
|
} catch (err) {
|
|
12189
12317
|
log.error("Error terminating session:", err);
|
|
12190
12318
|
throw err;
|
|
@@ -12544,7 +12672,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12544
12672
|
}
|
|
12545
12673
|
};
|
|
12546
12674
|
};
|
|
12547
|
-
const version$1 = "4.5.
|
|
12675
|
+
const version$1 = "4.5.3";
|
|
12548
12676
|
const require$$2 = {
|
|
12549
12677
|
version: version$1
|
|
12550
12678
|
};
|
package/dist/server/index.mjs
CHANGED
|
@@ -9968,17 +9968,28 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9968
9968
|
});
|
|
9969
9969
|
log.info("[SUCCESS] Failed-login lockout middleware mounted");
|
|
9970
9970
|
}
|
|
9971
|
+
function isJwtIssuingPath(path2, method) {
|
|
9972
|
+
if (!path2) return false;
|
|
9973
|
+
const get2 = method === "GET";
|
|
9974
|
+
const post = method === "POST";
|
|
9975
|
+
if (post && path2 === "/api/auth/local") return true;
|
|
9976
|
+
if (post && path2 === "/api/auth/local/register") return true;
|
|
9977
|
+
if (post && path2 === "/api/auth/reset-password") return true;
|
|
9978
|
+
if ((get2 || post) && path2 === "/api/auth/email-confirmation") return true;
|
|
9979
|
+
if (get2 && /^\/api\/auth\/[a-z0-9-]+\/callback$/i.test(path2)) return true;
|
|
9980
|
+
if ((get2 || post) && path2.startsWith("/api/magic-link/login")) return true;
|
|
9981
|
+
if (post && path2 === "/api/magic-link/verify-mfa-totp") return true;
|
|
9982
|
+
if (post && path2 === "/api/magic-link/otp/verify") return true;
|
|
9983
|
+
if (post && path2 === "/api/magic-link/login-totp") return true;
|
|
9984
|
+
if ((get2 || post) && path2.startsWith("/api/passwordless/")) return true;
|
|
9985
|
+
return false;
|
|
9986
|
+
}
|
|
9971
9987
|
function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
9972
9988
|
strapi2.server.use(async (ctx, next) => {
|
|
9973
9989
|
await next();
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
const isMagicLinkOTP = ctx.path.includes("/magic-link/otp/verify") && ctx.method === "POST";
|
|
9978
|
-
const isMagicLink = isMagicLinkLogin || isMagicLinkMFA || isMagicLinkOTP;
|
|
9979
|
-
if (!((isAuthLocal || isMagicLink) && ctx.status === 200 && ctx.body && ctx.body.jwt && ctx.body.user)) {
|
|
9980
|
-
return;
|
|
9981
|
-
}
|
|
9990
|
+
if (!isJwtIssuingPath(ctx.path, ctx.method)) return;
|
|
9991
|
+
if (ctx.status !== 200) return;
|
|
9992
|
+
if (!ctx.body || !ctx.body.jwt || !ctx.body.user) return;
|
|
9982
9993
|
try {
|
|
9983
9994
|
const user = ctx.body.user;
|
|
9984
9995
|
const ip = getClientIp(ctx);
|
|
@@ -10179,11 +10190,19 @@ function mountRefreshTokenInterceptor({ strapi: strapi2, log }) {
|
|
|
10179
10190
|
log.info("[SUCCESS] Refresh token interceptor middleware mounted");
|
|
10180
10191
|
}
|
|
10181
10192
|
async function ensureContentApiPermissions(strapi2, log) {
|
|
10193
|
+
const PERMISSIONS_VERSION = 2;
|
|
10182
10194
|
try {
|
|
10183
10195
|
const pluginStore = strapi2.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
10184
|
-
const
|
|
10185
|
-
if (
|
|
10186
|
-
|
|
10196
|
+
const storedVersion = await pluginStore.get({ key: "contentApiPermissionsVersion" });
|
|
10197
|
+
if (storedVersion === void 0) {
|
|
10198
|
+
const legacyFlag = await pluginStore.get({ key: "contentApiPermissionsInitialized" });
|
|
10199
|
+
if (legacyFlag === true) {
|
|
10200
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: 1 });
|
|
10201
|
+
}
|
|
10202
|
+
}
|
|
10203
|
+
const effectiveVersion = await pluginStore.get({ key: "contentApiPermissionsVersion" });
|
|
10204
|
+
if (effectiveVersion >= PERMISSIONS_VERSION) {
|
|
10205
|
+
log.debug("Content-API permissions already at current version (skipping auto-setup)");
|
|
10187
10206
|
return;
|
|
10188
10207
|
}
|
|
10189
10208
|
const ROLE_UID = "plugin::users-permissions.role";
|
|
@@ -10201,6 +10220,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10201
10220
|
const requiredActions = [
|
|
10202
10221
|
"plugin::magic-sessionmanager.session.logout",
|
|
10203
10222
|
"plugin::magic-sessionmanager.session.logoutAll",
|
|
10223
|
+
"plugin::magic-sessionmanager.session.logoutOthers",
|
|
10204
10224
|
"plugin::magic-sessionmanager.session.getOwnSessions",
|
|
10205
10225
|
"plugin::magic-sessionmanager.session.getUserSessions",
|
|
10206
10226
|
"plugin::magic-sessionmanager.session.getCurrentSession",
|
|
@@ -10217,7 +10237,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10217
10237
|
const existingActions = existingPermissions.map((p) => p.action);
|
|
10218
10238
|
const missingActions = requiredActions.filter((action) => !existingActions.includes(action));
|
|
10219
10239
|
if (missingActions.length === 0) {
|
|
10220
|
-
await pluginStore.set({ key: "
|
|
10240
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: PERMISSIONS_VERSION });
|
|
10221
10241
|
log.debug("Content-API permissions already configured");
|
|
10222
10242
|
return;
|
|
10223
10243
|
}
|
|
@@ -10227,7 +10247,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10227
10247
|
});
|
|
10228
10248
|
log.info(`[PERMISSION] Enabled ${action} for authenticated users`);
|
|
10229
10249
|
}
|
|
10230
|
-
await pluginStore.set({ key: "
|
|
10250
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: PERMISSIONS_VERSION });
|
|
10231
10251
|
log.info("[SUCCESS] Content-API permissions configured for authenticated users");
|
|
10232
10252
|
} catch (err) {
|
|
10233
10253
|
log.warn("Could not auto-configure permissions:", err.message);
|
|
@@ -10686,7 +10706,17 @@ var contentApi$1 = {
|
|
|
10686
10706
|
config: {
|
|
10687
10707
|
auth: { strategies: ["users-permissions"] },
|
|
10688
10708
|
middlewares: writeRateLimit,
|
|
10689
|
-
description: "Logout from
|
|
10709
|
+
description: "Logout from ALL devices including the current one (requires JWT)"
|
|
10710
|
+
}
|
|
10711
|
+
},
|
|
10712
|
+
{
|
|
10713
|
+
method: "POST",
|
|
10714
|
+
path: "/logout-others",
|
|
10715
|
+
handler: "session.logoutOthers",
|
|
10716
|
+
config: {
|
|
10717
|
+
auth: { strategies: ["users-permissions"] },
|
|
10718
|
+
middlewares: writeRateLimit,
|
|
10719
|
+
description: "Logout from all OTHER devices, keep current session alive (requires JWT)"
|
|
10690
10720
|
}
|
|
10691
10721
|
},
|
|
10692
10722
|
// ================== SESSION QUERIES ==================
|
|
@@ -11238,7 +11268,14 @@ var session$3 = {
|
|
|
11238
11268
|
}
|
|
11239
11269
|
},
|
|
11240
11270
|
/**
|
|
11241
|
-
* Terminates
|
|
11271
|
+
* Terminates EVERY session of the authenticated user — including the
|
|
11272
|
+
* current one. After this call the caller will also be logged out on
|
|
11273
|
+
* the next request (their own JWT is rejected by the JWT-verify wrapper
|
|
11274
|
+
* with reason=manual).
|
|
11275
|
+
*
|
|
11276
|
+
* For the "log me out everywhere ELSE but keep me here" flow, use
|
|
11277
|
+
* `/logout-others` below.
|
|
11278
|
+
*
|
|
11242
11279
|
* @route POST /api/magic-sessionmanager/logout-all
|
|
11243
11280
|
*/
|
|
11244
11281
|
async logoutAll(ctx) {
|
|
@@ -11248,14 +11285,82 @@ var session$3 = {
|
|
|
11248
11285
|
return ctx.unauthorized("Authentication required");
|
|
11249
11286
|
}
|
|
11250
11287
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11251
|
-
await sessionService.terminateSession({
|
|
11252
|
-
|
|
11253
|
-
|
|
11288
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11289
|
+
userId: userDocId,
|
|
11290
|
+
reason: "manual"
|
|
11291
|
+
});
|
|
11292
|
+
strapi.log.info(
|
|
11293
|
+
`[magic-sessionmanager] User ${userDocId} logged out from all devices (${terminatedCount})`
|
|
11294
|
+
);
|
|
11295
|
+
ctx.body = {
|
|
11296
|
+
message: "Logged out from all devices successfully",
|
|
11297
|
+
terminatedCount
|
|
11298
|
+
};
|
|
11254
11299
|
} catch (err) {
|
|
11255
11300
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
11256
11301
|
ctx.throw(500, "Error during logout");
|
|
11257
11302
|
}
|
|
11258
11303
|
},
|
|
11304
|
+
/**
|
|
11305
|
+
* Terminates every session of the authenticated user EXCEPT the current
|
|
11306
|
+
* one. This is the "kick everyone else off my account" flow — the
|
|
11307
|
+
* caller stays logged in on the device they are using.
|
|
11308
|
+
*
|
|
11309
|
+
* If no "current session" record can be located for the caller's JWT
|
|
11310
|
+
* (edge case: user logged in before the session-manager was installed,
|
|
11311
|
+
* or right at the end of the grace window) we fall back to terminating
|
|
11312
|
+
* ALL sessions so the user still gets the safety effect they asked for,
|
|
11313
|
+
* and we report `fellBackToLogoutAll: true` so the client can adjust
|
|
11314
|
+
* its success message.
|
|
11315
|
+
*
|
|
11316
|
+
* @route POST /api/magic-sessionmanager/logout-others
|
|
11317
|
+
*/
|
|
11318
|
+
async logoutOthers(ctx) {
|
|
11319
|
+
try {
|
|
11320
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11321
|
+
const token = extractBearerToken$1(ctx);
|
|
11322
|
+
if (!userDocId || !token) {
|
|
11323
|
+
return ctx.unauthorized("Authentication required");
|
|
11324
|
+
}
|
|
11325
|
+
const currentTokenHash = hashToken$2(token);
|
|
11326
|
+
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11327
|
+
filters: { user: { documentId: userDocId }, tokenHash: currentTokenHash },
|
|
11328
|
+
fields: ["documentId"]
|
|
11329
|
+
});
|
|
11330
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11331
|
+
if (!currentSession) {
|
|
11332
|
+
const { terminatedCount: terminatedCount2 } = await sessionService.terminateSession({
|
|
11333
|
+
userId: userDocId,
|
|
11334
|
+
reason: "manual"
|
|
11335
|
+
});
|
|
11336
|
+
strapi.log.warn(
|
|
11337
|
+
`[magic-sessionmanager] logoutOthers fell back to logoutAll for user ${userDocId} (no current session match)`
|
|
11338
|
+
);
|
|
11339
|
+
ctx.body = {
|
|
11340
|
+
message: "All sessions terminated (could not preserve current session)",
|
|
11341
|
+
terminatedCount: terminatedCount2,
|
|
11342
|
+
fellBackToLogoutAll: true
|
|
11343
|
+
};
|
|
11344
|
+
return;
|
|
11345
|
+
}
|
|
11346
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11347
|
+
userId: userDocId,
|
|
11348
|
+
exceptSessionId: currentSession.documentId,
|
|
11349
|
+
reason: "manual"
|
|
11350
|
+
});
|
|
11351
|
+
strapi.log.info(
|
|
11352
|
+
`[magic-sessionmanager] User ${userDocId} logged out ${terminatedCount} other device(s)`
|
|
11353
|
+
);
|
|
11354
|
+
ctx.body = {
|
|
11355
|
+
message: terminatedCount === 0 ? "No other active sessions to terminate" : `${terminatedCount} other session(s) terminated`,
|
|
11356
|
+
terminatedCount,
|
|
11357
|
+
currentSessionPreserved: true
|
|
11358
|
+
};
|
|
11359
|
+
} catch (err) {
|
|
11360
|
+
strapi.log.error("[magic-sessionmanager] Logout-others error:", err);
|
|
11361
|
+
ctx.throw(500, "Error terminating other sessions");
|
|
11362
|
+
}
|
|
11363
|
+
},
|
|
11259
11364
|
/**
|
|
11260
11365
|
* Returns the session associated with the current JWT.
|
|
11261
11366
|
*
|
|
@@ -12110,9 +12215,9 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12110
12215
|
}
|
|
12111
12216
|
},
|
|
12112
12217
|
/**
|
|
12113
|
-
* Terminates a single session
|
|
12114
|
-
* reason so the JWT-verify wrapper
|
|
12115
|
-
* communicate the cause to the client
|
|
12218
|
+
* Terminates a single session, all sessions of a user, or all sessions
|
|
12219
|
+
* of a user EXCEPT one with a typed reason so the JWT-verify wrapper
|
|
12220
|
+
* can communicate the cause to the client.
|
|
12116
12221
|
*
|
|
12117
12222
|
* Supported reasons:
|
|
12118
12223
|
* - 'manual': user clicked logout, or admin terminated a session
|
|
@@ -12126,12 +12231,18 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12126
12231
|
* to work, while new code relies on `terminationReason`.
|
|
12127
12232
|
*
|
|
12128
12233
|
* @param {Object} params
|
|
12129
|
-
* @param {string} [params.sessionId]
|
|
12130
|
-
* @param {string|number} [params.userId]
|
|
12234
|
+
* @param {string} [params.sessionId] Terminate exactly this session
|
|
12235
|
+
* @param {string|number} [params.userId] Terminate every active session
|
|
12236
|
+
* of this user …
|
|
12237
|
+
* @param {string} [params.exceptSessionId] … except for this one. Only
|
|
12238
|
+
* meaningful together with
|
|
12239
|
+
* `userId`. Used by
|
|
12240
|
+
* /logout-other-devices so
|
|
12241
|
+
* the caller stays logged in.
|
|
12131
12242
|
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
12132
|
-
* @returns {Promise<
|
|
12243
|
+
* @returns {Promise<{terminatedCount: number}>}
|
|
12133
12244
|
*/
|
|
12134
|
-
async terminateSession({ sessionId, userId, reason = "manual" }) {
|
|
12245
|
+
async terminateSession({ sessionId, userId, exceptSessionId = null, reason = "manual" }) {
|
|
12135
12246
|
try {
|
|
12136
12247
|
const now = /* @__PURE__ */ new Date();
|
|
12137
12248
|
const validReasons = ["manual", "idle", "expired", "blocked"];
|
|
@@ -12149,29 +12260,46 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12149
12260
|
});
|
|
12150
12261
|
if (!existing) {
|
|
12151
12262
|
log.warn(`Session ${sessionId} not found for termination`);
|
|
12152
|
-
return;
|
|
12263
|
+
return { terminatedCount: 0 };
|
|
12153
12264
|
}
|
|
12154
12265
|
await strapi2.documents(SESSION_UID$1).update({
|
|
12155
12266
|
documentId: sessionId,
|
|
12156
12267
|
data: updateData
|
|
12157
12268
|
});
|
|
12158
12269
|
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
12159
|
-
|
|
12270
|
+
return { terminatedCount: 1 };
|
|
12271
|
+
}
|
|
12272
|
+
if (userId) {
|
|
12160
12273
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
12161
|
-
if (!userDocumentId) return;
|
|
12274
|
+
if (!userDocumentId) return { terminatedCount: 0 };
|
|
12275
|
+
const filters2 = { user: { documentId: userDocumentId }, isActive: true };
|
|
12276
|
+
if (exceptSessionId) {
|
|
12277
|
+
filters2.documentId = { $ne: exceptSessionId };
|
|
12278
|
+
}
|
|
12162
12279
|
const activeSessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
12163
|
-
filters:
|
|
12280
|
+
filters: filters2,
|
|
12164
12281
|
fields: ["documentId"],
|
|
12165
12282
|
limit: MAX_SESSIONS_QUERY
|
|
12166
12283
|
});
|
|
12284
|
+
let terminatedCount = 0;
|
|
12167
12285
|
for (const session2 of activeSessions) {
|
|
12168
|
-
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
|
|
12286
|
+
try {
|
|
12287
|
+
await strapi2.documents(SESSION_UID$1).update({
|
|
12288
|
+
documentId: session2.documentId,
|
|
12289
|
+
data: updateData
|
|
12290
|
+
});
|
|
12291
|
+
terminatedCount++;
|
|
12292
|
+
} catch (err) {
|
|
12293
|
+
log.debug(`Failed to terminate session ${session2.documentId}:`, err.message);
|
|
12294
|
+
}
|
|
12172
12295
|
}
|
|
12173
|
-
|
|
12296
|
+
const label = exceptSessionId ? "OTHER sessions" : "ALL sessions";
|
|
12297
|
+
log.info(
|
|
12298
|
+
`${label} terminated for user ${userDocumentId} (reason: ${finalReason}, count: ${terminatedCount})`
|
|
12299
|
+
);
|
|
12300
|
+
return { terminatedCount };
|
|
12174
12301
|
}
|
|
12302
|
+
return { terminatedCount: 0 };
|
|
12175
12303
|
} catch (err) {
|
|
12176
12304
|
log.error("Error terminating session:", err);
|
|
12177
12305
|
throw err;
|
|
@@ -12531,7 +12659,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12531
12659
|
}
|
|
12532
12660
|
};
|
|
12533
12661
|
};
|
|
12534
|
-
const version$1 = "4.5.
|
|
12662
|
+
const version$1 = "4.5.3";
|
|
12535
12663
|
const require$$2 = {
|
|
12536
12664
|
version: version$1
|
|
12537
12665
|
};
|
package/package.json
CHANGED