@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.js
CHANGED
|
@@ -350,31 +350,56 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
350
350
|
return next(new Error("Max connections reached"));
|
|
351
351
|
}
|
|
352
352
|
const token = socket.handshake.auth?.token || socket.handshake.query?.token;
|
|
353
|
+
const strategy2 = socket.handshake.auth?.strategy;
|
|
354
|
+
const isAdmin = socket.handshake.auth?.isAdmin === true;
|
|
353
355
|
if (token) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
filters: { id: decoded.id },
|
|
360
|
-
populate: { role: true },
|
|
361
|
-
limit: 1
|
|
362
|
-
});
|
|
363
|
-
const user = users.length > 0 ? users[0] : null;
|
|
364
|
-
if (user) {
|
|
356
|
+
if (isAdmin || strategy2 === "admin-jwt") {
|
|
357
|
+
try {
|
|
358
|
+
const presenceController = strapi2.plugin(pluginId$6).controller("presence");
|
|
359
|
+
const session = presenceController.consumeSessionToken(token);
|
|
360
|
+
if (session) {
|
|
365
361
|
socket.user = {
|
|
366
|
-
id:
|
|
367
|
-
username: user.
|
|
368
|
-
email: user.email
|
|
369
|
-
role:
|
|
362
|
+
id: session.userId,
|
|
363
|
+
username: `${session.user.firstname || ""} ${session.user.lastname || ""}`.trim() || `Admin ${session.userId}`,
|
|
364
|
+
email: session.user.email || `admin-${session.userId}`,
|
|
365
|
+
role: "strapi-super-admin",
|
|
366
|
+
isAdmin: true
|
|
370
367
|
};
|
|
371
|
-
|
|
368
|
+
socket.adminUser = session.user;
|
|
369
|
+
presenceController.registerSocket(socket.id, token);
|
|
370
|
+
strapi2.log.info(`socket.io: Admin authenticated - ${socket.user.username} (ID: ${session.userId})`);
|
|
372
371
|
} else {
|
|
373
|
-
strapi2.log.warn(`socket.io:
|
|
372
|
+
strapi2.log.warn(`socket.io: Admin session token invalid or expired`);
|
|
374
373
|
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
strapi2.log.warn(`socket.io: Admin session verification failed: ${err.message}`);
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
try {
|
|
379
|
+
const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
|
|
380
|
+
strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
|
|
381
|
+
if (decoded.id) {
|
|
382
|
+
const users = await strapi2.documents("plugin::users-permissions.user").findMany({
|
|
383
|
+
filters: { id: decoded.id },
|
|
384
|
+
populate: { role: true },
|
|
385
|
+
limit: 1
|
|
386
|
+
});
|
|
387
|
+
const user = users.length > 0 ? users[0] : null;
|
|
388
|
+
if (user) {
|
|
389
|
+
socket.user = {
|
|
390
|
+
id: user.id,
|
|
391
|
+
username: user.username,
|
|
392
|
+
email: user.email,
|
|
393
|
+
role: user.role?.name || "authenticated"
|
|
394
|
+
};
|
|
395
|
+
strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
|
|
396
|
+
} else {
|
|
397
|
+
strapi2.log.warn(`socket.io: User not found for id: ${decoded.id}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
|
|
375
402
|
}
|
|
376
|
-
} catch (err) {
|
|
377
|
-
strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
|
|
378
403
|
}
|
|
379
404
|
} else {
|
|
380
405
|
strapi2.log.debug(`socket.io: No token provided, connecting as public`);
|
|
@@ -641,7 +666,9 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
641
666
|
});
|
|
642
667
|
socket.on("get-entity-subscriptions", (callback) => {
|
|
643
668
|
const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id && r.includes(":")).map((room) => {
|
|
644
|
-
const
|
|
669
|
+
const lastColonIndex = room.lastIndexOf(":");
|
|
670
|
+
const uid = room.substring(0, lastColonIndex);
|
|
671
|
+
const id = room.substring(lastColonIndex + 1);
|
|
645
672
|
return { uid, id, room };
|
|
646
673
|
});
|
|
647
674
|
if (callback) callback({ success: true, subscriptions: rooms });
|
|
@@ -690,6 +717,13 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
690
717
|
if (settings2.livePreview?.enabled !== false) {
|
|
691
718
|
previewService.cleanupSocket(socket.id);
|
|
692
719
|
}
|
|
720
|
+
try {
|
|
721
|
+
const presenceController = strapi2.plugin(pluginId$6).controller("presence");
|
|
722
|
+
if (presenceController?.unregisterSocket) {
|
|
723
|
+
presenceController.unregisterSocket(socket.id);
|
|
724
|
+
}
|
|
725
|
+
} catch (e) {
|
|
726
|
+
}
|
|
693
727
|
});
|
|
694
728
|
socket.on("error", (error2) => {
|
|
695
729
|
strapi2.log.error(`socket.io: Socket error (id: ${socket.id}): ${error2.message}`);
|
|
@@ -1311,19 +1345,38 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1311
1345
|
};
|
|
1312
1346
|
}
|
|
1313
1347
|
});
|
|
1314
|
-
const { randomUUID } = require$$1__default.default;
|
|
1348
|
+
const { randomUUID, createHash } = require$$1__default.default;
|
|
1315
1349
|
const sessionTokens = /* @__PURE__ */ new Map();
|
|
1350
|
+
const activeSockets = /* @__PURE__ */ new Map();
|
|
1351
|
+
const refreshThrottle = /* @__PURE__ */ new Map();
|
|
1352
|
+
const SESSION_TTL = 10 * 60 * 1e3;
|
|
1353
|
+
const REFRESH_COOLDOWN = 3 * 1e3;
|
|
1354
|
+
const CLEANUP_INTERVAL = 2 * 60 * 1e3;
|
|
1355
|
+
const hashToken = (token) => {
|
|
1356
|
+
return createHash("sha256").update(token).digest("hex");
|
|
1357
|
+
};
|
|
1316
1358
|
setInterval(() => {
|
|
1317
1359
|
const now = Date.now();
|
|
1318
|
-
|
|
1360
|
+
let cleaned = 0;
|
|
1361
|
+
for (const [tokenHash, session] of sessionTokens.entries()) {
|
|
1319
1362
|
if (session.expiresAt < now) {
|
|
1320
|
-
sessionTokens.delete(
|
|
1363
|
+
sessionTokens.delete(tokenHash);
|
|
1364
|
+
cleaned++;
|
|
1321
1365
|
}
|
|
1322
1366
|
}
|
|
1323
|
-
|
|
1367
|
+
for (const [userId, lastRefresh] of refreshThrottle.entries()) {
|
|
1368
|
+
if (now - lastRefresh > 60 * 60 * 1e3) {
|
|
1369
|
+
refreshThrottle.delete(userId);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (cleaned > 0) {
|
|
1373
|
+
console.log(`[plugin-io] [CLEANUP] Removed ${cleaned} expired session tokens`);
|
|
1374
|
+
}
|
|
1375
|
+
}, CLEANUP_INTERVAL);
|
|
1324
1376
|
var presence$3 = ({ strapi: strapi2 }) => ({
|
|
1325
1377
|
/**
|
|
1326
1378
|
* Creates a session token for admin users to connect to Socket.IO
|
|
1379
|
+
* Implements rate limiting and secure token storage
|
|
1327
1380
|
* @param {object} ctx - Koa context
|
|
1328
1381
|
*/
|
|
1329
1382
|
async createSession(ctx) {
|
|
@@ -1332,28 +1385,40 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1332
1385
|
strapi2.log.warn("[plugin-io] Presence session requested without admin user");
|
|
1333
1386
|
return ctx.unauthorized("Admin authentication required");
|
|
1334
1387
|
}
|
|
1388
|
+
const lastRefresh = refreshThrottle.get(adminUser.id);
|
|
1389
|
+
const now = Date.now();
|
|
1390
|
+
if (lastRefresh && now - lastRefresh < REFRESH_COOLDOWN) {
|
|
1391
|
+
const waitTime = Math.ceil((REFRESH_COOLDOWN - (now - lastRefresh)) / 1e3);
|
|
1392
|
+
strapi2.log.warn(`[plugin-io] Rate limit: User ${adminUser.id} must wait ${waitTime}s`);
|
|
1393
|
+
return ctx.tooManyRequests(`Please wait ${waitTime} seconds before requesting a new session`);
|
|
1394
|
+
}
|
|
1335
1395
|
try {
|
|
1336
1396
|
const token = randomUUID();
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1339
|
-
|
|
1397
|
+
const tokenHash = hashToken(token);
|
|
1398
|
+
const expiresAt = now + SESSION_TTL;
|
|
1399
|
+
sessionTokens.set(tokenHash, {
|
|
1400
|
+
tokenHash,
|
|
1401
|
+
userId: adminUser.id,
|
|
1340
1402
|
user: {
|
|
1341
1403
|
id: adminUser.id,
|
|
1342
1404
|
email: adminUser.email,
|
|
1343
1405
|
firstname: adminUser.firstname,
|
|
1344
1406
|
lastname: adminUser.lastname
|
|
1345
1407
|
},
|
|
1346
|
-
|
|
1408
|
+
createdAt: now,
|
|
1409
|
+
expiresAt,
|
|
1410
|
+
usageCount: 0,
|
|
1411
|
+
maxUsage: 10
|
|
1412
|
+
// Max reconnects with same token
|
|
1347
1413
|
});
|
|
1348
|
-
|
|
1414
|
+
refreshThrottle.set(adminUser.id, now);
|
|
1415
|
+
strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.id}`);
|
|
1349
1416
|
ctx.body = {
|
|
1350
1417
|
token,
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
lastname: adminUser.lastname
|
|
1356
|
-
},
|
|
1418
|
+
// Send plaintext token to client (only time it's exposed)
|
|
1419
|
+
expiresAt,
|
|
1420
|
+
refreshAfter: now + SESSION_TTL * 0.7,
|
|
1421
|
+
// Suggest refresh at 70% of TTL
|
|
1357
1422
|
wsPath: "/socket.io",
|
|
1358
1423
|
wsUrl: `${ctx.protocol}://${ctx.host}`
|
|
1359
1424
|
};
|
|
@@ -1363,23 +1428,168 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1363
1428
|
}
|
|
1364
1429
|
},
|
|
1365
1430
|
/**
|
|
1366
|
-
* Validates
|
|
1431
|
+
* Validates a session token and tracks usage
|
|
1432
|
+
* Implements usage limits to prevent token abuse
|
|
1367
1433
|
* @param {string} token - Session token to validate
|
|
1368
1434
|
* @returns {object|null} Session data or null if invalid/expired
|
|
1369
1435
|
*/
|
|
1370
1436
|
consumeSessionToken(token) {
|
|
1371
|
-
if (!token) {
|
|
1437
|
+
if (!token || typeof token !== "string") {
|
|
1372
1438
|
return null;
|
|
1373
1439
|
}
|
|
1374
|
-
const
|
|
1440
|
+
const tokenHash = hashToken(token);
|
|
1441
|
+
const session = sessionTokens.get(tokenHash);
|
|
1375
1442
|
if (!session) {
|
|
1443
|
+
strapi2.log.debug("[plugin-io] Token not found in session store");
|
|
1376
1444
|
return null;
|
|
1377
1445
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1446
|
+
const now = Date.now();
|
|
1447
|
+
if (session.expiresAt < now) {
|
|
1448
|
+
sessionTokens.delete(tokenHash);
|
|
1449
|
+
strapi2.log.debug("[plugin-io] Token expired, removed from store");
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
if (session.usageCount >= session.maxUsage) {
|
|
1453
|
+
strapi2.log.warn(`[plugin-io] Token usage limit exceeded for user ${session.userId}`);
|
|
1454
|
+
sessionTokens.delete(tokenHash);
|
|
1380
1455
|
return null;
|
|
1381
1456
|
}
|
|
1457
|
+
session.usageCount++;
|
|
1458
|
+
session.lastUsed = now;
|
|
1382
1459
|
return session;
|
|
1460
|
+
},
|
|
1461
|
+
/**
|
|
1462
|
+
* Registers a socket as using a specific token
|
|
1463
|
+
* @param {string} socketId - Socket ID
|
|
1464
|
+
* @param {string} token - The token being used
|
|
1465
|
+
*/
|
|
1466
|
+
registerSocket(socketId, token) {
|
|
1467
|
+
if (!socketId || !token) return;
|
|
1468
|
+
const tokenHash = hashToken(token);
|
|
1469
|
+
activeSockets.set(socketId, tokenHash);
|
|
1470
|
+
},
|
|
1471
|
+
/**
|
|
1472
|
+
* Unregisters a socket when it disconnects
|
|
1473
|
+
* @param {string} socketId - Socket ID
|
|
1474
|
+
*/
|
|
1475
|
+
unregisterSocket(socketId) {
|
|
1476
|
+
activeSockets.delete(socketId);
|
|
1477
|
+
},
|
|
1478
|
+
/**
|
|
1479
|
+
* Invalidates all sessions for a specific user (e.g., on logout)
|
|
1480
|
+
* @param {number} userId - User ID to invalidate
|
|
1481
|
+
* @returns {number} Number of sessions invalidated
|
|
1482
|
+
*/
|
|
1483
|
+
invalidateUserSessions(userId) {
|
|
1484
|
+
let invalidated = 0;
|
|
1485
|
+
for (const [tokenHash, session] of sessionTokens.entries()) {
|
|
1486
|
+
if (session.userId === userId) {
|
|
1487
|
+
sessionTokens.delete(tokenHash);
|
|
1488
|
+
invalidated++;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
refreshThrottle.delete(userId);
|
|
1492
|
+
strapi2.log.info(`[plugin-io] Invalidated ${invalidated} sessions for user ${userId}`);
|
|
1493
|
+
return invalidated;
|
|
1494
|
+
},
|
|
1495
|
+
/**
|
|
1496
|
+
* Gets session statistics (for monitoring) - internal method
|
|
1497
|
+
* @returns {object} Session statistics
|
|
1498
|
+
*/
|
|
1499
|
+
getSessionStatsInternal() {
|
|
1500
|
+
const now = Date.now();
|
|
1501
|
+
let active = 0;
|
|
1502
|
+
let expiringSoon = 0;
|
|
1503
|
+
for (const session of sessionTokens.values()) {
|
|
1504
|
+
if (session.expiresAt > now) {
|
|
1505
|
+
active++;
|
|
1506
|
+
if (session.expiresAt - now < 2 * 60 * 1e3) {
|
|
1507
|
+
expiringSoon++;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
activeSessions: active,
|
|
1513
|
+
expiringSoon,
|
|
1514
|
+
activeSocketConnections: activeSockets.size,
|
|
1515
|
+
sessionTTL: SESSION_TTL,
|
|
1516
|
+
refreshCooldown: REFRESH_COOLDOWN
|
|
1517
|
+
};
|
|
1518
|
+
},
|
|
1519
|
+
/**
|
|
1520
|
+
* HTTP Handler: Gets session statistics for admin monitoring
|
|
1521
|
+
* @param {object} ctx - Koa context
|
|
1522
|
+
*/
|
|
1523
|
+
async getSessionStats(ctx) {
|
|
1524
|
+
const adminUser = ctx.state.user;
|
|
1525
|
+
if (!adminUser) {
|
|
1526
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
const stats = this.getSessionStatsInternal();
|
|
1530
|
+
ctx.body = { data: stats };
|
|
1531
|
+
} catch (error2) {
|
|
1532
|
+
strapi2.log.error("[plugin-io] Failed to get session stats:", error2);
|
|
1533
|
+
return ctx.internalServerError("Failed to get session statistics");
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
/**
|
|
1537
|
+
* HTTP Handler: Invalidates all sessions for a specific user
|
|
1538
|
+
* @param {object} ctx - Koa context
|
|
1539
|
+
*/
|
|
1540
|
+
async invalidateUserSessionsHandler(ctx) {
|
|
1541
|
+
const adminUser = ctx.state.user;
|
|
1542
|
+
if (!adminUser) {
|
|
1543
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1544
|
+
}
|
|
1545
|
+
const { userId } = ctx.params;
|
|
1546
|
+
if (!userId) {
|
|
1547
|
+
return ctx.badRequest("User ID is required");
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const userIdNum = parseInt(userId, 10);
|
|
1551
|
+
if (isNaN(userIdNum)) {
|
|
1552
|
+
return ctx.badRequest("Invalid user ID");
|
|
1553
|
+
}
|
|
1554
|
+
const invalidated = this.invalidateUserSessions(userIdNum);
|
|
1555
|
+
strapi2.log.info(`[plugin-io] Admin ${adminUser.id} invalidated ${invalidated} sessions for user ${userIdNum}`);
|
|
1556
|
+
ctx.body = {
|
|
1557
|
+
data: {
|
|
1558
|
+
userId: userIdNum,
|
|
1559
|
+
invalidatedSessions: invalidated,
|
|
1560
|
+
message: `Successfully invalidated ${invalidated} session(s)`
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
} catch (error2) {
|
|
1564
|
+
strapi2.log.error("[plugin-io] Failed to invalidate user sessions:", error2);
|
|
1565
|
+
return ctx.internalServerError("Failed to invalidate sessions");
|
|
1566
|
+
}
|
|
1567
|
+
},
|
|
1568
|
+
/**
|
|
1569
|
+
* HTTP Handler: Gets all online users with their editing info
|
|
1570
|
+
* Used for the "Who's Online" dashboard widget
|
|
1571
|
+
* @param {object} ctx - Koa context
|
|
1572
|
+
*/
|
|
1573
|
+
async getOnlineUsers(ctx) {
|
|
1574
|
+
const adminUser = ctx.state.user;
|
|
1575
|
+
if (!adminUser) {
|
|
1576
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1577
|
+
}
|
|
1578
|
+
try {
|
|
1579
|
+
const presenceService = strapi2.plugin("io").service("presence");
|
|
1580
|
+
const onlineUsers = presenceService.getOnlineUsers();
|
|
1581
|
+
const counts = presenceService.getOnlineCounts();
|
|
1582
|
+
ctx.body = {
|
|
1583
|
+
data: {
|
|
1584
|
+
users: onlineUsers,
|
|
1585
|
+
counts,
|
|
1586
|
+
timestamp: Date.now()
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
} catch (error2) {
|
|
1590
|
+
strapi2.log.error("[plugin-io] Failed to get online users:", error2);
|
|
1591
|
+
return ctx.internalServerError("Failed to get online users");
|
|
1592
|
+
}
|
|
1383
1593
|
}
|
|
1384
1594
|
});
|
|
1385
1595
|
const settings$2 = settings$3;
|
|
@@ -1471,6 +1681,33 @@ var admin$1 = {
|
|
|
1471
1681
|
config: {
|
|
1472
1682
|
policies: ["admin::isAuthenticatedAdmin"]
|
|
1473
1683
|
}
|
|
1684
|
+
},
|
|
1685
|
+
// Security: Session statistics
|
|
1686
|
+
{
|
|
1687
|
+
method: "GET",
|
|
1688
|
+
path: "/security/sessions",
|
|
1689
|
+
handler: "presence.getSessionStats",
|
|
1690
|
+
config: {
|
|
1691
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
// Security: Invalidate user sessions (force logout)
|
|
1695
|
+
{
|
|
1696
|
+
method: "POST",
|
|
1697
|
+
path: "/security/invalidate/:userId",
|
|
1698
|
+
handler: "presence.invalidateUserSessionsHandler",
|
|
1699
|
+
config: {
|
|
1700
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
// Who's Online: Get all online users with editing info
|
|
1704
|
+
{
|
|
1705
|
+
method: "GET",
|
|
1706
|
+
path: "/online-users",
|
|
1707
|
+
handler: "presence.getOnlineUsers",
|
|
1708
|
+
config: {
|
|
1709
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1710
|
+
}
|
|
1474
1711
|
}
|
|
1475
1712
|
]
|
|
1476
1713
|
};
|
|
@@ -29677,21 +29914,55 @@ var strategies = ({ strapi: strapi2 }) => {
|
|
|
29677
29914
|
credentials: function(user) {
|
|
29678
29915
|
return `${this.name}-${user.id}`;
|
|
29679
29916
|
},
|
|
29680
|
-
|
|
29917
|
+
/**
|
|
29918
|
+
* Authenticates admin user via session token
|
|
29919
|
+
* @param {object} auth - Auth object containing token
|
|
29920
|
+
* @param {object} socket - Socket instance for registration
|
|
29921
|
+
* @returns {object} User data if authenticated
|
|
29922
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
29923
|
+
*/
|
|
29924
|
+
authenticate: async function(auth, socket) {
|
|
29681
29925
|
const token2 = auth.token;
|
|
29682
|
-
if (!token2) {
|
|
29926
|
+
if (!token2 || typeof token2 !== "string") {
|
|
29927
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: No token provided");
|
|
29683
29928
|
throw new UnauthorizedError2("Invalid admin credentials");
|
|
29684
29929
|
}
|
|
29930
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29931
|
+
if (!uuidRegex.test(token2)) {
|
|
29932
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: Invalid token format");
|
|
29933
|
+
throw new UnauthorizedError2("Invalid token format");
|
|
29934
|
+
}
|
|
29685
29935
|
try {
|
|
29686
29936
|
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29687
29937
|
const session = presenceController.consumeSessionToken(token2);
|
|
29688
29938
|
if (!session) {
|
|
29939
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: Token not valid or expired");
|
|
29689
29940
|
throw new UnauthorizedError2("Invalid or expired session token");
|
|
29690
29941
|
}
|
|
29691
|
-
|
|
29942
|
+
if (socket?.id) {
|
|
29943
|
+
presenceController.registerSocket(socket.id, token2);
|
|
29944
|
+
}
|
|
29945
|
+
strapi2.log.info(`[plugin-io] Admin authenticated: User ID ${session.userId}`);
|
|
29946
|
+
return {
|
|
29947
|
+
id: session.userId,
|
|
29948
|
+
...session.user
|
|
29949
|
+
};
|
|
29692
29950
|
} catch (error2) {
|
|
29693
|
-
|
|
29694
|
-
|
|
29951
|
+
if (error2 instanceof UnauthorizedError2) {
|
|
29952
|
+
throw error2;
|
|
29953
|
+
}
|
|
29954
|
+
strapi2.log.error("[plugin-io] Admin session verification error:", error2.message);
|
|
29955
|
+
throw new UnauthorizedError2("Authentication failed");
|
|
29956
|
+
}
|
|
29957
|
+
},
|
|
29958
|
+
/**
|
|
29959
|
+
* Cleanup when socket disconnects
|
|
29960
|
+
* @param {object} socket - Socket instance
|
|
29961
|
+
*/
|
|
29962
|
+
onDisconnect: function(socket) {
|
|
29963
|
+
if (socket?.id) {
|
|
29964
|
+
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29965
|
+
presenceController.unregisterSocket(socket.id);
|
|
29695
29966
|
}
|
|
29696
29967
|
},
|
|
29697
29968
|
getRoomName: function(user) {
|
|
@@ -30316,7 +30587,10 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30316
30587
|
*/
|
|
30317
30588
|
registerConnection(socketId, user = null) {
|
|
30318
30589
|
const settings2 = getPresenceSettings();
|
|
30319
|
-
if (!settings2.enabled)
|
|
30590
|
+
if (!settings2.enabled) {
|
|
30591
|
+
strapi2.log.warn(`socket.io: Presence disabled, skipping registration for ${socketId}`);
|
|
30592
|
+
return;
|
|
30593
|
+
}
|
|
30320
30594
|
activeConnections.set(socketId, {
|
|
30321
30595
|
user,
|
|
30322
30596
|
entities: /* @__PURE__ */ new Map(),
|
|
@@ -30324,7 +30598,8 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30324
30598
|
lastSeen: Date.now(),
|
|
30325
30599
|
connectedAt: Date.now()
|
|
30326
30600
|
});
|
|
30327
|
-
|
|
30601
|
+
const username = user?.username || user?.firstname || "anonymous";
|
|
30602
|
+
strapi2.log.info(`socket.io: Presence registered for ${username} (socket: ${socketId}, total: ${activeConnections.size})`);
|
|
30328
30603
|
},
|
|
30329
30604
|
/**
|
|
30330
30605
|
* Unregisters a socket connection and cleans up all entity presence
|
|
@@ -30335,7 +30610,9 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30335
30610
|
if (!connection) return;
|
|
30336
30611
|
if (connection.entities) {
|
|
30337
30612
|
for (const entityKey of connection.entities.keys()) {
|
|
30338
|
-
const
|
|
30613
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30614
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30615
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30339
30616
|
await this.leaveEntity(socketId, uid, documentId, false);
|
|
30340
30617
|
}
|
|
30341
30618
|
}
|
|
@@ -30570,6 +30847,90 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30570
30847
|
timestamp: Date.now()
|
|
30571
30848
|
});
|
|
30572
30849
|
}
|
|
30850
|
+
},
|
|
30851
|
+
/**
|
|
30852
|
+
* Gets all online users with their currently editing entities
|
|
30853
|
+
* Used for the "Who's Online" dashboard widget
|
|
30854
|
+
* @returns {Array} List of online users with their editing info
|
|
30855
|
+
*/
|
|
30856
|
+
getOnlineUsers() {
|
|
30857
|
+
const users = [];
|
|
30858
|
+
const now = Date.now();
|
|
30859
|
+
for (const [socketId, connection] of activeConnections) {
|
|
30860
|
+
if (!connection.user) continue;
|
|
30861
|
+
const editingEntities = [];
|
|
30862
|
+
if (connection.entities) {
|
|
30863
|
+
for (const [entityKey, joinedAt] of connection.entities) {
|
|
30864
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30865
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30866
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30867
|
+
let contentTypeName = uid;
|
|
30868
|
+
try {
|
|
30869
|
+
const contentType = strapi2.contentTypes[uid];
|
|
30870
|
+
if (contentType?.info?.displayName) {
|
|
30871
|
+
contentTypeName = contentType.info.displayName;
|
|
30872
|
+
} else if (contentType?.info?.singularName) {
|
|
30873
|
+
contentTypeName = contentType.info.singularName;
|
|
30874
|
+
}
|
|
30875
|
+
} catch (e) {
|
|
30876
|
+
}
|
|
30877
|
+
editingEntities.push({
|
|
30878
|
+
uid,
|
|
30879
|
+
documentId,
|
|
30880
|
+
contentTypeName,
|
|
30881
|
+
joinedAt,
|
|
30882
|
+
editingFor: Math.floor((now - joinedAt) / 1e3)
|
|
30883
|
+
// seconds
|
|
30884
|
+
});
|
|
30885
|
+
}
|
|
30886
|
+
}
|
|
30887
|
+
users.push({
|
|
30888
|
+
socketId,
|
|
30889
|
+
user: {
|
|
30890
|
+
id: connection.user.id,
|
|
30891
|
+
username: connection.user.username,
|
|
30892
|
+
email: connection.user.email,
|
|
30893
|
+
firstname: connection.user.firstname,
|
|
30894
|
+
lastname: connection.user.lastname,
|
|
30895
|
+
isAdmin: connection.user.isAdmin || false
|
|
30896
|
+
},
|
|
30897
|
+
connectedAt: connection.connectedAt,
|
|
30898
|
+
lastSeen: connection.lastSeen,
|
|
30899
|
+
onlineFor: Math.floor((now - connection.connectedAt) / 1e3),
|
|
30900
|
+
// seconds
|
|
30901
|
+
editingEntities,
|
|
30902
|
+
isEditing: editingEntities.length > 0
|
|
30903
|
+
});
|
|
30904
|
+
}
|
|
30905
|
+
users.sort((a, b) => {
|
|
30906
|
+
if (a.isEditing && !b.isEditing) return -1;
|
|
30907
|
+
if (!a.isEditing && b.isEditing) return 1;
|
|
30908
|
+
return b.connectedAt - a.connectedAt;
|
|
30909
|
+
});
|
|
30910
|
+
return users;
|
|
30911
|
+
},
|
|
30912
|
+
/**
|
|
30913
|
+
* Gets count of online users
|
|
30914
|
+
* @returns {object} Online user counts
|
|
30915
|
+
*/
|
|
30916
|
+
getOnlineCounts() {
|
|
30917
|
+
let total = 0;
|
|
30918
|
+
let admins = 0;
|
|
30919
|
+
let users = 0;
|
|
30920
|
+
let editing = 0;
|
|
30921
|
+
for (const connection of activeConnections.values()) {
|
|
30922
|
+
if (!connection.user) continue;
|
|
30923
|
+
total++;
|
|
30924
|
+
if (connection.user.isAdmin) {
|
|
30925
|
+
admins++;
|
|
30926
|
+
} else {
|
|
30927
|
+
users++;
|
|
30928
|
+
}
|
|
30929
|
+
if (connection.entities?.size > 0) {
|
|
30930
|
+
editing++;
|
|
30931
|
+
}
|
|
30932
|
+
}
|
|
30933
|
+
return { total, admins, users, editing };
|
|
30573
30934
|
}
|
|
30574
30935
|
};
|
|
30575
30936
|
};
|
|
@@ -30786,7 +31147,9 @@ var preview$1 = ({ strapi: strapi2 }) => {
|
|
|
30786
31147
|
getActivePreviewEntities() {
|
|
30787
31148
|
const entities = [];
|
|
30788
31149
|
for (const [entityKey, subscribers] of previewSubscribers) {
|
|
30789
|
-
const
|
|
31150
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
31151
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
31152
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30790
31153
|
entities.push({
|
|
30791
31154
|
uid,
|
|
30792
31155
|
documentId,
|