strapi-plugin-magic-sessionmanager 4.2.5 → 4.2.7
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 +0 -2
- package/dist/server/index.js +201 -22
- package/dist/server/index.mjs +201 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,8 +7,6 @@ Track logins, monitor active users, and secure your app with one simple plugin.
|
|
|
7
7
|
[](https://www.npmjs.com/package/strapi-plugin-magic-sessionmanager)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
> 💡 **Tip:** Set `debug: true` in plugin config to enable detailed logging
|
|
11
|
-
|
|
12
10
|
---
|
|
13
11
|
|
|
14
12
|
## 📸 What It Looks Like
|
package/dist/server/index.js
CHANGED
|
@@ -179,7 +179,7 @@ function encryptToken$2(token) {
|
|
|
179
179
|
throw new Error("Failed to encrypt token");
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
function decryptToken$
|
|
182
|
+
function decryptToken$4(encryptedToken) {
|
|
183
183
|
if (!encryptedToken) return null;
|
|
184
184
|
try {
|
|
185
185
|
const key = getEncryptionKey();
|
|
@@ -208,38 +208,59 @@ function generateSessionId$1(userId) {
|
|
|
208
208
|
}
|
|
209
209
|
var encryption = {
|
|
210
210
|
encryptToken: encryptToken$2,
|
|
211
|
-
decryptToken: decryptToken$
|
|
211
|
+
decryptToken: decryptToken$4,
|
|
212
212
|
generateSessionId: generateSessionId$1
|
|
213
213
|
};
|
|
214
214
|
const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
|
|
215
|
+
const { decryptToken: decryptToken$3 } = encryption;
|
|
215
216
|
var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
216
217
|
return async (ctx, next) => {
|
|
217
218
|
if (ctx.state.user && ctx.state.user.documentId) {
|
|
218
219
|
try {
|
|
219
220
|
const userId = ctx.state.user.documentId;
|
|
221
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
222
|
+
if (!currentToken) {
|
|
223
|
+
await next();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
220
226
|
const activeSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
221
227
|
filters: {
|
|
222
228
|
user: { documentId: userId },
|
|
223
229
|
isActive: true
|
|
224
|
-
}
|
|
225
|
-
limit: 1
|
|
230
|
+
}
|
|
226
231
|
});
|
|
227
232
|
if (!activeSessions || activeSessions.length === 0) {
|
|
228
|
-
strapi2.log.info(`[magic-sessionmanager] [BLOCKED]
|
|
233
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] User ${userId} has no active sessions`);
|
|
229
234
|
return ctx.unauthorized("All sessions have been terminated. Please login again.");
|
|
230
235
|
}
|
|
236
|
+
let matchingSession = null;
|
|
237
|
+
for (const session2 of activeSessions) {
|
|
238
|
+
if (!session2.token) continue;
|
|
239
|
+
try {
|
|
240
|
+
const decrypted = decryptToken$3(session2.token);
|
|
241
|
+
if (decrypted === currentToken) {
|
|
242
|
+
matchingSession = session2;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!matchingSession) {
|
|
249
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] Session for user ${userId} has been terminated`);
|
|
250
|
+
return ctx.unauthorized("This session has been terminated. Please login again.");
|
|
251
|
+
}
|
|
252
|
+
ctx.state.sessionId = matchingSession.documentId;
|
|
253
|
+
ctx.state.currentSession = matchingSession;
|
|
231
254
|
} catch (err) {
|
|
232
|
-
strapi2.log.debug("[magic-sessionmanager] Error checking
|
|
255
|
+
strapi2.log.debug("[magic-sessionmanager] Error checking session:", err.message);
|
|
233
256
|
}
|
|
234
257
|
}
|
|
235
258
|
await next();
|
|
236
|
-
if (ctx.state.user && ctx.state.user.documentId) {
|
|
259
|
+
if (ctx.state.user && ctx.state.user.documentId && ctx.state.sessionId) {
|
|
237
260
|
try {
|
|
238
|
-
const userId = ctx.state.user.documentId;
|
|
239
|
-
const sessionId = ctx.state.sessionId;
|
|
240
261
|
await sessionService.touch({
|
|
241
|
-
userId,
|
|
242
|
-
sessionId
|
|
262
|
+
userId: ctx.state.user.documentId,
|
|
263
|
+
sessionId: ctx.state.sessionId
|
|
243
264
|
});
|
|
244
265
|
} catch (err) {
|
|
245
266
|
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
@@ -562,9 +583,13 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
562
583
|
};
|
|
563
584
|
async function ensureContentApiPermissions(strapi2, log) {
|
|
564
585
|
try {
|
|
565
|
-
const
|
|
566
|
-
|
|
586
|
+
const ROLE_UID = "plugin::users-permissions.role";
|
|
587
|
+
const PERMISSION_UID = "plugin::users-permissions.permission";
|
|
588
|
+
const roles = await strapi2.entityService.findMany(ROLE_UID, {
|
|
589
|
+
filters: { type: "authenticated" },
|
|
590
|
+
limit: 1
|
|
567
591
|
});
|
|
592
|
+
const authenticatedRole = roles?.[0];
|
|
568
593
|
if (!authenticatedRole) {
|
|
569
594
|
log.warn("Authenticated role not found - skipping permission setup");
|
|
570
595
|
return;
|
|
@@ -573,10 +598,12 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
573
598
|
"plugin::magic-sessionmanager.session.logout",
|
|
574
599
|
"plugin::magic-sessionmanager.session.logoutAll",
|
|
575
600
|
"plugin::magic-sessionmanager.session.getOwnSessions",
|
|
576
|
-
"plugin::magic-sessionmanager.session.getUserSessions"
|
|
601
|
+
"plugin::magic-sessionmanager.session.getUserSessions",
|
|
602
|
+
"plugin::magic-sessionmanager.session.getCurrentSession",
|
|
603
|
+
"plugin::magic-sessionmanager.session.terminateOwnSession"
|
|
577
604
|
];
|
|
578
|
-
const existingPermissions = await strapi2.
|
|
579
|
-
|
|
605
|
+
const existingPermissions = await strapi2.entityService.findMany(PERMISSION_UID, {
|
|
606
|
+
filters: {
|
|
580
607
|
role: authenticatedRole.id,
|
|
581
608
|
action: { $in: requiredActions }
|
|
582
609
|
}
|
|
@@ -588,7 +615,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
588
615
|
return;
|
|
589
616
|
}
|
|
590
617
|
for (const action of missingActions) {
|
|
591
|
-
await strapi2.
|
|
618
|
+
await strapi2.entityService.create(PERMISSION_UID, {
|
|
592
619
|
data: {
|
|
593
620
|
action,
|
|
594
621
|
role: authenticatedRole.id
|
|
@@ -767,6 +794,15 @@ var contentApi$1 = {
|
|
|
767
794
|
description: "Get own sessions (automatically uses authenticated user)"
|
|
768
795
|
}
|
|
769
796
|
},
|
|
797
|
+
{
|
|
798
|
+
method: "GET",
|
|
799
|
+
path: "/current-session",
|
|
800
|
+
handler: "session.getCurrentSession",
|
|
801
|
+
config: {
|
|
802
|
+
auth: { strategies: ["users-permissions"] },
|
|
803
|
+
description: "Get current session info based on JWT token"
|
|
804
|
+
}
|
|
805
|
+
},
|
|
770
806
|
{
|
|
771
807
|
method: "GET",
|
|
772
808
|
path: "/user/:userId/sessions",
|
|
@@ -775,6 +811,18 @@ var contentApi$1 = {
|
|
|
775
811
|
auth: { strategies: ["users-permissions"] },
|
|
776
812
|
description: "Get sessions by userId (validates user can only see own sessions)"
|
|
777
813
|
}
|
|
814
|
+
},
|
|
815
|
+
// ============================================================
|
|
816
|
+
// SESSION MANAGEMENT (for own sessions only)
|
|
817
|
+
// ============================================================
|
|
818
|
+
{
|
|
819
|
+
method: "DELETE",
|
|
820
|
+
path: "/my-sessions/:sessionId",
|
|
821
|
+
handler: "session.terminateOwnSession",
|
|
822
|
+
config: {
|
|
823
|
+
auth: { strategies: ["users-permissions"] },
|
|
824
|
+
description: "Terminate a specific own session (not current)"
|
|
825
|
+
}
|
|
778
826
|
}
|
|
779
827
|
]
|
|
780
828
|
};
|
|
@@ -977,19 +1025,52 @@ var session$3 = {
|
|
|
977
1025
|
* Get own sessions (authenticated user)
|
|
978
1026
|
* GET /api/magic-sessionmanager/my-sessions
|
|
979
1027
|
* Automatically uses the authenticated user's documentId
|
|
1028
|
+
* Marks which session is the current one (based on JWT token)
|
|
980
1029
|
*/
|
|
981
1030
|
async getOwnSessions(ctx) {
|
|
982
1031
|
try {
|
|
983
1032
|
const userId = ctx.state.user?.documentId;
|
|
1033
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
984
1034
|
if (!userId) {
|
|
985
1035
|
return ctx.throw(401, "Unauthorized");
|
|
986
1036
|
}
|
|
987
|
-
const
|
|
988
|
-
|
|
1037
|
+
const allSessions = await strapi.documents(SESSION_UID$1).findMany({
|
|
1038
|
+
filters: { user: { documentId: userId } },
|
|
1039
|
+
sort: { loginTime: "desc" }
|
|
1040
|
+
});
|
|
1041
|
+
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1042
|
+
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
1043
|
+
const now = /* @__PURE__ */ new Date();
|
|
1044
|
+
const sessionsWithCurrent = allSessions.map((session2) => {
|
|
1045
|
+
const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
|
|
1046
|
+
const timeSinceActive = now - lastActiveTime;
|
|
1047
|
+
const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
|
|
1048
|
+
let isCurrentSession = false;
|
|
1049
|
+
if (session2.token && currentToken) {
|
|
1050
|
+
try {
|
|
1051
|
+
const decrypted = decryptToken$1(session2.token);
|
|
1052
|
+
isCurrentSession = decrypted === currentToken;
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const { token, refreshToken, ...sessionWithoutTokens } = session2;
|
|
1057
|
+
return {
|
|
1058
|
+
...sessionWithoutTokens,
|
|
1059
|
+
isCurrentSession,
|
|
1060
|
+
isTrulyActive,
|
|
1061
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
|
|
1062
|
+
};
|
|
1063
|
+
});
|
|
1064
|
+
sessionsWithCurrent.sort((a, b) => {
|
|
1065
|
+
if (a.isCurrentSession) return -1;
|
|
1066
|
+
if (b.isCurrentSession) return 1;
|
|
1067
|
+
return new Date(b.loginTime) - new Date(a.loginTime);
|
|
1068
|
+
});
|
|
989
1069
|
ctx.body = {
|
|
990
|
-
data:
|
|
1070
|
+
data: sessionsWithCurrent,
|
|
991
1071
|
meta: {
|
|
992
|
-
count:
|
|
1072
|
+
count: sessionsWithCurrent.length,
|
|
1073
|
+
active: sessionsWithCurrent.filter((s) => s.isTrulyActive).length
|
|
993
1074
|
}
|
|
994
1075
|
};
|
|
995
1076
|
} catch (err) {
|
|
@@ -1084,6 +1165,104 @@ var session$3 = {
|
|
|
1084
1165
|
ctx.throw(500, "Error during logout");
|
|
1085
1166
|
}
|
|
1086
1167
|
},
|
|
1168
|
+
/**
|
|
1169
|
+
* Get current session info based on JWT token
|
|
1170
|
+
* GET /api/magic-sessionmanager/current-session
|
|
1171
|
+
* Returns the session associated with the current JWT token
|
|
1172
|
+
*/
|
|
1173
|
+
async getCurrentSession(ctx) {
|
|
1174
|
+
try {
|
|
1175
|
+
const userId = ctx.state.user?.documentId;
|
|
1176
|
+
const token = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
1177
|
+
if (!userId || !token) {
|
|
1178
|
+
return ctx.throw(401, "Unauthorized");
|
|
1179
|
+
}
|
|
1180
|
+
const sessions = await strapi.documents(SESSION_UID$1).findMany({
|
|
1181
|
+
filters: {
|
|
1182
|
+
user: { documentId: userId },
|
|
1183
|
+
isActive: true
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
const currentSession = sessions.find((session2) => {
|
|
1187
|
+
if (!session2.token) return false;
|
|
1188
|
+
try {
|
|
1189
|
+
const decrypted = decryptToken$1(session2.token);
|
|
1190
|
+
return decrypted === token;
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
if (!currentSession) {
|
|
1196
|
+
return ctx.notFound("Current session not found");
|
|
1197
|
+
}
|
|
1198
|
+
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1199
|
+
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
1200
|
+
const now = /* @__PURE__ */ new Date();
|
|
1201
|
+
const lastActiveTime = currentSession.lastActive ? new Date(currentSession.lastActive) : new Date(currentSession.loginTime);
|
|
1202
|
+
const timeSinceActive = now - lastActiveTime;
|
|
1203
|
+
const { token: _, refreshToken: __, ...sessionWithoutTokens } = currentSession;
|
|
1204
|
+
ctx.body = {
|
|
1205
|
+
data: {
|
|
1206
|
+
...sessionWithoutTokens,
|
|
1207
|
+
isCurrentSession: true,
|
|
1208
|
+
isTrulyActive: timeSinceActive < inactivityTimeout,
|
|
1209
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
strapi.log.error("[magic-sessionmanager] Error getting current session:", err);
|
|
1214
|
+
ctx.throw(500, "Error fetching current session");
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
/**
|
|
1218
|
+
* Terminate a specific own session (not the current one)
|
|
1219
|
+
* DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
1220
|
+
* SECURITY: User can only terminate their OWN sessions
|
|
1221
|
+
*/
|
|
1222
|
+
async terminateOwnSession(ctx) {
|
|
1223
|
+
try {
|
|
1224
|
+
const userId = ctx.state.user?.documentId;
|
|
1225
|
+
const { sessionId } = ctx.params;
|
|
1226
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
1227
|
+
if (!userId) {
|
|
1228
|
+
return ctx.throw(401, "Unauthorized");
|
|
1229
|
+
}
|
|
1230
|
+
if (!sessionId) {
|
|
1231
|
+
return ctx.badRequest("Session ID is required");
|
|
1232
|
+
}
|
|
1233
|
+
const sessionToTerminate = await strapi.documents(SESSION_UID$1).findOne({
|
|
1234
|
+
documentId: sessionId,
|
|
1235
|
+
populate: { user: { fields: ["documentId"] } }
|
|
1236
|
+
});
|
|
1237
|
+
if (!sessionToTerminate) {
|
|
1238
|
+
return ctx.notFound("Session not found");
|
|
1239
|
+
}
|
|
1240
|
+
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
1241
|
+
if (sessionUserId !== userId) {
|
|
1242
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${userId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
|
|
1243
|
+
return ctx.forbidden("You can only terminate your own sessions");
|
|
1244
|
+
}
|
|
1245
|
+
if (sessionToTerminate.token && currentToken) {
|
|
1246
|
+
try {
|
|
1247
|
+
const decrypted = decryptToken$1(sessionToTerminate.token);
|
|
1248
|
+
if (decrypted === currentToken) {
|
|
1249
|
+
return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
|
|
1250
|
+
}
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
1255
|
+
await sessionService.terminateSession({ sessionId });
|
|
1256
|
+
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
1257
|
+
ctx.body = {
|
|
1258
|
+
message: `Session ${sessionId} terminated successfully`,
|
|
1259
|
+
success: true
|
|
1260
|
+
};
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
1263
|
+
ctx.throw(500, "Error terminating session");
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1087
1266
|
/**
|
|
1088
1267
|
* Terminate specific session
|
|
1089
1268
|
* DELETE /magic-sessionmanager/sessions/:sessionId
|
|
@@ -1862,7 +2041,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
1862
2041
|
}
|
|
1863
2042
|
};
|
|
1864
2043
|
};
|
|
1865
|
-
const version = "4.2.
|
|
2044
|
+
const version = "4.2.7";
|
|
1866
2045
|
const require$$2 = {
|
|
1867
2046
|
version
|
|
1868
2047
|
};
|
package/dist/server/index.mjs
CHANGED
|
@@ -175,7 +175,7 @@ function encryptToken$2(token) {
|
|
|
175
175
|
throw new Error("Failed to encrypt token");
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
-
function decryptToken$
|
|
178
|
+
function decryptToken$4(encryptedToken) {
|
|
179
179
|
if (!encryptedToken) return null;
|
|
180
180
|
try {
|
|
181
181
|
const key = getEncryptionKey();
|
|
@@ -204,38 +204,59 @@ function generateSessionId$1(userId) {
|
|
|
204
204
|
}
|
|
205
205
|
var encryption = {
|
|
206
206
|
encryptToken: encryptToken$2,
|
|
207
|
-
decryptToken: decryptToken$
|
|
207
|
+
decryptToken: decryptToken$4,
|
|
208
208
|
generateSessionId: generateSessionId$1
|
|
209
209
|
};
|
|
210
210
|
const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
|
|
211
|
+
const { decryptToken: decryptToken$3 } = encryption;
|
|
211
212
|
var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
212
213
|
return async (ctx, next) => {
|
|
213
214
|
if (ctx.state.user && ctx.state.user.documentId) {
|
|
214
215
|
try {
|
|
215
216
|
const userId = ctx.state.user.documentId;
|
|
217
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
218
|
+
if (!currentToken) {
|
|
219
|
+
await next();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
216
222
|
const activeSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
217
223
|
filters: {
|
|
218
224
|
user: { documentId: userId },
|
|
219
225
|
isActive: true
|
|
220
|
-
}
|
|
221
|
-
limit: 1
|
|
226
|
+
}
|
|
222
227
|
});
|
|
223
228
|
if (!activeSessions || activeSessions.length === 0) {
|
|
224
|
-
strapi2.log.info(`[magic-sessionmanager] [BLOCKED]
|
|
229
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] User ${userId} has no active sessions`);
|
|
225
230
|
return ctx.unauthorized("All sessions have been terminated. Please login again.");
|
|
226
231
|
}
|
|
232
|
+
let matchingSession = null;
|
|
233
|
+
for (const session2 of activeSessions) {
|
|
234
|
+
if (!session2.token) continue;
|
|
235
|
+
try {
|
|
236
|
+
const decrypted = decryptToken$3(session2.token);
|
|
237
|
+
if (decrypted === currentToken) {
|
|
238
|
+
matchingSession = session2;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!matchingSession) {
|
|
245
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] Session for user ${userId} has been terminated`);
|
|
246
|
+
return ctx.unauthorized("This session has been terminated. Please login again.");
|
|
247
|
+
}
|
|
248
|
+
ctx.state.sessionId = matchingSession.documentId;
|
|
249
|
+
ctx.state.currentSession = matchingSession;
|
|
227
250
|
} catch (err) {
|
|
228
|
-
strapi2.log.debug("[magic-sessionmanager] Error checking
|
|
251
|
+
strapi2.log.debug("[magic-sessionmanager] Error checking session:", err.message);
|
|
229
252
|
}
|
|
230
253
|
}
|
|
231
254
|
await next();
|
|
232
|
-
if (ctx.state.user && ctx.state.user.documentId) {
|
|
255
|
+
if (ctx.state.user && ctx.state.user.documentId && ctx.state.sessionId) {
|
|
233
256
|
try {
|
|
234
|
-
const userId = ctx.state.user.documentId;
|
|
235
|
-
const sessionId = ctx.state.sessionId;
|
|
236
257
|
await sessionService.touch({
|
|
237
|
-
userId,
|
|
238
|
-
sessionId
|
|
258
|
+
userId: ctx.state.user.documentId,
|
|
259
|
+
sessionId: ctx.state.sessionId
|
|
239
260
|
});
|
|
240
261
|
} catch (err) {
|
|
241
262
|
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
@@ -558,9 +579,13 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
558
579
|
};
|
|
559
580
|
async function ensureContentApiPermissions(strapi2, log) {
|
|
560
581
|
try {
|
|
561
|
-
const
|
|
562
|
-
|
|
582
|
+
const ROLE_UID = "plugin::users-permissions.role";
|
|
583
|
+
const PERMISSION_UID = "plugin::users-permissions.permission";
|
|
584
|
+
const roles = await strapi2.entityService.findMany(ROLE_UID, {
|
|
585
|
+
filters: { type: "authenticated" },
|
|
586
|
+
limit: 1
|
|
563
587
|
});
|
|
588
|
+
const authenticatedRole = roles?.[0];
|
|
564
589
|
if (!authenticatedRole) {
|
|
565
590
|
log.warn("Authenticated role not found - skipping permission setup");
|
|
566
591
|
return;
|
|
@@ -569,10 +594,12 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
569
594
|
"plugin::magic-sessionmanager.session.logout",
|
|
570
595
|
"plugin::magic-sessionmanager.session.logoutAll",
|
|
571
596
|
"plugin::magic-sessionmanager.session.getOwnSessions",
|
|
572
|
-
"plugin::magic-sessionmanager.session.getUserSessions"
|
|
597
|
+
"plugin::magic-sessionmanager.session.getUserSessions",
|
|
598
|
+
"plugin::magic-sessionmanager.session.getCurrentSession",
|
|
599
|
+
"plugin::magic-sessionmanager.session.terminateOwnSession"
|
|
573
600
|
];
|
|
574
|
-
const existingPermissions = await strapi2.
|
|
575
|
-
|
|
601
|
+
const existingPermissions = await strapi2.entityService.findMany(PERMISSION_UID, {
|
|
602
|
+
filters: {
|
|
576
603
|
role: authenticatedRole.id,
|
|
577
604
|
action: { $in: requiredActions }
|
|
578
605
|
}
|
|
@@ -584,7 +611,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
584
611
|
return;
|
|
585
612
|
}
|
|
586
613
|
for (const action of missingActions) {
|
|
587
|
-
await strapi2.
|
|
614
|
+
await strapi2.entityService.create(PERMISSION_UID, {
|
|
588
615
|
data: {
|
|
589
616
|
action,
|
|
590
617
|
role: authenticatedRole.id
|
|
@@ -763,6 +790,15 @@ var contentApi$1 = {
|
|
|
763
790
|
description: "Get own sessions (automatically uses authenticated user)"
|
|
764
791
|
}
|
|
765
792
|
},
|
|
793
|
+
{
|
|
794
|
+
method: "GET",
|
|
795
|
+
path: "/current-session",
|
|
796
|
+
handler: "session.getCurrentSession",
|
|
797
|
+
config: {
|
|
798
|
+
auth: { strategies: ["users-permissions"] },
|
|
799
|
+
description: "Get current session info based on JWT token"
|
|
800
|
+
}
|
|
801
|
+
},
|
|
766
802
|
{
|
|
767
803
|
method: "GET",
|
|
768
804
|
path: "/user/:userId/sessions",
|
|
@@ -771,6 +807,18 @@ var contentApi$1 = {
|
|
|
771
807
|
auth: { strategies: ["users-permissions"] },
|
|
772
808
|
description: "Get sessions by userId (validates user can only see own sessions)"
|
|
773
809
|
}
|
|
810
|
+
},
|
|
811
|
+
// ============================================================
|
|
812
|
+
// SESSION MANAGEMENT (for own sessions only)
|
|
813
|
+
// ============================================================
|
|
814
|
+
{
|
|
815
|
+
method: "DELETE",
|
|
816
|
+
path: "/my-sessions/:sessionId",
|
|
817
|
+
handler: "session.terminateOwnSession",
|
|
818
|
+
config: {
|
|
819
|
+
auth: { strategies: ["users-permissions"] },
|
|
820
|
+
description: "Terminate a specific own session (not current)"
|
|
821
|
+
}
|
|
774
822
|
}
|
|
775
823
|
]
|
|
776
824
|
};
|
|
@@ -973,19 +1021,52 @@ var session$3 = {
|
|
|
973
1021
|
* Get own sessions (authenticated user)
|
|
974
1022
|
* GET /api/magic-sessionmanager/my-sessions
|
|
975
1023
|
* Automatically uses the authenticated user's documentId
|
|
1024
|
+
* Marks which session is the current one (based on JWT token)
|
|
976
1025
|
*/
|
|
977
1026
|
async getOwnSessions(ctx) {
|
|
978
1027
|
try {
|
|
979
1028
|
const userId = ctx.state.user?.documentId;
|
|
1029
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
980
1030
|
if (!userId) {
|
|
981
1031
|
return ctx.throw(401, "Unauthorized");
|
|
982
1032
|
}
|
|
983
|
-
const
|
|
984
|
-
|
|
1033
|
+
const allSessions = await strapi.documents(SESSION_UID$1).findMany({
|
|
1034
|
+
filters: { user: { documentId: userId } },
|
|
1035
|
+
sort: { loginTime: "desc" }
|
|
1036
|
+
});
|
|
1037
|
+
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1038
|
+
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
1039
|
+
const now = /* @__PURE__ */ new Date();
|
|
1040
|
+
const sessionsWithCurrent = allSessions.map((session2) => {
|
|
1041
|
+
const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
|
|
1042
|
+
const timeSinceActive = now - lastActiveTime;
|
|
1043
|
+
const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
|
|
1044
|
+
let isCurrentSession = false;
|
|
1045
|
+
if (session2.token && currentToken) {
|
|
1046
|
+
try {
|
|
1047
|
+
const decrypted = decryptToken$1(session2.token);
|
|
1048
|
+
isCurrentSession = decrypted === currentToken;
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const { token, refreshToken, ...sessionWithoutTokens } = session2;
|
|
1053
|
+
return {
|
|
1054
|
+
...sessionWithoutTokens,
|
|
1055
|
+
isCurrentSession,
|
|
1056
|
+
isTrulyActive,
|
|
1057
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
|
|
1058
|
+
};
|
|
1059
|
+
});
|
|
1060
|
+
sessionsWithCurrent.sort((a, b) => {
|
|
1061
|
+
if (a.isCurrentSession) return -1;
|
|
1062
|
+
if (b.isCurrentSession) return 1;
|
|
1063
|
+
return new Date(b.loginTime) - new Date(a.loginTime);
|
|
1064
|
+
});
|
|
985
1065
|
ctx.body = {
|
|
986
|
-
data:
|
|
1066
|
+
data: sessionsWithCurrent,
|
|
987
1067
|
meta: {
|
|
988
|
-
count:
|
|
1068
|
+
count: sessionsWithCurrent.length,
|
|
1069
|
+
active: sessionsWithCurrent.filter((s) => s.isTrulyActive).length
|
|
989
1070
|
}
|
|
990
1071
|
};
|
|
991
1072
|
} catch (err) {
|
|
@@ -1080,6 +1161,104 @@ var session$3 = {
|
|
|
1080
1161
|
ctx.throw(500, "Error during logout");
|
|
1081
1162
|
}
|
|
1082
1163
|
},
|
|
1164
|
+
/**
|
|
1165
|
+
* Get current session info based on JWT token
|
|
1166
|
+
* GET /api/magic-sessionmanager/current-session
|
|
1167
|
+
* Returns the session associated with the current JWT token
|
|
1168
|
+
*/
|
|
1169
|
+
async getCurrentSession(ctx) {
|
|
1170
|
+
try {
|
|
1171
|
+
const userId = ctx.state.user?.documentId;
|
|
1172
|
+
const token = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
1173
|
+
if (!userId || !token) {
|
|
1174
|
+
return ctx.throw(401, "Unauthorized");
|
|
1175
|
+
}
|
|
1176
|
+
const sessions = await strapi.documents(SESSION_UID$1).findMany({
|
|
1177
|
+
filters: {
|
|
1178
|
+
user: { documentId: userId },
|
|
1179
|
+
isActive: true
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
const currentSession = sessions.find((session2) => {
|
|
1183
|
+
if (!session2.token) return false;
|
|
1184
|
+
try {
|
|
1185
|
+
const decrypted = decryptToken$1(session2.token);
|
|
1186
|
+
return decrypted === token;
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
if (!currentSession) {
|
|
1192
|
+
return ctx.notFound("Current session not found");
|
|
1193
|
+
}
|
|
1194
|
+
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1195
|
+
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
1196
|
+
const now = /* @__PURE__ */ new Date();
|
|
1197
|
+
const lastActiveTime = currentSession.lastActive ? new Date(currentSession.lastActive) : new Date(currentSession.loginTime);
|
|
1198
|
+
const timeSinceActive = now - lastActiveTime;
|
|
1199
|
+
const { token: _, refreshToken: __, ...sessionWithoutTokens } = currentSession;
|
|
1200
|
+
ctx.body = {
|
|
1201
|
+
data: {
|
|
1202
|
+
...sessionWithoutTokens,
|
|
1203
|
+
isCurrentSession: true,
|
|
1204
|
+
isTrulyActive: timeSinceActive < inactivityTimeout,
|
|
1205
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
strapi.log.error("[magic-sessionmanager] Error getting current session:", err);
|
|
1210
|
+
ctx.throw(500, "Error fetching current session");
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
/**
|
|
1214
|
+
* Terminate a specific own session (not the current one)
|
|
1215
|
+
* DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
1216
|
+
* SECURITY: User can only terminate their OWN sessions
|
|
1217
|
+
*/
|
|
1218
|
+
async terminateOwnSession(ctx) {
|
|
1219
|
+
try {
|
|
1220
|
+
const userId = ctx.state.user?.documentId;
|
|
1221
|
+
const { sessionId } = ctx.params;
|
|
1222
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
1223
|
+
if (!userId) {
|
|
1224
|
+
return ctx.throw(401, "Unauthorized");
|
|
1225
|
+
}
|
|
1226
|
+
if (!sessionId) {
|
|
1227
|
+
return ctx.badRequest("Session ID is required");
|
|
1228
|
+
}
|
|
1229
|
+
const sessionToTerminate = await strapi.documents(SESSION_UID$1).findOne({
|
|
1230
|
+
documentId: sessionId,
|
|
1231
|
+
populate: { user: { fields: ["documentId"] } }
|
|
1232
|
+
});
|
|
1233
|
+
if (!sessionToTerminate) {
|
|
1234
|
+
return ctx.notFound("Session not found");
|
|
1235
|
+
}
|
|
1236
|
+
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
1237
|
+
if (sessionUserId !== userId) {
|
|
1238
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${userId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
|
|
1239
|
+
return ctx.forbidden("You can only terminate your own sessions");
|
|
1240
|
+
}
|
|
1241
|
+
if (sessionToTerminate.token && currentToken) {
|
|
1242
|
+
try {
|
|
1243
|
+
const decrypted = decryptToken$1(sessionToTerminate.token);
|
|
1244
|
+
if (decrypted === currentToken) {
|
|
1245
|
+
return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
|
|
1246
|
+
}
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
1251
|
+
await sessionService.terminateSession({ sessionId });
|
|
1252
|
+
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
1253
|
+
ctx.body = {
|
|
1254
|
+
message: `Session ${sessionId} terminated successfully`,
|
|
1255
|
+
success: true
|
|
1256
|
+
};
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
1259
|
+
ctx.throw(500, "Error terminating session");
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1083
1262
|
/**
|
|
1084
1263
|
* Terminate specific session
|
|
1085
1264
|
* DELETE /magic-sessionmanager/sessions/:sessionId
|
|
@@ -1858,7 +2037,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
1858
2037
|
}
|
|
1859
2038
|
};
|
|
1860
2039
|
};
|
|
1861
|
-
const version = "4.2.
|
|
2040
|
+
const version = "4.2.7";
|
|
1862
2041
|
const require$$2 = {
|
|
1863
2042
|
version
|
|
1864
2043
|
};
|
package/package.json
CHANGED