strapi-plugin-magic-sessionmanager 4.5.3 → 4.5.5
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 +212 -57
- package/dist/server/index.mjs +212 -57
- 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 ==================
|
|
@@ -10746,17 +10776,26 @@ var contentApi$1 = {
|
|
|
10746
10776
|
}
|
|
10747
10777
|
]
|
|
10748
10778
|
};
|
|
10779
|
+
const PLUGIN_ACCESS_ACTION = "plugin::magic-sessionmanager.access";
|
|
10780
|
+
const adminPolicy = () => [
|
|
10781
|
+
"admin::isAuthenticatedAdmin",
|
|
10782
|
+
{
|
|
10783
|
+
name: "admin::hasPermissions",
|
|
10784
|
+
config: { actions: [PLUGIN_ACCESS_ACTION] }
|
|
10785
|
+
}
|
|
10786
|
+
];
|
|
10749
10787
|
const isDevEnvironment = (() => {
|
|
10750
10788
|
const env2 = (process.env.NODE_ENV || "development").toLowerCase();
|
|
10751
10789
|
return env2 !== "production" && env2 !== "staging";
|
|
10752
10790
|
})();
|
|
10753
10791
|
const baseRoutes = [
|
|
10792
|
+
// ============================ SESSIONS ============================
|
|
10754
10793
|
{
|
|
10755
10794
|
method: "GET",
|
|
10756
10795
|
path: "/sessions",
|
|
10757
10796
|
handler: "session.getAllSessionsAdmin",
|
|
10758
10797
|
config: {
|
|
10759
|
-
policies:
|
|
10798
|
+
policies: adminPolicy(),
|
|
10760
10799
|
description: "Get all sessions - active and inactive (admin)"
|
|
10761
10800
|
}
|
|
10762
10801
|
},
|
|
@@ -10765,7 +10804,7 @@ const baseRoutes = [
|
|
|
10765
10804
|
path: "/sessions/active",
|
|
10766
10805
|
handler: "session.getActiveSessions",
|
|
10767
10806
|
config: {
|
|
10768
|
-
policies:
|
|
10807
|
+
policies: adminPolicy(),
|
|
10769
10808
|
description: "Get only active sessions (admin)"
|
|
10770
10809
|
}
|
|
10771
10810
|
},
|
|
@@ -10774,8 +10813,8 @@ const baseRoutes = [
|
|
|
10774
10813
|
path: "/user/:userId/sessions",
|
|
10775
10814
|
handler: "session.getUserSessions",
|
|
10776
10815
|
config: {
|
|
10777
|
-
policies:
|
|
10778
|
-
description: "Get user
|
|
10816
|
+
policies: adminPolicy(),
|
|
10817
|
+
description: "Get sessions for a specific user (admin)"
|
|
10779
10818
|
}
|
|
10780
10819
|
},
|
|
10781
10820
|
{
|
|
@@ -10783,7 +10822,7 @@ const baseRoutes = [
|
|
|
10783
10822
|
path: "/sessions/:sessionId/terminate",
|
|
10784
10823
|
handler: "session.terminateSingleSession",
|
|
10785
10824
|
config: {
|
|
10786
|
-
policies:
|
|
10825
|
+
policies: adminPolicy(),
|
|
10787
10826
|
description: "Terminate a specific session (admin)"
|
|
10788
10827
|
}
|
|
10789
10828
|
},
|
|
@@ -10792,7 +10831,7 @@ const baseRoutes = [
|
|
|
10792
10831
|
path: "/sessions/:sessionId",
|
|
10793
10832
|
handler: "session.deleteSession",
|
|
10794
10833
|
config: {
|
|
10795
|
-
policies:
|
|
10834
|
+
policies: adminPolicy(),
|
|
10796
10835
|
description: "Delete a single session permanently (admin)"
|
|
10797
10836
|
}
|
|
10798
10837
|
},
|
|
@@ -10801,7 +10840,7 @@ const baseRoutes = [
|
|
|
10801
10840
|
path: "/sessions/clean-inactive",
|
|
10802
10841
|
handler: "session.cleanInactiveSessions",
|
|
10803
10842
|
config: {
|
|
10804
|
-
policies:
|
|
10843
|
+
policies: adminPolicy(),
|
|
10805
10844
|
description: "Delete all inactive sessions from database (admin)"
|
|
10806
10845
|
}
|
|
10807
10846
|
},
|
|
@@ -10810,7 +10849,7 @@ const baseRoutes = [
|
|
|
10810
10849
|
path: "/user/:userId/terminate-all",
|
|
10811
10850
|
handler: "session.terminateAllUserSessions",
|
|
10812
10851
|
config: {
|
|
10813
|
-
policies:
|
|
10852
|
+
policies: adminPolicy(),
|
|
10814
10853
|
description: "Terminate all sessions for a user (admin)"
|
|
10815
10854
|
}
|
|
10816
10855
|
},
|
|
@@ -10819,56 +10858,74 @@ const baseRoutes = [
|
|
|
10819
10858
|
path: "/user/:userId/toggle-block",
|
|
10820
10859
|
handler: "session.toggleUserBlock",
|
|
10821
10860
|
config: {
|
|
10822
|
-
policies:
|
|
10861
|
+
policies: adminPolicy(),
|
|
10823
10862
|
description: "Toggle user blocked status (admin)"
|
|
10824
10863
|
}
|
|
10825
10864
|
},
|
|
10865
|
+
// ============================ LICENSE ============================
|
|
10826
10866
|
{
|
|
10827
10867
|
method: "GET",
|
|
10828
10868
|
path: "/license/status",
|
|
10829
10869
|
handler: "license.getStatus",
|
|
10830
|
-
config: {
|
|
10870
|
+
config: {
|
|
10871
|
+
policies: adminPolicy(),
|
|
10872
|
+
description: "Get license status (admin)"
|
|
10873
|
+
}
|
|
10831
10874
|
},
|
|
10832
10875
|
{
|
|
10833
10876
|
method: "POST",
|
|
10834
10877
|
path: "/license/auto-create",
|
|
10835
10878
|
handler: "license.autoCreate",
|
|
10836
|
-
config: {
|
|
10879
|
+
config: {
|
|
10880
|
+
policies: adminPolicy(),
|
|
10881
|
+
description: "Auto-create license for current admin (admin)"
|
|
10882
|
+
}
|
|
10837
10883
|
},
|
|
10838
10884
|
{
|
|
10839
10885
|
method: "POST",
|
|
10840
10886
|
path: "/license/create",
|
|
10841
10887
|
handler: "license.createAndActivate",
|
|
10842
|
-
config: {
|
|
10888
|
+
config: {
|
|
10889
|
+
policies: adminPolicy(),
|
|
10890
|
+
description: "Create and activate a new license (admin)"
|
|
10891
|
+
}
|
|
10843
10892
|
},
|
|
10844
10893
|
{
|
|
10845
10894
|
method: "POST",
|
|
10846
10895
|
path: "/license/ping",
|
|
10847
10896
|
handler: "license.ping",
|
|
10848
|
-
config: {
|
|
10897
|
+
config: {
|
|
10898
|
+
policies: adminPolicy(),
|
|
10899
|
+
description: "Ping the license server (admin)"
|
|
10900
|
+
}
|
|
10849
10901
|
},
|
|
10850
10902
|
{
|
|
10851
10903
|
method: "POST",
|
|
10852
10904
|
path: "/license/store-key",
|
|
10853
10905
|
handler: "license.storeKey",
|
|
10854
|
-
config: {
|
|
10906
|
+
config: {
|
|
10907
|
+
policies: adminPolicy(),
|
|
10908
|
+
description: "Store a license key (admin)"
|
|
10909
|
+
}
|
|
10855
10910
|
},
|
|
10911
|
+
// ============================ GEOLOCATION ============================
|
|
10856
10912
|
{
|
|
10857
10913
|
method: "GET",
|
|
10858
10914
|
path: "/geolocation/:ipAddress",
|
|
10859
10915
|
handler: "session.getIpGeolocation",
|
|
10860
10916
|
config: {
|
|
10861
|
-
policies:
|
|
10862
|
-
description: "Get IP geolocation data (Premium feature)"
|
|
10917
|
+
policies: adminPolicy(),
|
|
10918
|
+
description: "Get IP geolocation data (Premium feature, admin)"
|
|
10863
10919
|
}
|
|
10864
10920
|
},
|
|
10921
|
+
// ============================ SETTINGS ============================
|
|
10865
10922
|
{
|
|
10866
10923
|
method: "GET",
|
|
10867
10924
|
path: "/settings",
|
|
10868
10925
|
handler: "settings.getSettings",
|
|
10869
10926
|
config: {
|
|
10870
|
-
policies:
|
|
10871
|
-
description: "Get plugin settings"
|
|
10927
|
+
policies: adminPolicy(),
|
|
10928
|
+
description: "Get plugin settings (admin)"
|
|
10872
10929
|
}
|
|
10873
10930
|
},
|
|
10874
10931
|
{
|
|
@@ -10876,8 +10933,8 @@ const baseRoutes = [
|
|
|
10876
10933
|
path: "/settings",
|
|
10877
10934
|
handler: "settings.updateSettings",
|
|
10878
10935
|
config: {
|
|
10879
|
-
policies:
|
|
10880
|
-
description: "Update plugin settings"
|
|
10936
|
+
policies: adminPolicy(),
|
|
10937
|
+
description: "Update plugin settings (admin)"
|
|
10881
10938
|
}
|
|
10882
10939
|
}
|
|
10883
10940
|
];
|
|
@@ -10887,8 +10944,8 @@ const devOnlyRoutes = [
|
|
|
10887
10944
|
path: "/sessions/:sessionId/simulate-timeout",
|
|
10888
10945
|
handler: "session.simulateTimeout",
|
|
10889
10946
|
config: {
|
|
10890
|
-
policies:
|
|
10891
|
-
description: "Simulate session timeout (dev-only)"
|
|
10947
|
+
policies: adminPolicy(),
|
|
10948
|
+
description: "Simulate session timeout (dev-only, admin)"
|
|
10892
10949
|
}
|
|
10893
10950
|
}
|
|
10894
10951
|
];
|
|
@@ -11251,7 +11308,14 @@ var session$3 = {
|
|
|
11251
11308
|
}
|
|
11252
11309
|
},
|
|
11253
11310
|
/**
|
|
11254
|
-
* Terminates
|
|
11311
|
+
* Terminates EVERY session of the authenticated user — including the
|
|
11312
|
+
* current one. After this call the caller will also be logged out on
|
|
11313
|
+
* the next request (their own JWT is rejected by the JWT-verify wrapper
|
|
11314
|
+
* with reason=manual).
|
|
11315
|
+
*
|
|
11316
|
+
* For the "log me out everywhere ELSE but keep me here" flow, use
|
|
11317
|
+
* `/logout-others` below.
|
|
11318
|
+
*
|
|
11255
11319
|
* @route POST /api/magic-sessionmanager/logout-all
|
|
11256
11320
|
*/
|
|
11257
11321
|
async logoutAll(ctx) {
|
|
@@ -11261,14 +11325,82 @@ var session$3 = {
|
|
|
11261
11325
|
return ctx.unauthorized("Authentication required");
|
|
11262
11326
|
}
|
|
11263
11327
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11264
|
-
await sessionService.terminateSession({
|
|
11265
|
-
|
|
11266
|
-
|
|
11328
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11329
|
+
userId: userDocId,
|
|
11330
|
+
reason: "manual"
|
|
11331
|
+
});
|
|
11332
|
+
strapi.log.info(
|
|
11333
|
+
`[magic-sessionmanager] User ${userDocId} logged out from all devices (${terminatedCount})`
|
|
11334
|
+
);
|
|
11335
|
+
ctx.body = {
|
|
11336
|
+
message: "Logged out from all devices successfully",
|
|
11337
|
+
terminatedCount
|
|
11338
|
+
};
|
|
11267
11339
|
} catch (err) {
|
|
11268
11340
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
11269
11341
|
ctx.throw(500, "Error during logout");
|
|
11270
11342
|
}
|
|
11271
11343
|
},
|
|
11344
|
+
/**
|
|
11345
|
+
* Terminates every session of the authenticated user EXCEPT the current
|
|
11346
|
+
* one. This is the "kick everyone else off my account" flow — the
|
|
11347
|
+
* caller stays logged in on the device they are using.
|
|
11348
|
+
*
|
|
11349
|
+
* If no "current session" record can be located for the caller's JWT
|
|
11350
|
+
* (edge case: user logged in before the session-manager was installed,
|
|
11351
|
+
* or right at the end of the grace window) we fall back to terminating
|
|
11352
|
+
* ALL sessions so the user still gets the safety effect they asked for,
|
|
11353
|
+
* and we report `fellBackToLogoutAll: true` so the client can adjust
|
|
11354
|
+
* its success message.
|
|
11355
|
+
*
|
|
11356
|
+
* @route POST /api/magic-sessionmanager/logout-others
|
|
11357
|
+
*/
|
|
11358
|
+
async logoutOthers(ctx) {
|
|
11359
|
+
try {
|
|
11360
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11361
|
+
const token = extractBearerToken$1(ctx);
|
|
11362
|
+
if (!userDocId || !token) {
|
|
11363
|
+
return ctx.unauthorized("Authentication required");
|
|
11364
|
+
}
|
|
11365
|
+
const currentTokenHash = hashToken$2(token);
|
|
11366
|
+
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11367
|
+
filters: { user: { documentId: userDocId }, tokenHash: currentTokenHash },
|
|
11368
|
+
fields: ["documentId"]
|
|
11369
|
+
});
|
|
11370
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11371
|
+
if (!currentSession) {
|
|
11372
|
+
const { terminatedCount: terminatedCount2 } = await sessionService.terminateSession({
|
|
11373
|
+
userId: userDocId,
|
|
11374
|
+
reason: "manual"
|
|
11375
|
+
});
|
|
11376
|
+
strapi.log.warn(
|
|
11377
|
+
`[magic-sessionmanager] logoutOthers fell back to logoutAll for user ${userDocId} (no current session match)`
|
|
11378
|
+
);
|
|
11379
|
+
ctx.body = {
|
|
11380
|
+
message: "All sessions terminated (could not preserve current session)",
|
|
11381
|
+
terminatedCount: terminatedCount2,
|
|
11382
|
+
fellBackToLogoutAll: true
|
|
11383
|
+
};
|
|
11384
|
+
return;
|
|
11385
|
+
}
|
|
11386
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11387
|
+
userId: userDocId,
|
|
11388
|
+
exceptSessionId: currentSession.documentId,
|
|
11389
|
+
reason: "manual"
|
|
11390
|
+
});
|
|
11391
|
+
strapi.log.info(
|
|
11392
|
+
`[magic-sessionmanager] User ${userDocId} logged out ${terminatedCount} other device(s)`
|
|
11393
|
+
);
|
|
11394
|
+
ctx.body = {
|
|
11395
|
+
message: terminatedCount === 0 ? "No other active sessions to terminate" : `${terminatedCount} other session(s) terminated`,
|
|
11396
|
+
terminatedCount,
|
|
11397
|
+
currentSessionPreserved: true
|
|
11398
|
+
};
|
|
11399
|
+
} catch (err) {
|
|
11400
|
+
strapi.log.error("[magic-sessionmanager] Logout-others error:", err);
|
|
11401
|
+
ctx.throw(500, "Error terminating other sessions");
|
|
11402
|
+
}
|
|
11403
|
+
},
|
|
11272
11404
|
/**
|
|
11273
11405
|
* Returns the session associated with the current JWT.
|
|
11274
11406
|
*
|
|
@@ -12123,9 +12255,9 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12123
12255
|
}
|
|
12124
12256
|
},
|
|
12125
12257
|
/**
|
|
12126
|
-
* Terminates a single session
|
|
12127
|
-
* reason so the JWT-verify wrapper
|
|
12128
|
-
* communicate the cause to the client
|
|
12258
|
+
* Terminates a single session, all sessions of a user, or all sessions
|
|
12259
|
+
* of a user EXCEPT one with a typed reason so the JWT-verify wrapper
|
|
12260
|
+
* can communicate the cause to the client.
|
|
12129
12261
|
*
|
|
12130
12262
|
* Supported reasons:
|
|
12131
12263
|
* - 'manual': user clicked logout, or admin terminated a session
|
|
@@ -12139,12 +12271,18 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12139
12271
|
* to work, while new code relies on `terminationReason`.
|
|
12140
12272
|
*
|
|
12141
12273
|
* @param {Object} params
|
|
12142
|
-
* @param {string} [params.sessionId]
|
|
12143
|
-
* @param {string|number} [params.userId]
|
|
12274
|
+
* @param {string} [params.sessionId] Terminate exactly this session
|
|
12275
|
+
* @param {string|number} [params.userId] Terminate every active session
|
|
12276
|
+
* of this user …
|
|
12277
|
+
* @param {string} [params.exceptSessionId] … except for this one. Only
|
|
12278
|
+
* meaningful together with
|
|
12279
|
+
* `userId`. Used by
|
|
12280
|
+
* /logout-other-devices so
|
|
12281
|
+
* the caller stays logged in.
|
|
12144
12282
|
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
12145
|
-
* @returns {Promise<
|
|
12283
|
+
* @returns {Promise<{terminatedCount: number}>}
|
|
12146
12284
|
*/
|
|
12147
|
-
async terminateSession({ sessionId, userId, reason = "manual" }) {
|
|
12285
|
+
async terminateSession({ sessionId, userId, exceptSessionId = null, reason = "manual" }) {
|
|
12148
12286
|
try {
|
|
12149
12287
|
const now = /* @__PURE__ */ new Date();
|
|
12150
12288
|
const validReasons = ["manual", "idle", "expired", "blocked"];
|
|
@@ -12162,29 +12300,46 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12162
12300
|
});
|
|
12163
12301
|
if (!existing) {
|
|
12164
12302
|
log.warn(`Session ${sessionId} not found for termination`);
|
|
12165
|
-
return;
|
|
12303
|
+
return { terminatedCount: 0 };
|
|
12166
12304
|
}
|
|
12167
12305
|
await strapi2.documents(SESSION_UID$1).update({
|
|
12168
12306
|
documentId: sessionId,
|
|
12169
12307
|
data: updateData
|
|
12170
12308
|
});
|
|
12171
12309
|
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
12172
|
-
|
|
12310
|
+
return { terminatedCount: 1 };
|
|
12311
|
+
}
|
|
12312
|
+
if (userId) {
|
|
12173
12313
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
12174
|
-
if (!userDocumentId) return;
|
|
12314
|
+
if (!userDocumentId) return { terminatedCount: 0 };
|
|
12315
|
+
const filters2 = { user: { documentId: userDocumentId }, isActive: true };
|
|
12316
|
+
if (exceptSessionId) {
|
|
12317
|
+
filters2.documentId = { $ne: exceptSessionId };
|
|
12318
|
+
}
|
|
12175
12319
|
const activeSessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
12176
|
-
filters:
|
|
12320
|
+
filters: filters2,
|
|
12177
12321
|
fields: ["documentId"],
|
|
12178
12322
|
limit: MAX_SESSIONS_QUERY
|
|
12179
12323
|
});
|
|
12324
|
+
let terminatedCount = 0;
|
|
12180
12325
|
for (const session2 of activeSessions) {
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12326
|
+
try {
|
|
12327
|
+
await strapi2.documents(SESSION_UID$1).update({
|
|
12328
|
+
documentId: session2.documentId,
|
|
12329
|
+
data: updateData
|
|
12330
|
+
});
|
|
12331
|
+
terminatedCount++;
|
|
12332
|
+
} catch (err) {
|
|
12333
|
+
log.debug(`Failed to terminate session ${session2.documentId}:`, err.message);
|
|
12334
|
+
}
|
|
12185
12335
|
}
|
|
12186
|
-
|
|
12336
|
+
const label = exceptSessionId ? "OTHER sessions" : "ALL sessions";
|
|
12337
|
+
log.info(
|
|
12338
|
+
`${label} terminated for user ${userDocumentId} (reason: ${finalReason}, count: ${terminatedCount})`
|
|
12339
|
+
);
|
|
12340
|
+
return { terminatedCount };
|
|
12187
12341
|
}
|
|
12342
|
+
return { terminatedCount: 0 };
|
|
12188
12343
|
} catch (err) {
|
|
12189
12344
|
log.error("Error terminating session:", err);
|
|
12190
12345
|
throw err;
|
|
@@ -12544,7 +12699,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12544
12699
|
}
|
|
12545
12700
|
};
|
|
12546
12701
|
};
|
|
12547
|
-
const version$1 = "4.5.
|
|
12702
|
+
const version$1 = "4.5.4";
|
|
12548
12703
|
const require$$2 = {
|
|
12549
12704
|
version: version$1
|
|
12550
12705
|
};
|
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 ==================
|
|
@@ -10733,17 +10763,26 @@ var contentApi$1 = {
|
|
|
10733
10763
|
}
|
|
10734
10764
|
]
|
|
10735
10765
|
};
|
|
10766
|
+
const PLUGIN_ACCESS_ACTION = "plugin::magic-sessionmanager.access";
|
|
10767
|
+
const adminPolicy = () => [
|
|
10768
|
+
"admin::isAuthenticatedAdmin",
|
|
10769
|
+
{
|
|
10770
|
+
name: "admin::hasPermissions",
|
|
10771
|
+
config: { actions: [PLUGIN_ACCESS_ACTION] }
|
|
10772
|
+
}
|
|
10773
|
+
];
|
|
10736
10774
|
const isDevEnvironment = (() => {
|
|
10737
10775
|
const env2 = (process.env.NODE_ENV || "development").toLowerCase();
|
|
10738
10776
|
return env2 !== "production" && env2 !== "staging";
|
|
10739
10777
|
})();
|
|
10740
10778
|
const baseRoutes = [
|
|
10779
|
+
// ============================ SESSIONS ============================
|
|
10741
10780
|
{
|
|
10742
10781
|
method: "GET",
|
|
10743
10782
|
path: "/sessions",
|
|
10744
10783
|
handler: "session.getAllSessionsAdmin",
|
|
10745
10784
|
config: {
|
|
10746
|
-
policies:
|
|
10785
|
+
policies: adminPolicy(),
|
|
10747
10786
|
description: "Get all sessions - active and inactive (admin)"
|
|
10748
10787
|
}
|
|
10749
10788
|
},
|
|
@@ -10752,7 +10791,7 @@ const baseRoutes = [
|
|
|
10752
10791
|
path: "/sessions/active",
|
|
10753
10792
|
handler: "session.getActiveSessions",
|
|
10754
10793
|
config: {
|
|
10755
|
-
policies:
|
|
10794
|
+
policies: adminPolicy(),
|
|
10756
10795
|
description: "Get only active sessions (admin)"
|
|
10757
10796
|
}
|
|
10758
10797
|
},
|
|
@@ -10761,8 +10800,8 @@ const baseRoutes = [
|
|
|
10761
10800
|
path: "/user/:userId/sessions",
|
|
10762
10801
|
handler: "session.getUserSessions",
|
|
10763
10802
|
config: {
|
|
10764
|
-
policies:
|
|
10765
|
-
description: "Get user
|
|
10803
|
+
policies: adminPolicy(),
|
|
10804
|
+
description: "Get sessions for a specific user (admin)"
|
|
10766
10805
|
}
|
|
10767
10806
|
},
|
|
10768
10807
|
{
|
|
@@ -10770,7 +10809,7 @@ const baseRoutes = [
|
|
|
10770
10809
|
path: "/sessions/:sessionId/terminate",
|
|
10771
10810
|
handler: "session.terminateSingleSession",
|
|
10772
10811
|
config: {
|
|
10773
|
-
policies:
|
|
10812
|
+
policies: adminPolicy(),
|
|
10774
10813
|
description: "Terminate a specific session (admin)"
|
|
10775
10814
|
}
|
|
10776
10815
|
},
|
|
@@ -10779,7 +10818,7 @@ const baseRoutes = [
|
|
|
10779
10818
|
path: "/sessions/:sessionId",
|
|
10780
10819
|
handler: "session.deleteSession",
|
|
10781
10820
|
config: {
|
|
10782
|
-
policies:
|
|
10821
|
+
policies: adminPolicy(),
|
|
10783
10822
|
description: "Delete a single session permanently (admin)"
|
|
10784
10823
|
}
|
|
10785
10824
|
},
|
|
@@ -10788,7 +10827,7 @@ const baseRoutes = [
|
|
|
10788
10827
|
path: "/sessions/clean-inactive",
|
|
10789
10828
|
handler: "session.cleanInactiveSessions",
|
|
10790
10829
|
config: {
|
|
10791
|
-
policies:
|
|
10830
|
+
policies: adminPolicy(),
|
|
10792
10831
|
description: "Delete all inactive sessions from database (admin)"
|
|
10793
10832
|
}
|
|
10794
10833
|
},
|
|
@@ -10797,7 +10836,7 @@ const baseRoutes = [
|
|
|
10797
10836
|
path: "/user/:userId/terminate-all",
|
|
10798
10837
|
handler: "session.terminateAllUserSessions",
|
|
10799
10838
|
config: {
|
|
10800
|
-
policies:
|
|
10839
|
+
policies: adminPolicy(),
|
|
10801
10840
|
description: "Terminate all sessions for a user (admin)"
|
|
10802
10841
|
}
|
|
10803
10842
|
},
|
|
@@ -10806,56 +10845,74 @@ const baseRoutes = [
|
|
|
10806
10845
|
path: "/user/:userId/toggle-block",
|
|
10807
10846
|
handler: "session.toggleUserBlock",
|
|
10808
10847
|
config: {
|
|
10809
|
-
policies:
|
|
10848
|
+
policies: adminPolicy(),
|
|
10810
10849
|
description: "Toggle user blocked status (admin)"
|
|
10811
10850
|
}
|
|
10812
10851
|
},
|
|
10852
|
+
// ============================ LICENSE ============================
|
|
10813
10853
|
{
|
|
10814
10854
|
method: "GET",
|
|
10815
10855
|
path: "/license/status",
|
|
10816
10856
|
handler: "license.getStatus",
|
|
10817
|
-
config: {
|
|
10857
|
+
config: {
|
|
10858
|
+
policies: adminPolicy(),
|
|
10859
|
+
description: "Get license status (admin)"
|
|
10860
|
+
}
|
|
10818
10861
|
},
|
|
10819
10862
|
{
|
|
10820
10863
|
method: "POST",
|
|
10821
10864
|
path: "/license/auto-create",
|
|
10822
10865
|
handler: "license.autoCreate",
|
|
10823
|
-
config: {
|
|
10866
|
+
config: {
|
|
10867
|
+
policies: adminPolicy(),
|
|
10868
|
+
description: "Auto-create license for current admin (admin)"
|
|
10869
|
+
}
|
|
10824
10870
|
},
|
|
10825
10871
|
{
|
|
10826
10872
|
method: "POST",
|
|
10827
10873
|
path: "/license/create",
|
|
10828
10874
|
handler: "license.createAndActivate",
|
|
10829
|
-
config: {
|
|
10875
|
+
config: {
|
|
10876
|
+
policies: adminPolicy(),
|
|
10877
|
+
description: "Create and activate a new license (admin)"
|
|
10878
|
+
}
|
|
10830
10879
|
},
|
|
10831
10880
|
{
|
|
10832
10881
|
method: "POST",
|
|
10833
10882
|
path: "/license/ping",
|
|
10834
10883
|
handler: "license.ping",
|
|
10835
|
-
config: {
|
|
10884
|
+
config: {
|
|
10885
|
+
policies: adminPolicy(),
|
|
10886
|
+
description: "Ping the license server (admin)"
|
|
10887
|
+
}
|
|
10836
10888
|
},
|
|
10837
10889
|
{
|
|
10838
10890
|
method: "POST",
|
|
10839
10891
|
path: "/license/store-key",
|
|
10840
10892
|
handler: "license.storeKey",
|
|
10841
|
-
config: {
|
|
10893
|
+
config: {
|
|
10894
|
+
policies: adminPolicy(),
|
|
10895
|
+
description: "Store a license key (admin)"
|
|
10896
|
+
}
|
|
10842
10897
|
},
|
|
10898
|
+
// ============================ GEOLOCATION ============================
|
|
10843
10899
|
{
|
|
10844
10900
|
method: "GET",
|
|
10845
10901
|
path: "/geolocation/:ipAddress",
|
|
10846
10902
|
handler: "session.getIpGeolocation",
|
|
10847
10903
|
config: {
|
|
10848
|
-
policies:
|
|
10849
|
-
description: "Get IP geolocation data (Premium feature)"
|
|
10904
|
+
policies: adminPolicy(),
|
|
10905
|
+
description: "Get IP geolocation data (Premium feature, admin)"
|
|
10850
10906
|
}
|
|
10851
10907
|
},
|
|
10908
|
+
// ============================ SETTINGS ============================
|
|
10852
10909
|
{
|
|
10853
10910
|
method: "GET",
|
|
10854
10911
|
path: "/settings",
|
|
10855
10912
|
handler: "settings.getSettings",
|
|
10856
10913
|
config: {
|
|
10857
|
-
policies:
|
|
10858
|
-
description: "Get plugin settings"
|
|
10914
|
+
policies: adminPolicy(),
|
|
10915
|
+
description: "Get plugin settings (admin)"
|
|
10859
10916
|
}
|
|
10860
10917
|
},
|
|
10861
10918
|
{
|
|
@@ -10863,8 +10920,8 @@ const baseRoutes = [
|
|
|
10863
10920
|
path: "/settings",
|
|
10864
10921
|
handler: "settings.updateSettings",
|
|
10865
10922
|
config: {
|
|
10866
|
-
policies:
|
|
10867
|
-
description: "Update plugin settings"
|
|
10923
|
+
policies: adminPolicy(),
|
|
10924
|
+
description: "Update plugin settings (admin)"
|
|
10868
10925
|
}
|
|
10869
10926
|
}
|
|
10870
10927
|
];
|
|
@@ -10874,8 +10931,8 @@ const devOnlyRoutes = [
|
|
|
10874
10931
|
path: "/sessions/:sessionId/simulate-timeout",
|
|
10875
10932
|
handler: "session.simulateTimeout",
|
|
10876
10933
|
config: {
|
|
10877
|
-
policies:
|
|
10878
|
-
description: "Simulate session timeout (dev-only)"
|
|
10934
|
+
policies: adminPolicy(),
|
|
10935
|
+
description: "Simulate session timeout (dev-only, admin)"
|
|
10879
10936
|
}
|
|
10880
10937
|
}
|
|
10881
10938
|
];
|
|
@@ -11238,7 +11295,14 @@ var session$3 = {
|
|
|
11238
11295
|
}
|
|
11239
11296
|
},
|
|
11240
11297
|
/**
|
|
11241
|
-
* Terminates
|
|
11298
|
+
* Terminates EVERY session of the authenticated user — including the
|
|
11299
|
+
* current one. After this call the caller will also be logged out on
|
|
11300
|
+
* the next request (their own JWT is rejected by the JWT-verify wrapper
|
|
11301
|
+
* with reason=manual).
|
|
11302
|
+
*
|
|
11303
|
+
* For the "log me out everywhere ELSE but keep me here" flow, use
|
|
11304
|
+
* `/logout-others` below.
|
|
11305
|
+
*
|
|
11242
11306
|
* @route POST /api/magic-sessionmanager/logout-all
|
|
11243
11307
|
*/
|
|
11244
11308
|
async logoutAll(ctx) {
|
|
@@ -11248,14 +11312,82 @@ var session$3 = {
|
|
|
11248
11312
|
return ctx.unauthorized("Authentication required");
|
|
11249
11313
|
}
|
|
11250
11314
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11251
|
-
await sessionService.terminateSession({
|
|
11252
|
-
|
|
11253
|
-
|
|
11315
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11316
|
+
userId: userDocId,
|
|
11317
|
+
reason: "manual"
|
|
11318
|
+
});
|
|
11319
|
+
strapi.log.info(
|
|
11320
|
+
`[magic-sessionmanager] User ${userDocId} logged out from all devices (${terminatedCount})`
|
|
11321
|
+
);
|
|
11322
|
+
ctx.body = {
|
|
11323
|
+
message: "Logged out from all devices successfully",
|
|
11324
|
+
terminatedCount
|
|
11325
|
+
};
|
|
11254
11326
|
} catch (err) {
|
|
11255
11327
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
11256
11328
|
ctx.throw(500, "Error during logout");
|
|
11257
11329
|
}
|
|
11258
11330
|
},
|
|
11331
|
+
/**
|
|
11332
|
+
* Terminates every session of the authenticated user EXCEPT the current
|
|
11333
|
+
* one. This is the "kick everyone else off my account" flow — the
|
|
11334
|
+
* caller stays logged in on the device they are using.
|
|
11335
|
+
*
|
|
11336
|
+
* If no "current session" record can be located for the caller's JWT
|
|
11337
|
+
* (edge case: user logged in before the session-manager was installed,
|
|
11338
|
+
* or right at the end of the grace window) we fall back to terminating
|
|
11339
|
+
* ALL sessions so the user still gets the safety effect they asked for,
|
|
11340
|
+
* and we report `fellBackToLogoutAll: true` so the client can adjust
|
|
11341
|
+
* its success message.
|
|
11342
|
+
*
|
|
11343
|
+
* @route POST /api/magic-sessionmanager/logout-others
|
|
11344
|
+
*/
|
|
11345
|
+
async logoutOthers(ctx) {
|
|
11346
|
+
try {
|
|
11347
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11348
|
+
const token = extractBearerToken$1(ctx);
|
|
11349
|
+
if (!userDocId || !token) {
|
|
11350
|
+
return ctx.unauthorized("Authentication required");
|
|
11351
|
+
}
|
|
11352
|
+
const currentTokenHash = hashToken$2(token);
|
|
11353
|
+
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11354
|
+
filters: { user: { documentId: userDocId }, tokenHash: currentTokenHash },
|
|
11355
|
+
fields: ["documentId"]
|
|
11356
|
+
});
|
|
11357
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11358
|
+
if (!currentSession) {
|
|
11359
|
+
const { terminatedCount: terminatedCount2 } = await sessionService.terminateSession({
|
|
11360
|
+
userId: userDocId,
|
|
11361
|
+
reason: "manual"
|
|
11362
|
+
});
|
|
11363
|
+
strapi.log.warn(
|
|
11364
|
+
`[magic-sessionmanager] logoutOthers fell back to logoutAll for user ${userDocId} (no current session match)`
|
|
11365
|
+
);
|
|
11366
|
+
ctx.body = {
|
|
11367
|
+
message: "All sessions terminated (could not preserve current session)",
|
|
11368
|
+
terminatedCount: terminatedCount2,
|
|
11369
|
+
fellBackToLogoutAll: true
|
|
11370
|
+
};
|
|
11371
|
+
return;
|
|
11372
|
+
}
|
|
11373
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11374
|
+
userId: userDocId,
|
|
11375
|
+
exceptSessionId: currentSession.documentId,
|
|
11376
|
+
reason: "manual"
|
|
11377
|
+
});
|
|
11378
|
+
strapi.log.info(
|
|
11379
|
+
`[magic-sessionmanager] User ${userDocId} logged out ${terminatedCount} other device(s)`
|
|
11380
|
+
);
|
|
11381
|
+
ctx.body = {
|
|
11382
|
+
message: terminatedCount === 0 ? "No other active sessions to terminate" : `${terminatedCount} other session(s) terminated`,
|
|
11383
|
+
terminatedCount,
|
|
11384
|
+
currentSessionPreserved: true
|
|
11385
|
+
};
|
|
11386
|
+
} catch (err) {
|
|
11387
|
+
strapi.log.error("[magic-sessionmanager] Logout-others error:", err);
|
|
11388
|
+
ctx.throw(500, "Error terminating other sessions");
|
|
11389
|
+
}
|
|
11390
|
+
},
|
|
11259
11391
|
/**
|
|
11260
11392
|
* Returns the session associated with the current JWT.
|
|
11261
11393
|
*
|
|
@@ -12110,9 +12242,9 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12110
12242
|
}
|
|
12111
12243
|
},
|
|
12112
12244
|
/**
|
|
12113
|
-
* Terminates a single session
|
|
12114
|
-
* reason so the JWT-verify wrapper
|
|
12115
|
-
* communicate the cause to the client
|
|
12245
|
+
* Terminates a single session, all sessions of a user, or all sessions
|
|
12246
|
+
* of a user EXCEPT one with a typed reason so the JWT-verify wrapper
|
|
12247
|
+
* can communicate the cause to the client.
|
|
12116
12248
|
*
|
|
12117
12249
|
* Supported reasons:
|
|
12118
12250
|
* - 'manual': user clicked logout, or admin terminated a session
|
|
@@ -12126,12 +12258,18 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12126
12258
|
* to work, while new code relies on `terminationReason`.
|
|
12127
12259
|
*
|
|
12128
12260
|
* @param {Object} params
|
|
12129
|
-
* @param {string} [params.sessionId]
|
|
12130
|
-
* @param {string|number} [params.userId]
|
|
12261
|
+
* @param {string} [params.sessionId] Terminate exactly this session
|
|
12262
|
+
* @param {string|number} [params.userId] Terminate every active session
|
|
12263
|
+
* of this user …
|
|
12264
|
+
* @param {string} [params.exceptSessionId] … except for this one. Only
|
|
12265
|
+
* meaningful together with
|
|
12266
|
+
* `userId`. Used by
|
|
12267
|
+
* /logout-other-devices so
|
|
12268
|
+
* the caller stays logged in.
|
|
12131
12269
|
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
12132
|
-
* @returns {Promise<
|
|
12270
|
+
* @returns {Promise<{terminatedCount: number}>}
|
|
12133
12271
|
*/
|
|
12134
|
-
async terminateSession({ sessionId, userId, reason = "manual" }) {
|
|
12272
|
+
async terminateSession({ sessionId, userId, exceptSessionId = null, reason = "manual" }) {
|
|
12135
12273
|
try {
|
|
12136
12274
|
const now = /* @__PURE__ */ new Date();
|
|
12137
12275
|
const validReasons = ["manual", "idle", "expired", "blocked"];
|
|
@@ -12149,29 +12287,46 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12149
12287
|
});
|
|
12150
12288
|
if (!existing) {
|
|
12151
12289
|
log.warn(`Session ${sessionId} not found for termination`);
|
|
12152
|
-
return;
|
|
12290
|
+
return { terminatedCount: 0 };
|
|
12153
12291
|
}
|
|
12154
12292
|
await strapi2.documents(SESSION_UID$1).update({
|
|
12155
12293
|
documentId: sessionId,
|
|
12156
12294
|
data: updateData
|
|
12157
12295
|
});
|
|
12158
12296
|
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
12159
|
-
|
|
12297
|
+
return { terminatedCount: 1 };
|
|
12298
|
+
}
|
|
12299
|
+
if (userId) {
|
|
12160
12300
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
12161
|
-
if (!userDocumentId) return;
|
|
12301
|
+
if (!userDocumentId) return { terminatedCount: 0 };
|
|
12302
|
+
const filters2 = { user: { documentId: userDocumentId }, isActive: true };
|
|
12303
|
+
if (exceptSessionId) {
|
|
12304
|
+
filters2.documentId = { $ne: exceptSessionId };
|
|
12305
|
+
}
|
|
12162
12306
|
const activeSessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
12163
|
-
filters:
|
|
12307
|
+
filters: filters2,
|
|
12164
12308
|
fields: ["documentId"],
|
|
12165
12309
|
limit: MAX_SESSIONS_QUERY
|
|
12166
12310
|
});
|
|
12311
|
+
let terminatedCount = 0;
|
|
12167
12312
|
for (const session2 of activeSessions) {
|
|
12168
|
-
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
|
|
12313
|
+
try {
|
|
12314
|
+
await strapi2.documents(SESSION_UID$1).update({
|
|
12315
|
+
documentId: session2.documentId,
|
|
12316
|
+
data: updateData
|
|
12317
|
+
});
|
|
12318
|
+
terminatedCount++;
|
|
12319
|
+
} catch (err) {
|
|
12320
|
+
log.debug(`Failed to terminate session ${session2.documentId}:`, err.message);
|
|
12321
|
+
}
|
|
12172
12322
|
}
|
|
12173
|
-
|
|
12323
|
+
const label = exceptSessionId ? "OTHER sessions" : "ALL sessions";
|
|
12324
|
+
log.info(
|
|
12325
|
+
`${label} terminated for user ${userDocumentId} (reason: ${finalReason}, count: ${terminatedCount})`
|
|
12326
|
+
);
|
|
12327
|
+
return { terminatedCount };
|
|
12174
12328
|
}
|
|
12329
|
+
return { terminatedCount: 0 };
|
|
12175
12330
|
} catch (err) {
|
|
12176
12331
|
log.error("Error terminating session:", err);
|
|
12177
12332
|
throw err;
|
|
@@ -12531,7 +12686,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12531
12686
|
}
|
|
12532
12687
|
};
|
|
12533
12688
|
};
|
|
12534
|
-
const version$1 = "4.5.
|
|
12689
|
+
const version$1 = "4.5.4";
|
|
12535
12690
|
const require$$2 = {
|
|
12536
12691
|
version: version$1
|
|
12537
12692
|
};
|
package/package.json
CHANGED