@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.
@@ -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
- try {
323
- const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
324
- strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
325
- if (decoded.id) {
326
- const users = await strapi2.documents("plugin::users-permissions.user").findMany({
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: user.id,
335
- username: user.username,
336
- email: user.email,
337
- role: user.role?.name || "authenticated"
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
- strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
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: User not found for id: ${decoded.id}`);
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 [uid, id] = room.split(":");
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
- for (const [token, session] of sessionTokens.entries()) {
1328
+ let cleaned = 0;
1329
+ for (const [tokenHash, session] of sessionTokens.entries()) {
1287
1330
  if (session.expiresAt < now) {
1288
- sessionTokens.delete(token);
1331
+ sessionTokens.delete(tokenHash);
1332
+ cleaned++;
1289
1333
  }
1290
1334
  }
1291
- }, 5 * 60 * 1e3);
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 expiresAt = Date.now() + 2 * 60 * 1e3;
1306
- sessionTokens.set(token, {
1307
- token,
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
- expiresAt
1376
+ createdAt: now,
1377
+ expiresAt,
1378
+ usageCount: 0,
1379
+ maxUsage: 10
1380
+ // Max reconnects with same token
1315
1381
  });
1316
- strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.email}`);
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
- user: {
1320
- id: adminUser.id,
1321
- email: adminUser.email,
1322
- firstname: adminUser.firstname,
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 and consumes a session token (one-time use)
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 session = sessionTokens.get(token);
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
- if (session.expiresAt < Date.now()) {
1347
- sessionTokens.delete(token);
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
- authenticate: async function(auth) {
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
- return session.user;
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
- strapi2.log.warn("[plugin-io] Admin session verification failed:", error2.message);
29662
- throw new UnauthorizedError2("Invalid admin credentials");
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) return;
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
- strapi2.log.debug(`socket.io: Presence registered for socket ${socketId}`);
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 [uid, documentId] = entityKey.split(":");
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 [uid, documentId] = entityKey.split(":");
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@strapi-community/plugin-io",
4
- "version": "5.1.0",
4
+ "version": "5.3.0",
5
5
  "description": "A plugin for Strapi CMS that provides the ability for Socket IO integration",
6
6
  "keywords": [
7
7
  "strapi",