@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.
@@ -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
- try {
355
- const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
356
- strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
357
- if (decoded.id) {
358
- const users = await strapi2.documents("plugin::users-permissions.user").findMany({
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: user.id,
367
- username: user.username,
368
- email: user.email,
369
- role: user.role?.name || "authenticated"
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
- strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
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: User not found for id: ${decoded.id}`);
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 [uid, id] = room.split(":");
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
- for (const [token, session] of sessionTokens.entries()) {
1360
+ let cleaned = 0;
1361
+ for (const [tokenHash, session] of sessionTokens.entries()) {
1319
1362
  if (session.expiresAt < now) {
1320
- sessionTokens.delete(token);
1363
+ sessionTokens.delete(tokenHash);
1364
+ cleaned++;
1321
1365
  }
1322
1366
  }
1323
- }, 5 * 60 * 1e3);
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 expiresAt = Date.now() + 2 * 60 * 1e3;
1338
- sessionTokens.set(token, {
1339
- token,
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
- expiresAt
1408
+ createdAt: now,
1409
+ expiresAt,
1410
+ usageCount: 0,
1411
+ maxUsage: 10
1412
+ // Max reconnects with same token
1347
1413
  });
1348
- strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.email}`);
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
- user: {
1352
- id: adminUser.id,
1353
- email: adminUser.email,
1354
- firstname: adminUser.firstname,
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 and consumes a session token (one-time use)
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 session = sessionTokens.get(token);
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
- if (session.expiresAt < Date.now()) {
1379
- sessionTokens.delete(token);
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
- authenticate: async function(auth) {
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
- return session.user;
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
- strapi2.log.warn("[plugin-io] Admin session verification failed:", error2.message);
29694
- throw new UnauthorizedError2("Invalid admin credentials");
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) return;
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
- strapi2.log.debug(`socket.io: Presence registered for socket ${socketId}`);
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 [uid, documentId] = entityKey.split(":");
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 [uid, documentId] = entityKey.split(":");
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,