@strapi-community/plugin-io 5.1.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/dist/_chunks/{LivePresencePanel-CNaEK-Gk.js → LivePresencePanel-BkeWL4kq.js} +35 -5
- package/dist/_chunks/{LivePresencePanel-BeNq_EnQ.mjs → LivePresencePanel-D_vzQr4B.mjs} +35 -5
- package/dist/_chunks/{MonitoringPage-K5Y3hhKF.js → MonitoringPage-CYGqkzva.js} +1 -1
- package/dist/_chunks/{MonitoringPage-Bn9XJSlg.mjs → MonitoringPage-DKfhYUgU.mjs} +1 -1
- package/dist/_chunks/OnlineEditorsWidget-Bf8hfVha.js +341 -0
- package/dist/_chunks/OnlineEditorsWidget-RcYLxQke.mjs +339 -0
- package/dist/_chunks/{SettingsPage-4OkXJAjU.js → SettingsPage-0k9qPAJZ.js} +1 -1
- package/dist/_chunks/{SettingsPage-DMbMGU6J.mjs → SettingsPage-Qi0iMaWc.mjs} +1 -1
- package/dist/_chunks/{index-CzvX8YTe.mjs → index-Bw7WjN5H.mjs} +17 -4
- package/dist/_chunks/{index--2NeIKGR.js → index-DVNfszio.js} +17 -4
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +412 -49
- package/dist/server/index.mjs +412 -49
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -318,31 +318,56 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
318
318
|
return next(new Error("Max connections reached"));
|
|
319
319
|
}
|
|
320
320
|
const token = socket.handshake.auth?.token || socket.handshake.query?.token;
|
|
321
|
+
const strategy2 = socket.handshake.auth?.strategy;
|
|
322
|
+
const isAdmin = socket.handshake.auth?.isAdmin === true;
|
|
321
323
|
if (token) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
filters: { id: decoded.id },
|
|
328
|
-
populate: { role: true },
|
|
329
|
-
limit: 1
|
|
330
|
-
});
|
|
331
|
-
const user = users.length > 0 ? users[0] : null;
|
|
332
|
-
if (user) {
|
|
324
|
+
if (isAdmin || strategy2 === "admin-jwt") {
|
|
325
|
+
try {
|
|
326
|
+
const presenceController = strapi2.plugin(pluginId$6).controller("presence");
|
|
327
|
+
const session = presenceController.consumeSessionToken(token);
|
|
328
|
+
if (session) {
|
|
333
329
|
socket.user = {
|
|
334
|
-
id:
|
|
335
|
-
username: user.
|
|
336
|
-
email: user.email
|
|
337
|
-
role:
|
|
330
|
+
id: session.userId,
|
|
331
|
+
username: `${session.user.firstname || ""} ${session.user.lastname || ""}`.trim() || `Admin ${session.userId}`,
|
|
332
|
+
email: session.user.email || `admin-${session.userId}`,
|
|
333
|
+
role: "strapi-super-admin",
|
|
334
|
+
isAdmin: true
|
|
338
335
|
};
|
|
339
|
-
|
|
336
|
+
socket.adminUser = session.user;
|
|
337
|
+
presenceController.registerSocket(socket.id, token);
|
|
338
|
+
strapi2.log.info(`socket.io: Admin authenticated - ${socket.user.username} (ID: ${session.userId})`);
|
|
340
339
|
} else {
|
|
341
|
-
strapi2.log.warn(`socket.io:
|
|
340
|
+
strapi2.log.warn(`socket.io: Admin session token invalid or expired`);
|
|
342
341
|
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
strapi2.log.warn(`socket.io: Admin session verification failed: ${err.message}`);
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
try {
|
|
347
|
+
const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
|
|
348
|
+
strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
|
|
349
|
+
if (decoded.id) {
|
|
350
|
+
const users = await strapi2.documents("plugin::users-permissions.user").findMany({
|
|
351
|
+
filters: { id: decoded.id },
|
|
352
|
+
populate: { role: true },
|
|
353
|
+
limit: 1
|
|
354
|
+
});
|
|
355
|
+
const user = users.length > 0 ? users[0] : null;
|
|
356
|
+
if (user) {
|
|
357
|
+
socket.user = {
|
|
358
|
+
id: user.id,
|
|
359
|
+
username: user.username,
|
|
360
|
+
email: user.email,
|
|
361
|
+
role: user.role?.name || "authenticated"
|
|
362
|
+
};
|
|
363
|
+
strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
|
|
364
|
+
} else {
|
|
365
|
+
strapi2.log.warn(`socket.io: User not found for id: ${decoded.id}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
|
|
343
370
|
}
|
|
344
|
-
} catch (err) {
|
|
345
|
-
strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
|
|
346
371
|
}
|
|
347
372
|
} else {
|
|
348
373
|
strapi2.log.debug(`socket.io: No token provided, connecting as public`);
|
|
@@ -609,7 +634,9 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
609
634
|
});
|
|
610
635
|
socket.on("get-entity-subscriptions", (callback) => {
|
|
611
636
|
const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id && r.includes(":")).map((room) => {
|
|
612
|
-
const
|
|
637
|
+
const lastColonIndex = room.lastIndexOf(":");
|
|
638
|
+
const uid = room.substring(0, lastColonIndex);
|
|
639
|
+
const id = room.substring(lastColonIndex + 1);
|
|
613
640
|
return { uid, id, room };
|
|
614
641
|
});
|
|
615
642
|
if (callback) callback({ success: true, subscriptions: rooms });
|
|
@@ -658,6 +685,13 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
658
685
|
if (settings2.livePreview?.enabled !== false) {
|
|
659
686
|
previewService.cleanupSocket(socket.id);
|
|
660
687
|
}
|
|
688
|
+
try {
|
|
689
|
+
const presenceController = strapi2.plugin(pluginId$6).controller("presence");
|
|
690
|
+
if (presenceController?.unregisterSocket) {
|
|
691
|
+
presenceController.unregisterSocket(socket.id);
|
|
692
|
+
}
|
|
693
|
+
} catch (e) {
|
|
694
|
+
}
|
|
661
695
|
});
|
|
662
696
|
socket.on("error", (error2) => {
|
|
663
697
|
strapi2.log.error(`socket.io: Socket error (id: ${socket.id}): ${error2.message}`);
|
|
@@ -1279,19 +1313,38 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1279
1313
|
};
|
|
1280
1314
|
}
|
|
1281
1315
|
});
|
|
1282
|
-
const { randomUUID } = require$$1;
|
|
1316
|
+
const { randomUUID, createHash } = require$$1;
|
|
1283
1317
|
const sessionTokens = /* @__PURE__ */ new Map();
|
|
1318
|
+
const activeSockets = /* @__PURE__ */ new Map();
|
|
1319
|
+
const refreshThrottle = /* @__PURE__ */ new Map();
|
|
1320
|
+
const SESSION_TTL = 10 * 60 * 1e3;
|
|
1321
|
+
const REFRESH_COOLDOWN = 3 * 1e3;
|
|
1322
|
+
const CLEANUP_INTERVAL = 2 * 60 * 1e3;
|
|
1323
|
+
const hashToken = (token) => {
|
|
1324
|
+
return createHash("sha256").update(token).digest("hex");
|
|
1325
|
+
};
|
|
1284
1326
|
setInterval(() => {
|
|
1285
1327
|
const now = Date.now();
|
|
1286
|
-
|
|
1328
|
+
let cleaned = 0;
|
|
1329
|
+
for (const [tokenHash, session] of sessionTokens.entries()) {
|
|
1287
1330
|
if (session.expiresAt < now) {
|
|
1288
|
-
sessionTokens.delete(
|
|
1331
|
+
sessionTokens.delete(tokenHash);
|
|
1332
|
+
cleaned++;
|
|
1289
1333
|
}
|
|
1290
1334
|
}
|
|
1291
|
-
|
|
1335
|
+
for (const [userId, lastRefresh] of refreshThrottle.entries()) {
|
|
1336
|
+
if (now - lastRefresh > 60 * 60 * 1e3) {
|
|
1337
|
+
refreshThrottle.delete(userId);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (cleaned > 0) {
|
|
1341
|
+
console.log(`[plugin-io] [CLEANUP] Removed ${cleaned} expired session tokens`);
|
|
1342
|
+
}
|
|
1343
|
+
}, CLEANUP_INTERVAL);
|
|
1292
1344
|
var presence$3 = ({ strapi: strapi2 }) => ({
|
|
1293
1345
|
/**
|
|
1294
1346
|
* Creates a session token for admin users to connect to Socket.IO
|
|
1347
|
+
* Implements rate limiting and secure token storage
|
|
1295
1348
|
* @param {object} ctx - Koa context
|
|
1296
1349
|
*/
|
|
1297
1350
|
async createSession(ctx) {
|
|
@@ -1300,28 +1353,40 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1300
1353
|
strapi2.log.warn("[plugin-io] Presence session requested without admin user");
|
|
1301
1354
|
return ctx.unauthorized("Admin authentication required");
|
|
1302
1355
|
}
|
|
1356
|
+
const lastRefresh = refreshThrottle.get(adminUser.id);
|
|
1357
|
+
const now = Date.now();
|
|
1358
|
+
if (lastRefresh && now - lastRefresh < REFRESH_COOLDOWN) {
|
|
1359
|
+
const waitTime = Math.ceil((REFRESH_COOLDOWN - (now - lastRefresh)) / 1e3);
|
|
1360
|
+
strapi2.log.warn(`[plugin-io] Rate limit: User ${adminUser.id} must wait ${waitTime}s`);
|
|
1361
|
+
return ctx.tooManyRequests(`Please wait ${waitTime} seconds before requesting a new session`);
|
|
1362
|
+
}
|
|
1303
1363
|
try {
|
|
1304
1364
|
const token = randomUUID();
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1365
|
+
const tokenHash = hashToken(token);
|
|
1366
|
+
const expiresAt = now + SESSION_TTL;
|
|
1367
|
+
sessionTokens.set(tokenHash, {
|
|
1368
|
+
tokenHash,
|
|
1369
|
+
userId: adminUser.id,
|
|
1308
1370
|
user: {
|
|
1309
1371
|
id: adminUser.id,
|
|
1310
1372
|
email: adminUser.email,
|
|
1311
1373
|
firstname: adminUser.firstname,
|
|
1312
1374
|
lastname: adminUser.lastname
|
|
1313
1375
|
},
|
|
1314
|
-
|
|
1376
|
+
createdAt: now,
|
|
1377
|
+
expiresAt,
|
|
1378
|
+
usageCount: 0,
|
|
1379
|
+
maxUsage: 10
|
|
1380
|
+
// Max reconnects with same token
|
|
1315
1381
|
});
|
|
1316
|
-
|
|
1382
|
+
refreshThrottle.set(adminUser.id, now);
|
|
1383
|
+
strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.id}`);
|
|
1317
1384
|
ctx.body = {
|
|
1318
1385
|
token,
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
lastname: adminUser.lastname
|
|
1324
|
-
},
|
|
1386
|
+
// Send plaintext token to client (only time it's exposed)
|
|
1387
|
+
expiresAt,
|
|
1388
|
+
refreshAfter: now + SESSION_TTL * 0.7,
|
|
1389
|
+
// Suggest refresh at 70% of TTL
|
|
1325
1390
|
wsPath: "/socket.io",
|
|
1326
1391
|
wsUrl: `${ctx.protocol}://${ctx.host}`
|
|
1327
1392
|
};
|
|
@@ -1331,23 +1396,168 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1331
1396
|
}
|
|
1332
1397
|
},
|
|
1333
1398
|
/**
|
|
1334
|
-
* Validates
|
|
1399
|
+
* Validates a session token and tracks usage
|
|
1400
|
+
* Implements usage limits to prevent token abuse
|
|
1335
1401
|
* @param {string} token - Session token to validate
|
|
1336
1402
|
* @returns {object|null} Session data or null if invalid/expired
|
|
1337
1403
|
*/
|
|
1338
1404
|
consumeSessionToken(token) {
|
|
1339
|
-
if (!token) {
|
|
1405
|
+
if (!token || typeof token !== "string") {
|
|
1340
1406
|
return null;
|
|
1341
1407
|
}
|
|
1342
|
-
const
|
|
1408
|
+
const tokenHash = hashToken(token);
|
|
1409
|
+
const session = sessionTokens.get(tokenHash);
|
|
1343
1410
|
if (!session) {
|
|
1411
|
+
strapi2.log.debug("[plugin-io] Token not found in session store");
|
|
1344
1412
|
return null;
|
|
1345
1413
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1414
|
+
const now = Date.now();
|
|
1415
|
+
if (session.expiresAt < now) {
|
|
1416
|
+
sessionTokens.delete(tokenHash);
|
|
1417
|
+
strapi2.log.debug("[plugin-io] Token expired, removed from store");
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
if (session.usageCount >= session.maxUsage) {
|
|
1421
|
+
strapi2.log.warn(`[plugin-io] Token usage limit exceeded for user ${session.userId}`);
|
|
1422
|
+
sessionTokens.delete(tokenHash);
|
|
1348
1423
|
return null;
|
|
1349
1424
|
}
|
|
1425
|
+
session.usageCount++;
|
|
1426
|
+
session.lastUsed = now;
|
|
1350
1427
|
return session;
|
|
1428
|
+
},
|
|
1429
|
+
/**
|
|
1430
|
+
* Registers a socket as using a specific token
|
|
1431
|
+
* @param {string} socketId - Socket ID
|
|
1432
|
+
* @param {string} token - The token being used
|
|
1433
|
+
*/
|
|
1434
|
+
registerSocket(socketId, token) {
|
|
1435
|
+
if (!socketId || !token) return;
|
|
1436
|
+
const tokenHash = hashToken(token);
|
|
1437
|
+
activeSockets.set(socketId, tokenHash);
|
|
1438
|
+
},
|
|
1439
|
+
/**
|
|
1440
|
+
* Unregisters a socket when it disconnects
|
|
1441
|
+
* @param {string} socketId - Socket ID
|
|
1442
|
+
*/
|
|
1443
|
+
unregisterSocket(socketId) {
|
|
1444
|
+
activeSockets.delete(socketId);
|
|
1445
|
+
},
|
|
1446
|
+
/**
|
|
1447
|
+
* Invalidates all sessions for a specific user (e.g., on logout)
|
|
1448
|
+
* @param {number} userId - User ID to invalidate
|
|
1449
|
+
* @returns {number} Number of sessions invalidated
|
|
1450
|
+
*/
|
|
1451
|
+
invalidateUserSessions(userId) {
|
|
1452
|
+
let invalidated = 0;
|
|
1453
|
+
for (const [tokenHash, session] of sessionTokens.entries()) {
|
|
1454
|
+
if (session.userId === userId) {
|
|
1455
|
+
sessionTokens.delete(tokenHash);
|
|
1456
|
+
invalidated++;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
refreshThrottle.delete(userId);
|
|
1460
|
+
strapi2.log.info(`[plugin-io] Invalidated ${invalidated} sessions for user ${userId}`);
|
|
1461
|
+
return invalidated;
|
|
1462
|
+
},
|
|
1463
|
+
/**
|
|
1464
|
+
* Gets session statistics (for monitoring) - internal method
|
|
1465
|
+
* @returns {object} Session statistics
|
|
1466
|
+
*/
|
|
1467
|
+
getSessionStatsInternal() {
|
|
1468
|
+
const now = Date.now();
|
|
1469
|
+
let active = 0;
|
|
1470
|
+
let expiringSoon = 0;
|
|
1471
|
+
for (const session of sessionTokens.values()) {
|
|
1472
|
+
if (session.expiresAt > now) {
|
|
1473
|
+
active++;
|
|
1474
|
+
if (session.expiresAt - now < 2 * 60 * 1e3) {
|
|
1475
|
+
expiringSoon++;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
activeSessions: active,
|
|
1481
|
+
expiringSoon,
|
|
1482
|
+
activeSocketConnections: activeSockets.size,
|
|
1483
|
+
sessionTTL: SESSION_TTL,
|
|
1484
|
+
refreshCooldown: REFRESH_COOLDOWN
|
|
1485
|
+
};
|
|
1486
|
+
},
|
|
1487
|
+
/**
|
|
1488
|
+
* HTTP Handler: Gets session statistics for admin monitoring
|
|
1489
|
+
* @param {object} ctx - Koa context
|
|
1490
|
+
*/
|
|
1491
|
+
async getSessionStats(ctx) {
|
|
1492
|
+
const adminUser = ctx.state.user;
|
|
1493
|
+
if (!adminUser) {
|
|
1494
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1495
|
+
}
|
|
1496
|
+
try {
|
|
1497
|
+
const stats = this.getSessionStatsInternal();
|
|
1498
|
+
ctx.body = { data: stats };
|
|
1499
|
+
} catch (error2) {
|
|
1500
|
+
strapi2.log.error("[plugin-io] Failed to get session stats:", error2);
|
|
1501
|
+
return ctx.internalServerError("Failed to get session statistics");
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1504
|
+
/**
|
|
1505
|
+
* HTTP Handler: Invalidates all sessions for a specific user
|
|
1506
|
+
* @param {object} ctx - Koa context
|
|
1507
|
+
*/
|
|
1508
|
+
async invalidateUserSessionsHandler(ctx) {
|
|
1509
|
+
const adminUser = ctx.state.user;
|
|
1510
|
+
if (!adminUser) {
|
|
1511
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1512
|
+
}
|
|
1513
|
+
const { userId } = ctx.params;
|
|
1514
|
+
if (!userId) {
|
|
1515
|
+
return ctx.badRequest("User ID is required");
|
|
1516
|
+
}
|
|
1517
|
+
try {
|
|
1518
|
+
const userIdNum = parseInt(userId, 10);
|
|
1519
|
+
if (isNaN(userIdNum)) {
|
|
1520
|
+
return ctx.badRequest("Invalid user ID");
|
|
1521
|
+
}
|
|
1522
|
+
const invalidated = this.invalidateUserSessions(userIdNum);
|
|
1523
|
+
strapi2.log.info(`[plugin-io] Admin ${adminUser.id} invalidated ${invalidated} sessions for user ${userIdNum}`);
|
|
1524
|
+
ctx.body = {
|
|
1525
|
+
data: {
|
|
1526
|
+
userId: userIdNum,
|
|
1527
|
+
invalidatedSessions: invalidated,
|
|
1528
|
+
message: `Successfully invalidated ${invalidated} session(s)`
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
} catch (error2) {
|
|
1532
|
+
strapi2.log.error("[plugin-io] Failed to invalidate user sessions:", error2);
|
|
1533
|
+
return ctx.internalServerError("Failed to invalidate sessions");
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
/**
|
|
1537
|
+
* HTTP Handler: Gets all online users with their editing info
|
|
1538
|
+
* Used for the "Who's Online" dashboard widget
|
|
1539
|
+
* @param {object} ctx - Koa context
|
|
1540
|
+
*/
|
|
1541
|
+
async getOnlineUsers(ctx) {
|
|
1542
|
+
const adminUser = ctx.state.user;
|
|
1543
|
+
if (!adminUser) {
|
|
1544
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1545
|
+
}
|
|
1546
|
+
try {
|
|
1547
|
+
const presenceService = strapi2.plugin("io").service("presence");
|
|
1548
|
+
const onlineUsers = presenceService.getOnlineUsers();
|
|
1549
|
+
const counts = presenceService.getOnlineCounts();
|
|
1550
|
+
ctx.body = {
|
|
1551
|
+
data: {
|
|
1552
|
+
users: onlineUsers,
|
|
1553
|
+
counts,
|
|
1554
|
+
timestamp: Date.now()
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
} catch (error2) {
|
|
1558
|
+
strapi2.log.error("[plugin-io] Failed to get online users:", error2);
|
|
1559
|
+
return ctx.internalServerError("Failed to get online users");
|
|
1560
|
+
}
|
|
1351
1561
|
}
|
|
1352
1562
|
});
|
|
1353
1563
|
const settings$2 = settings$3;
|
|
@@ -1439,6 +1649,33 @@ var admin$1 = {
|
|
|
1439
1649
|
config: {
|
|
1440
1650
|
policies: ["admin::isAuthenticatedAdmin"]
|
|
1441
1651
|
}
|
|
1652
|
+
},
|
|
1653
|
+
// Security: Session statistics
|
|
1654
|
+
{
|
|
1655
|
+
method: "GET",
|
|
1656
|
+
path: "/security/sessions",
|
|
1657
|
+
handler: "presence.getSessionStats",
|
|
1658
|
+
config: {
|
|
1659
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1660
|
+
}
|
|
1661
|
+
},
|
|
1662
|
+
// Security: Invalidate user sessions (force logout)
|
|
1663
|
+
{
|
|
1664
|
+
method: "POST",
|
|
1665
|
+
path: "/security/invalidate/:userId",
|
|
1666
|
+
handler: "presence.invalidateUserSessionsHandler",
|
|
1667
|
+
config: {
|
|
1668
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1669
|
+
}
|
|
1670
|
+
},
|
|
1671
|
+
// Who's Online: Get all online users with editing info
|
|
1672
|
+
{
|
|
1673
|
+
method: "GET",
|
|
1674
|
+
path: "/online-users",
|
|
1675
|
+
handler: "presence.getOnlineUsers",
|
|
1676
|
+
config: {
|
|
1677
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1678
|
+
}
|
|
1442
1679
|
}
|
|
1443
1680
|
]
|
|
1444
1681
|
};
|
|
@@ -29645,21 +29882,55 @@ var strategies = ({ strapi: strapi2 }) => {
|
|
|
29645
29882
|
credentials: function(user) {
|
|
29646
29883
|
return `${this.name}-${user.id}`;
|
|
29647
29884
|
},
|
|
29648
|
-
|
|
29885
|
+
/**
|
|
29886
|
+
* Authenticates admin user via session token
|
|
29887
|
+
* @param {object} auth - Auth object containing token
|
|
29888
|
+
* @param {object} socket - Socket instance for registration
|
|
29889
|
+
* @returns {object} User data if authenticated
|
|
29890
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
29891
|
+
*/
|
|
29892
|
+
authenticate: async function(auth, socket) {
|
|
29649
29893
|
const token2 = auth.token;
|
|
29650
|
-
if (!token2) {
|
|
29894
|
+
if (!token2 || typeof token2 !== "string") {
|
|
29895
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: No token provided");
|
|
29651
29896
|
throw new UnauthorizedError2("Invalid admin credentials");
|
|
29652
29897
|
}
|
|
29898
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29899
|
+
if (!uuidRegex.test(token2)) {
|
|
29900
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: Invalid token format");
|
|
29901
|
+
throw new UnauthorizedError2("Invalid token format");
|
|
29902
|
+
}
|
|
29653
29903
|
try {
|
|
29654
29904
|
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29655
29905
|
const session = presenceController.consumeSessionToken(token2);
|
|
29656
29906
|
if (!session) {
|
|
29907
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: Token not valid or expired");
|
|
29657
29908
|
throw new UnauthorizedError2("Invalid or expired session token");
|
|
29658
29909
|
}
|
|
29659
|
-
|
|
29910
|
+
if (socket?.id) {
|
|
29911
|
+
presenceController.registerSocket(socket.id, token2);
|
|
29912
|
+
}
|
|
29913
|
+
strapi2.log.info(`[plugin-io] Admin authenticated: User ID ${session.userId}`);
|
|
29914
|
+
return {
|
|
29915
|
+
id: session.userId,
|
|
29916
|
+
...session.user
|
|
29917
|
+
};
|
|
29660
29918
|
} catch (error2) {
|
|
29661
|
-
|
|
29662
|
-
|
|
29919
|
+
if (error2 instanceof UnauthorizedError2) {
|
|
29920
|
+
throw error2;
|
|
29921
|
+
}
|
|
29922
|
+
strapi2.log.error("[plugin-io] Admin session verification error:", error2.message);
|
|
29923
|
+
throw new UnauthorizedError2("Authentication failed");
|
|
29924
|
+
}
|
|
29925
|
+
},
|
|
29926
|
+
/**
|
|
29927
|
+
* Cleanup when socket disconnects
|
|
29928
|
+
* @param {object} socket - Socket instance
|
|
29929
|
+
*/
|
|
29930
|
+
onDisconnect: function(socket) {
|
|
29931
|
+
if (socket?.id) {
|
|
29932
|
+
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29933
|
+
presenceController.unregisterSocket(socket.id);
|
|
29663
29934
|
}
|
|
29664
29935
|
},
|
|
29665
29936
|
getRoomName: function(user) {
|
|
@@ -30284,7 +30555,10 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30284
30555
|
*/
|
|
30285
30556
|
registerConnection(socketId, user = null) {
|
|
30286
30557
|
const settings2 = getPresenceSettings();
|
|
30287
|
-
if (!settings2.enabled)
|
|
30558
|
+
if (!settings2.enabled) {
|
|
30559
|
+
strapi2.log.warn(`socket.io: Presence disabled, skipping registration for ${socketId}`);
|
|
30560
|
+
return;
|
|
30561
|
+
}
|
|
30288
30562
|
activeConnections.set(socketId, {
|
|
30289
30563
|
user,
|
|
30290
30564
|
entities: /* @__PURE__ */ new Map(),
|
|
@@ -30292,7 +30566,8 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30292
30566
|
lastSeen: Date.now(),
|
|
30293
30567
|
connectedAt: Date.now()
|
|
30294
30568
|
});
|
|
30295
|
-
|
|
30569
|
+
const username = user?.username || user?.firstname || "anonymous";
|
|
30570
|
+
strapi2.log.info(`socket.io: Presence registered for ${username} (socket: ${socketId}, total: ${activeConnections.size})`);
|
|
30296
30571
|
},
|
|
30297
30572
|
/**
|
|
30298
30573
|
* Unregisters a socket connection and cleans up all entity presence
|
|
@@ -30303,7 +30578,9 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30303
30578
|
if (!connection) return;
|
|
30304
30579
|
if (connection.entities) {
|
|
30305
30580
|
for (const entityKey of connection.entities.keys()) {
|
|
30306
|
-
const
|
|
30581
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30582
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30583
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30307
30584
|
await this.leaveEntity(socketId, uid, documentId, false);
|
|
30308
30585
|
}
|
|
30309
30586
|
}
|
|
@@ -30538,6 +30815,90 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30538
30815
|
timestamp: Date.now()
|
|
30539
30816
|
});
|
|
30540
30817
|
}
|
|
30818
|
+
},
|
|
30819
|
+
/**
|
|
30820
|
+
* Gets all online users with their currently editing entities
|
|
30821
|
+
* Used for the "Who's Online" dashboard widget
|
|
30822
|
+
* @returns {Array} List of online users with their editing info
|
|
30823
|
+
*/
|
|
30824
|
+
getOnlineUsers() {
|
|
30825
|
+
const users = [];
|
|
30826
|
+
const now = Date.now();
|
|
30827
|
+
for (const [socketId, connection] of activeConnections) {
|
|
30828
|
+
if (!connection.user) continue;
|
|
30829
|
+
const editingEntities = [];
|
|
30830
|
+
if (connection.entities) {
|
|
30831
|
+
for (const [entityKey, joinedAt] of connection.entities) {
|
|
30832
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30833
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30834
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30835
|
+
let contentTypeName = uid;
|
|
30836
|
+
try {
|
|
30837
|
+
const contentType = strapi2.contentTypes[uid];
|
|
30838
|
+
if (contentType?.info?.displayName) {
|
|
30839
|
+
contentTypeName = contentType.info.displayName;
|
|
30840
|
+
} else if (contentType?.info?.singularName) {
|
|
30841
|
+
contentTypeName = contentType.info.singularName;
|
|
30842
|
+
}
|
|
30843
|
+
} catch (e) {
|
|
30844
|
+
}
|
|
30845
|
+
editingEntities.push({
|
|
30846
|
+
uid,
|
|
30847
|
+
documentId,
|
|
30848
|
+
contentTypeName,
|
|
30849
|
+
joinedAt,
|
|
30850
|
+
editingFor: Math.floor((now - joinedAt) / 1e3)
|
|
30851
|
+
// seconds
|
|
30852
|
+
});
|
|
30853
|
+
}
|
|
30854
|
+
}
|
|
30855
|
+
users.push({
|
|
30856
|
+
socketId,
|
|
30857
|
+
user: {
|
|
30858
|
+
id: connection.user.id,
|
|
30859
|
+
username: connection.user.username,
|
|
30860
|
+
email: connection.user.email,
|
|
30861
|
+
firstname: connection.user.firstname,
|
|
30862
|
+
lastname: connection.user.lastname,
|
|
30863
|
+
isAdmin: connection.user.isAdmin || false
|
|
30864
|
+
},
|
|
30865
|
+
connectedAt: connection.connectedAt,
|
|
30866
|
+
lastSeen: connection.lastSeen,
|
|
30867
|
+
onlineFor: Math.floor((now - connection.connectedAt) / 1e3),
|
|
30868
|
+
// seconds
|
|
30869
|
+
editingEntities,
|
|
30870
|
+
isEditing: editingEntities.length > 0
|
|
30871
|
+
});
|
|
30872
|
+
}
|
|
30873
|
+
users.sort((a, b) => {
|
|
30874
|
+
if (a.isEditing && !b.isEditing) return -1;
|
|
30875
|
+
if (!a.isEditing && b.isEditing) return 1;
|
|
30876
|
+
return b.connectedAt - a.connectedAt;
|
|
30877
|
+
});
|
|
30878
|
+
return users;
|
|
30879
|
+
},
|
|
30880
|
+
/**
|
|
30881
|
+
* Gets count of online users
|
|
30882
|
+
* @returns {object} Online user counts
|
|
30883
|
+
*/
|
|
30884
|
+
getOnlineCounts() {
|
|
30885
|
+
let total = 0;
|
|
30886
|
+
let admins = 0;
|
|
30887
|
+
let users = 0;
|
|
30888
|
+
let editing = 0;
|
|
30889
|
+
for (const connection of activeConnections.values()) {
|
|
30890
|
+
if (!connection.user) continue;
|
|
30891
|
+
total++;
|
|
30892
|
+
if (connection.user.isAdmin) {
|
|
30893
|
+
admins++;
|
|
30894
|
+
} else {
|
|
30895
|
+
users++;
|
|
30896
|
+
}
|
|
30897
|
+
if (connection.entities?.size > 0) {
|
|
30898
|
+
editing++;
|
|
30899
|
+
}
|
|
30900
|
+
}
|
|
30901
|
+
return { total, admins, users, editing };
|
|
30541
30902
|
}
|
|
30542
30903
|
};
|
|
30543
30904
|
};
|
|
@@ -30754,7 +31115,9 @@ var preview$1 = ({ strapi: strapi2 }) => {
|
|
|
30754
31115
|
getActivePreviewEntities() {
|
|
30755
31116
|
const entities = [];
|
|
30756
31117
|
for (const [entityKey, subscribers] of previewSubscribers) {
|
|
30757
|
-
const
|
|
31118
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
31119
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
31120
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30758
31121
|
entities.push({
|
|
30759
31122
|
uid,
|
|
30760
31123
|
documentId,
|
package/package.json
CHANGED