payload-socket-plugin 1.0.0 → 1.1.1
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 +108 -25
- package/dist/browser.d.ts +24 -0
- package/dist/browser.js +61 -0
- package/dist/socketManager.js +10 -155
- package/dist/types.d.ts +19 -0
- package/package.json +2 -6
- package/dist/mock.d.ts +0 -13
- package/dist/mock.js +0 -19
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Real-time event broadcasting plugin for Payload CMS using Socket.IO with Redis s
|
|
|
18
18
|
## Prerequisites
|
|
19
19
|
|
|
20
20
|
- **Node.js**: >= 20.0.0
|
|
21
|
-
- **Payload CMS**:
|
|
21
|
+
- **Payload CMS**: ^2.0.0 || ^3.0.0
|
|
22
22
|
- **Redis** (optional): Required for multi-instance deployments
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
@@ -61,15 +61,17 @@ export default buildConfig({
|
|
|
61
61
|
},
|
|
62
62
|
path: "/socket.io",
|
|
63
63
|
},
|
|
64
|
-
includeCollections: ["
|
|
64
|
+
includeCollections: ["posts", "users"],
|
|
65
65
|
authorize: {
|
|
66
|
-
projects: async (user, event) => {
|
|
67
|
-
// Only allow project owner to receive events
|
|
68
|
-
return user.id === event.doc.user;
|
|
69
|
-
},
|
|
70
66
|
posts: async (user, event) => {
|
|
71
67
|
// Allow everyone to receive public post events
|
|
72
|
-
return
|
|
68
|
+
return (
|
|
69
|
+
event.doc.status === "published" || user.id === event.doc.author
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
users: async (user, event) => {
|
|
73
|
+
// Only allow user to receive their own events
|
|
74
|
+
return user.id === event.id;
|
|
73
75
|
},
|
|
74
76
|
},
|
|
75
77
|
}),
|
|
@@ -115,16 +117,16 @@ const socket = io("http://localhost:3000", {
|
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
// Subscribe to collection events
|
|
118
|
-
socket.emit("join-collection", "
|
|
120
|
+
socket.emit("join-collection", "posts");
|
|
119
121
|
|
|
120
122
|
// Listen for events
|
|
121
123
|
socket.on("payload:event", (event) => {
|
|
122
124
|
console.log("Event received:", event);
|
|
123
125
|
// {
|
|
124
126
|
// type: 'update',
|
|
125
|
-
// collection: '
|
|
127
|
+
// collection: 'posts',
|
|
126
128
|
// id: '123',
|
|
127
|
-
// doc: { ... },
|
|
129
|
+
// doc: { title: 'My Post', status: 'published', ... },
|
|
128
130
|
// user: { id: '456', email: 'user@example.com' },
|
|
129
131
|
// timestamp: '2024-01-01T00:00:00.000Z'
|
|
130
132
|
// }
|
|
@@ -144,6 +146,7 @@ socket.on("payload:event", (event) => {
|
|
|
144
146
|
| `authorize` | `object` | - | Per-collection authorization handlers |
|
|
145
147
|
| `shouldEmit` | `function` | - | Filter function to determine if event should be emitted |
|
|
146
148
|
| `transformEvent` | `function` | - | Transform events before emitting |
|
|
149
|
+
| `onSocketConnection` | `function` | - | Custom event handlers for each socket connection |
|
|
147
150
|
|
|
148
151
|
## Authorization
|
|
149
152
|
|
|
@@ -152,28 +155,25 @@ Authorization handlers determine which users can receive events for specific doc
|
|
|
152
155
|
```typescript
|
|
153
156
|
import type { CollectionAuthorizationHandler } from "payload-socket-plugin";
|
|
154
157
|
|
|
155
|
-
const
|
|
156
|
-
user,
|
|
157
|
-
event
|
|
158
|
-
) => {
|
|
158
|
+
const authorizePost: CollectionAuthorizationHandler = async (user, event) => {
|
|
159
159
|
// Admin can see all events
|
|
160
160
|
if (user.role === "admin") {
|
|
161
161
|
return true;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// Check if user
|
|
165
|
-
const
|
|
166
|
-
collection: "
|
|
164
|
+
// Check if post is published or user is the author
|
|
165
|
+
const post = await payload.findByID({
|
|
166
|
+
collection: "posts",
|
|
167
167
|
id: event.id as string,
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
return user.id ===
|
|
170
|
+
return post.status === "published" || user.id === post.author;
|
|
171
171
|
};
|
|
172
172
|
|
|
173
173
|
// Use in plugin config
|
|
174
174
|
socketPlugin({
|
|
175
175
|
authorize: {
|
|
176
|
-
|
|
176
|
+
posts: authorizePost,
|
|
177
177
|
},
|
|
178
178
|
});
|
|
179
179
|
```
|
|
@@ -184,13 +184,13 @@ socketPlugin({
|
|
|
184
184
|
|
|
185
185
|
```typescript
|
|
186
186
|
// Subscribe to a single collection
|
|
187
|
-
socket.emit("join-collection", "
|
|
187
|
+
socket.emit("join-collection", "posts");
|
|
188
188
|
|
|
189
189
|
// Subscribe to multiple collections
|
|
190
|
-
socket.emit("subscribe", ["
|
|
190
|
+
socket.emit("subscribe", ["posts", "users", "media"]);
|
|
191
191
|
|
|
192
192
|
// Unsubscribe
|
|
193
|
-
socket.emit("unsubscribe", ["
|
|
193
|
+
socket.emit("unsubscribe", ["posts"]);
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
### Listening for Events
|
|
@@ -198,8 +198,8 @@ socket.emit("unsubscribe", ["projects"]);
|
|
|
198
198
|
```typescript
|
|
199
199
|
// Listen to specific collection events
|
|
200
200
|
socket.on("payload:event", (event) => {
|
|
201
|
-
if (event.collection === "
|
|
202
|
-
// Handle
|
|
201
|
+
if (event.collection === "posts" && event.type === "update") {
|
|
202
|
+
// Handle post update
|
|
203
203
|
}
|
|
204
204
|
});
|
|
205
205
|
|
|
@@ -211,6 +211,80 @@ socket.on("payload:event:all", (event) => {
|
|
|
211
211
|
|
|
212
212
|
## Advanced Usage
|
|
213
213
|
|
|
214
|
+
### Custom Socket Event Handlers
|
|
215
|
+
|
|
216
|
+
You can register your own custom event handlers that will be attached to each authenticated socket.
|
|
217
|
+
|
|
218
|
+
**Simple inline handlers:**
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
socketPlugin({
|
|
222
|
+
onSocketConnection: (socket, io, payload) => {
|
|
223
|
+
// Custom event handler
|
|
224
|
+
socket.on("send-message", async (data) => {
|
|
225
|
+
const { roomId, message } = data;
|
|
226
|
+
|
|
227
|
+
// Broadcast to room
|
|
228
|
+
io.to(`room:${roomId}`).emit("new-message", {
|
|
229
|
+
user: socket.user,
|
|
230
|
+
message,
|
|
231
|
+
timestamp: new Date().toISOString(),
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Custom room management
|
|
236
|
+
socket.on("join-custom-room", (roomId) => {
|
|
237
|
+
socket.join(`room:${roomId}`);
|
|
238
|
+
socket.emit("joined-room", { roomId });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Access Payload CMS from your handlers
|
|
242
|
+
socket.on("get-user-data", async () => {
|
|
243
|
+
const user = await payload.findByID({
|
|
244
|
+
collection: "users",
|
|
245
|
+
id: socket.user!.id as string,
|
|
246
|
+
});
|
|
247
|
+
socket.emit("user-data", user);
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Organized in separate files (recommended):**
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// Import from examples directory
|
|
257
|
+
import { projectHandlers } from "./examples/projectHandlers";
|
|
258
|
+
import { chatHandlers } from "./examples/chatHandlers";
|
|
259
|
+
import { notificationHandlers } from "./examples/notificationHandlers";
|
|
260
|
+
|
|
261
|
+
// Or import all at once
|
|
262
|
+
import {
|
|
263
|
+
projectHandlers,
|
|
264
|
+
chatHandlers,
|
|
265
|
+
notificationHandlers,
|
|
266
|
+
} from "./examples";
|
|
267
|
+
|
|
268
|
+
socketPlugin({
|
|
269
|
+
onSocketConnection: (socket, io, payload) => {
|
|
270
|
+
// Use one or more pre-built handlers
|
|
271
|
+
projectHandlers(socket, io, payload);
|
|
272
|
+
chatHandlers(socket, io, payload);
|
|
273
|
+
notificationHandlers(socket, io, payload);
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**See the [examples directory](./examples) for complete implementations** including:
|
|
279
|
+
|
|
280
|
+
| Handler | Features |
|
|
281
|
+
| ------------------------- | -------------------------------------------------------------------- |
|
|
282
|
+
| **Project Collaboration** | Join/leave rooms, permission checking, presence tracking, kick users |
|
|
283
|
+
| **Chat/Messaging** | Send messages, typing indicators, read receipts |
|
|
284
|
+
| **Notifications** | User notifications, broadcast announcements (admin only) |
|
|
285
|
+
|
|
286
|
+
Each example includes full client-side and server-side code with error handling and best practices.
|
|
287
|
+
|
|
214
288
|
### Custom Event Filtering
|
|
215
289
|
|
|
216
290
|
```typescript
|
|
@@ -271,6 +345,16 @@ socketPlugin({
|
|
|
271
345
|
4. Authorization handlers determine which users receive the event
|
|
272
346
|
5. Redis adapter ensures events sync across multiple server instances
|
|
273
347
|
|
|
348
|
+
## Browser Compatibility
|
|
349
|
+
|
|
350
|
+
This plugin includes automatic browser-safe mocking for the Payload admin panel. When bundled for the browser (e.g., in the Payload admin UI), the plugin automatically uses a mock implementation that:
|
|
351
|
+
|
|
352
|
+
- Returns the config unchanged (no Socket.IO server initialization)
|
|
353
|
+
- Provides no-op functions for `initSocketIO()` and `SocketIOManager` methods
|
|
354
|
+
- Prevents server-side dependencies (Socket.IO, Redis) from being bundled in the browser
|
|
355
|
+
|
|
356
|
+
This is handled automatically via the `"browser"` field in `package.json`, so you don't need to configure anything special. The Socket.IO server only runs on the server side.
|
|
357
|
+
|
|
274
358
|
## Environment Variables
|
|
275
359
|
|
|
276
360
|
```bash
|
|
@@ -353,7 +437,6 @@ import type {
|
|
|
353
437
|
|
|
354
438
|
## Known Limitations
|
|
355
439
|
|
|
356
|
-
- Only supports Payload CMS v2.x (v3.x support coming soon)
|
|
357
440
|
- Authorization handlers are called for each connected user on every event
|
|
358
441
|
- No built-in event replay or history mechanism
|
|
359
442
|
- Redis is required for multi-instance deployments
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Config } from "payload/config";
|
|
2
|
+
/**
|
|
3
|
+
* Browser-safe mock for the Socket.IO plugin
|
|
4
|
+
* This file is used when bundling for the browser (e.g., Payload admin panel)
|
|
5
|
+
* The actual Socket.IO server only runs on the server side
|
|
6
|
+
*/
|
|
7
|
+
export declare const socketPlugin: () => (config: Config) => Config;
|
|
8
|
+
/**
|
|
9
|
+
* Browser-safe mock for initSocketIO
|
|
10
|
+
* Does nothing in browser environment
|
|
11
|
+
*/
|
|
12
|
+
export declare const initSocketIO: () => Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Browser-safe mock for SocketIOManager
|
|
15
|
+
* Returns a mock class that does nothing in browser environment
|
|
16
|
+
*/
|
|
17
|
+
export declare class SocketIOManager {
|
|
18
|
+
constructor();
|
|
19
|
+
init(): Promise<null>;
|
|
20
|
+
emitEvent(): Promise<void>;
|
|
21
|
+
getIO(): null;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export * from "./types";
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.SocketIOManager = exports.initSocketIO = exports.socketPlugin = void 0;
|
|
18
|
+
/**
|
|
19
|
+
* Browser-safe mock for the Socket.IO plugin
|
|
20
|
+
* This file is used when bundling for the browser (e.g., Payload admin panel)
|
|
21
|
+
* The actual Socket.IO server only runs on the server side
|
|
22
|
+
*/
|
|
23
|
+
const socketPlugin = () => (config) => {
|
|
24
|
+
// Return config unchanged - Socket.IO server is server-side only
|
|
25
|
+
return config;
|
|
26
|
+
};
|
|
27
|
+
exports.socketPlugin = socketPlugin;
|
|
28
|
+
/**
|
|
29
|
+
* Browser-safe mock for initSocketIO
|
|
30
|
+
* Does nothing in browser environment
|
|
31
|
+
*/
|
|
32
|
+
const initSocketIO = async () => {
|
|
33
|
+
// No-op in browser
|
|
34
|
+
console.warn("initSocketIO called in browser environment - this is a no-op");
|
|
35
|
+
};
|
|
36
|
+
exports.initSocketIO = initSocketIO;
|
|
37
|
+
/**
|
|
38
|
+
* Browser-safe mock for SocketIOManager
|
|
39
|
+
* Returns a mock class that does nothing in browser environment
|
|
40
|
+
*/
|
|
41
|
+
class SocketIOManager {
|
|
42
|
+
constructor() {
|
|
43
|
+
// No-op in browser
|
|
44
|
+
}
|
|
45
|
+
async init() {
|
|
46
|
+
console.warn("SocketIOManager.init called in browser environment - this is a no-op");
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
async emitEvent() {
|
|
50
|
+
// No-op in browser
|
|
51
|
+
}
|
|
52
|
+
getIO() {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
async close() {
|
|
56
|
+
// No-op in browser
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.SocketIOManager = SocketIOManager;
|
|
60
|
+
// Export types (these are safe for browser)
|
|
61
|
+
__exportStar(require("./types"), exports);
|
package/dist/socketManager.js
CHANGED
|
@@ -123,7 +123,7 @@ class SocketIOManager {
|
|
|
123
123
|
* Setup connection event handlers
|
|
124
124
|
*/
|
|
125
125
|
setupConnectionHandlers() {
|
|
126
|
-
this.io.on("connection", (socket) => {
|
|
126
|
+
this.io.on("connection", async (socket) => {
|
|
127
127
|
payload_1.default.logger.info(`Client connected: ${socket.id}, User: ${socket.user?.email || socket.user?.id}`);
|
|
128
128
|
// Allow clients to subscribe to specific collections
|
|
129
129
|
socket.on("subscribe", (collections) => {
|
|
@@ -135,7 +135,7 @@ class SocketIOManager {
|
|
|
135
135
|
payload_1.default.logger.info(`Client ${socket.id} subscribed to collection: ${collection}`);
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
|
-
//
|
|
138
|
+
// Allow clients to unsubscribe from collections
|
|
139
139
|
socket.on("unsubscribe", (collections) => {
|
|
140
140
|
const collectionList = Array.isArray(collections)
|
|
141
141
|
? collections
|
|
@@ -145,169 +145,24 @@ class SocketIOManager {
|
|
|
145
145
|
payload_1.default.logger.info(`Client ${socket.id} unsubscribed from collection: ${collection}`);
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
|
-
// Allow clients to join collection rooms
|
|
148
|
+
// Allow clients to join collection rooms (alias for subscribe)
|
|
149
149
|
socket.on("join-collection", (collection) => {
|
|
150
150
|
const roomName = `collection:${collection}`;
|
|
151
151
|
socket.join(roomName);
|
|
152
152
|
payload_1.default.logger.info(`Client ${socket.id} (${socket.user?.email}) joined collection room: ${roomName}`);
|
|
153
153
|
});
|
|
154
|
-
//
|
|
155
|
-
socket.on("
|
|
156
|
-
|
|
157
|
-
payload_1.default.logger.warn(`Client ${socket.id} tried to join project without ID`);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
// Check if user has permission to join this project
|
|
161
|
-
try {
|
|
162
|
-
const project = await payload_1.default.findByID({
|
|
163
|
-
collection: "projects",
|
|
164
|
-
id: projectId,
|
|
165
|
-
depth: 0,
|
|
166
|
-
});
|
|
167
|
-
const projectOwnerId = typeof project.user === "string"
|
|
168
|
-
? project.user
|
|
169
|
-
: project.user?.id;
|
|
170
|
-
const isOwner = socket.user.id === projectOwnerId;
|
|
171
|
-
// Check if user has editor invitation
|
|
172
|
-
const invitation = await payload_1.default.find({
|
|
173
|
-
collection: "projectInvitations",
|
|
174
|
-
depth: 0,
|
|
175
|
-
where: {
|
|
176
|
-
user: { equals: socket.user.id },
|
|
177
|
-
status: { equals: "accepted" },
|
|
178
|
-
project: { equals: projectId },
|
|
179
|
-
role: { equals: "editor" },
|
|
180
|
-
},
|
|
181
|
-
limit: 1,
|
|
182
|
-
});
|
|
183
|
-
const hasEditorInvite = invitation.docs.length > 0;
|
|
184
|
-
// Only allow owner, users with editor invitation
|
|
185
|
-
if (!isOwner && !hasEditorInvite) {
|
|
186
|
-
payload_1.default.logger.warn(`Client ${socket.id} (${socket.user?.email}) denied access to project ${projectId} - no editor permission`);
|
|
187
|
-
socket.emit("join-project-error", {
|
|
188
|
-
message: "You need editor access to join this project room",
|
|
189
|
-
});
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
payload_1.default.logger.error("Error checking project permissions:", error);
|
|
195
|
-
socket.emit("join-project-error", {
|
|
196
|
-
message: "Failed to verify project permissions",
|
|
197
|
-
});
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const roomName = `project:${projectId}:room`;
|
|
201
|
-
await socket.join(roomName);
|
|
202
|
-
payload_1.default.logger.info(`Client ${socket.id} (${socket.user?.email || socket.user?.id}) joined project: ${projectId}, room: ${roomName}`);
|
|
203
|
-
// Debug: Log all rooms this socket is in
|
|
204
|
-
payload_1.default.logger.info(`Socket ${socket.id} is now in rooms: ${Array.from(socket.rooms).join(", ")}`);
|
|
205
|
-
// Get all sockets in this project room
|
|
206
|
-
const socketsInRoom = await this.io.in(roomName).fetchSockets();
|
|
207
|
-
// Build list of active users
|
|
208
|
-
const activeUsers = socketsInRoom
|
|
209
|
-
.map((s) => {
|
|
210
|
-
if (s.user) {
|
|
211
|
-
return {
|
|
212
|
-
id: s.user.id,
|
|
213
|
-
email: s.user.email || undefined,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
return null;
|
|
217
|
-
})
|
|
218
|
-
.filter((u) => u !== null);
|
|
219
|
-
// Remove duplicates (same user, multiple tabs)
|
|
220
|
-
const uniqueUsers = Array.from(new Map(activeUsers.map((u) => [u.id, u])).values());
|
|
221
|
-
// Send current active users to the joining client
|
|
222
|
-
socket.emit("project:active-users", uniqueUsers);
|
|
223
|
-
// Notify others in the room that a new user joined
|
|
224
|
-
socket.to(roomName).emit("project:user-joined", {
|
|
225
|
-
id: socket.user.id,
|
|
226
|
-
email: socket.user.email || undefined,
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
socket.on("leave-project", (projectId) => {
|
|
230
|
-
if (!projectId)
|
|
231
|
-
return;
|
|
232
|
-
const roomName = `project:${projectId}:room`;
|
|
233
|
-
socket.leave(roomName);
|
|
234
|
-
payload_1.default.logger.info(`Client ${socket.id} left project: ${projectId}`);
|
|
235
|
-
// Notify others that user left
|
|
236
|
-
socket.to(roomName).emit("project:user-left", socket.user.id);
|
|
154
|
+
// Handle disconnection
|
|
155
|
+
socket.on("disconnect", () => {
|
|
156
|
+
payload_1.default.logger.info(`Client disconnected: ${socket.id}, User: ${socket.user?.email || socket.user?.id}`);
|
|
237
157
|
});
|
|
238
|
-
|
|
239
|
-
const { projectId, userId } = data;
|
|
240
|
-
if (!projectId || !userId) {
|
|
241
|
-
payload_1.default.logger.warn(`Client ${socket.id} tried to kick user without projectId or userId`);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
// Verify the kicker is the project owner
|
|
158
|
+
if (this.options.onSocketConnection) {
|
|
245
159
|
try {
|
|
246
|
-
|
|
247
|
-
collection: "projects",
|
|
248
|
-
id: projectId,
|
|
249
|
-
});
|
|
250
|
-
const projectOwnerId = typeof project.user === "string"
|
|
251
|
-
? project.user
|
|
252
|
-
: project.user?.id;
|
|
253
|
-
if (socket.user.id !== projectOwnerId) {
|
|
254
|
-
payload_1.default.logger.warn(`Client ${socket.id} (${socket.user?.email}) tried to kick user but is not the owner`);
|
|
255
|
-
socket.emit("kick-error", {
|
|
256
|
-
message: "Only the project owner can kick users",
|
|
257
|
-
});
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// Find all sockets for the user to kick
|
|
261
|
-
const roomName = `project:${projectId}:room`;
|
|
262
|
-
const socketsInRoom = await this.io.in(roomName).fetchSockets();
|
|
263
|
-
let kicked = false;
|
|
264
|
-
for (const s of socketsInRoom) {
|
|
265
|
-
const socketUser = s.user;
|
|
266
|
-
if (socketUser && socketUser.id === userId) {
|
|
267
|
-
// Emit kick event to the user being kicked
|
|
268
|
-
s.emit("kicked-from-project", {
|
|
269
|
-
projectId,
|
|
270
|
-
message: "You have been removed from this project by the owner",
|
|
271
|
-
});
|
|
272
|
-
// Remove them from the room
|
|
273
|
-
s.leave(roomName);
|
|
274
|
-
kicked = true;
|
|
275
|
-
payload_1.default.logger.info(`User ${userId} (${socketUser.email}) was kicked from project ${projectId} by ${socket.user?.email}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (kicked) {
|
|
279
|
-
// Notify others that user was kicked
|
|
280
|
-
socket.to(roomName).emit("project:user-left", userId);
|
|
281
|
-
// Confirm to the kicker
|
|
282
|
-
socket.emit("kick-success", { userId });
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
socket.emit("kick-error", {
|
|
286
|
-
message: "User not found in project",
|
|
287
|
-
});
|
|
288
|
-
}
|
|
160
|
+
await this.options.onSocketConnection(socket, this.io, payload_1.default);
|
|
289
161
|
}
|
|
290
162
|
catch (error) {
|
|
291
|
-
payload_1.default.logger.error(
|
|
292
|
-
socket.emit("kick-error", {
|
|
293
|
-
message: "Failed to kick user",
|
|
294
|
-
});
|
|
163
|
+
payload_1.default.logger.error(`Error in custom socket connection handler: ${error}`);
|
|
295
164
|
}
|
|
296
|
-
}
|
|
297
|
-
// Handle disconnection - clean up project presence
|
|
298
|
-
socket.on("disconnect", async () => {
|
|
299
|
-
payload_1.default.logger.info(`Client disconnected: ${socket.id}, User: ${socket.user?.email || socket.user?.id}`);
|
|
300
|
-
// Notify all project rooms that this user left
|
|
301
|
-
// Socket.IO automatically tracks which rooms the socket was in
|
|
302
|
-
const rooms = Array.from(socket.rooms);
|
|
303
|
-
for (const room of rooms) {
|
|
304
|
-
// Only process project rooms (skip the socket's own room)
|
|
305
|
-
if (room.startsWith("project:") && socket.user) {
|
|
306
|
-
socket.to(room).emit("project:user-left", socket.user.id);
|
|
307
|
-
payload_1.default.logger.info(`Notified room ${room} that user ${socket.user.id} left`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
});
|
|
165
|
+
}
|
|
311
166
|
});
|
|
312
167
|
}
|
|
313
168
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -110,4 +110,23 @@ export interface RealtimeEventsPluginOptions {
|
|
|
110
110
|
* Custom event transformer
|
|
111
111
|
*/
|
|
112
112
|
transformEvent?: (event: RealtimeEventPayload) => RealtimeEventPayload;
|
|
113
|
+
/**
|
|
114
|
+
* Custom socket event handlers
|
|
115
|
+
* Register your own event handlers that will be attached to each authenticated socket
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* onSocketConnection: (socket, io, payload) => {
|
|
120
|
+
* socket.on('custom-event', (data) => {
|
|
121
|
+
* // Handle custom event
|
|
122
|
+
* socket.emit('custom-response', { success: true });
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* socket.on('join-room', (roomId) => {
|
|
126
|
+
* socket.join(`custom:${roomId}`);
|
|
127
|
+
* });
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
onSocketConnection?: (socket: AuthenticatedSocket, io: any, payload: any) => void | Promise<void>;
|
|
113
132
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-socket-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Real-time Socket.IO plugin for Payload CMS with Redis support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
-
"browser": "dist/
|
|
7
|
+
"browser": "dist/browser.js",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
10
10
|
"README.md"
|
|
@@ -30,9 +30,6 @@
|
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=20.0.0"
|
|
32
32
|
},
|
|
33
|
-
"peerDependencies": {
|
|
34
|
-
"payload": "^2.0.0"
|
|
35
|
-
},
|
|
36
33
|
"dependencies": {
|
|
37
34
|
"@socket.io/redis-adapter": "^8.0.0",
|
|
38
35
|
"ioredis": "^5.3.0",
|
|
@@ -42,7 +39,6 @@
|
|
|
42
39
|
"devDependencies": {
|
|
43
40
|
"@types/jsonwebtoken": "^9.0.0",
|
|
44
41
|
"@types/node": "^20.0.0",
|
|
45
|
-
"payload": "^2.0.0",
|
|
46
42
|
"typescript": "^5.0.0"
|
|
47
43
|
},
|
|
48
44
|
"repository": {
|
package/dist/mock.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mock module for socket plugin
|
|
3
|
-
* Used by webpack to prevent bundling server-side Socket.IO code in the admin panel
|
|
4
|
-
*
|
|
5
|
-
* This is aliased in payload.config.ts webpack configuration:
|
|
6
|
-
* [socketPluginPath]: socketPluginMockPath
|
|
7
|
-
*/
|
|
8
|
-
import { Config } from "payload/config";
|
|
9
|
-
/**
|
|
10
|
-
* Mock plugin that does nothing
|
|
11
|
-
* The real plugin is only used on the server side
|
|
12
|
-
*/
|
|
13
|
-
export declare const socketPlugin: () => (config: Config) => Config;
|
package/dist/mock.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Mock module for socket plugin
|
|
4
|
-
* Used by webpack to prevent bundling server-side Socket.IO code in the admin panel
|
|
5
|
-
*
|
|
6
|
-
* This is aliased in payload.config.ts webpack configuration:
|
|
7
|
-
* [socketPluginPath]: socketPluginMockPath
|
|
8
|
-
*/
|
|
9
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.socketPlugin = void 0;
|
|
11
|
-
/**
|
|
12
|
-
* Mock plugin that does nothing
|
|
13
|
-
* The real plugin is only used on the server side
|
|
14
|
-
*/
|
|
15
|
-
const socketPlugin = () => (config) => {
|
|
16
|
-
// Return config unchanged - no Socket.IO in admin panel
|
|
17
|
-
return config;
|
|
18
|
-
};
|
|
19
|
-
exports.socketPlugin = socketPlugin;
|