@signe/room 2.3.3 → 2.4.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/dist/index.d.ts +65 -2
- package/dist/index.js +229 -13
- package/dist/index.js.map +1 -1
- package/examples/game/app/client.tsx +2 -1
- package/examples/game/app/components/Room.tsx +8 -4
- package/examples/game/party/game.room.ts +14 -1
- package/examples/game/party/server.ts +2 -2
- package/package.json +2 -2
- package/src/index.ts +2 -1
- package/src/mock.ts +4 -3
- package/src/server.ts +231 -10
- package/src/session.guard.ts +111 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from "react";
|
|
2
|
-
import { connectionWorld } from '../../../../../sync/src/client';
|
|
2
|
+
import { connectionRoom, connectionWorld } from '../../../../../sync/src/client';
|
|
3
3
|
import { RoomSchema } from "../../shared/room.schema";
|
|
4
4
|
import { effect } from "@signe/reactive";
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ export default function Room() {
|
|
|
7
7
|
const [isConnected, setIsConnected] = useState(false);
|
|
8
8
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
9
9
|
const [error, setError] = useState<string | null>(null);
|
|
10
|
-
const [roomId, setRoomId] = useState("
|
|
10
|
+
const [roomId, setRoomId] = useState("game");
|
|
11
11
|
const [count, setCount] = useState(0);
|
|
12
12
|
const socketRef = useRef<any>(null);
|
|
13
13
|
const roomRef = useRef<any>(null);
|
|
@@ -21,11 +21,15 @@ export default function Room() {
|
|
|
21
21
|
roomRef.current = new RoomSchema();
|
|
22
22
|
|
|
23
23
|
// Connect to the room through the World service with auto-creation enabled
|
|
24
|
-
socketRef.current = await
|
|
24
|
+
socketRef.current = await connectionRoom({
|
|
25
25
|
host: 'http://localhost:1999',
|
|
26
|
+
id: 'test',
|
|
26
27
|
room: roomId,
|
|
27
|
-
autoCreate: true // Enable auto-creation of room and shards
|
|
28
28
|
}, roomRef.current);
|
|
29
|
+
|
|
30
|
+
socketRef.current.on('sync', (data) => {
|
|
31
|
+
console.log('sync', data);
|
|
32
|
+
})
|
|
29
33
|
|
|
30
34
|
// Listen for disconnection events
|
|
31
35
|
socketRef.current.on('disconnect', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Action, Guard, Room } from "../../../src";
|
|
1
|
+
import { Action, Guard, requireSession, Room } from "../../../src";
|
|
2
2
|
import { RoomSchema } from "../shared/room.schema";
|
|
3
3
|
|
|
4
4
|
@Room({
|
|
@@ -10,10 +10,23 @@ export class GameRoom extends RoomSchema {
|
|
|
10
10
|
increment(player) {
|
|
11
11
|
this.count.update((count) => count + 1);
|
|
12
12
|
player.score.update((score) => score + 1);
|
|
13
|
+
this.$sessionTransfer(player.id(), "protected-1");
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
async onRequest(req: Party.Request, room: any) {
|
|
16
17
|
const map = await room.storage.list() as Map<string, any>;
|
|
17
18
|
return Object.fromEntries(map);
|
|
18
19
|
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Room({
|
|
23
|
+
path: 'protected-{gameId}',
|
|
24
|
+
sessionExpiryTime: 5000,
|
|
25
|
+
guards: [requireSession]
|
|
26
|
+
})
|
|
27
|
+
export class ProtectedRoom extends RoomSchema {
|
|
28
|
+
@Action('increment')
|
|
29
|
+
increment(player) {
|
|
30
|
+
console.log('increment', player);
|
|
31
|
+
}
|
|
19
32
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Server, WorldRoom } from '../../../src';
|
|
2
2
|
import type * as Party from "../../../src/types/party";
|
|
3
|
-
import { GameRoom } from "./game.room";
|
|
3
|
+
import { GameRoom, ProtectedRoom } from "./game.room";
|
|
4
4
|
|
|
5
5
|
export default class MainServer extends Server {
|
|
6
6
|
rooms = [
|
|
7
7
|
GameRoom ,
|
|
8
|
-
|
|
8
|
+
ProtectedRoom
|
|
9
9
|
]
|
|
10
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signe/room",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"keywords": [],
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dset": "^3.1.3",
|
|
18
18
|
"partysocket": "^1.0.1",
|
|
19
19
|
"zod": "^3.23.8",
|
|
20
|
-
"@signe/sync": "2.
|
|
20
|
+
"@signe/sync": "2.4.1"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
package/src/index.ts
CHANGED
package/src/mock.ts
CHANGED
|
@@ -47,14 +47,15 @@ export class MockPartyClient {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
class MockLobby {
|
|
50
|
-
constructor(public server: Server) {}
|
|
50
|
+
constructor(public server: Server, public lobbyId: string) {}
|
|
51
51
|
|
|
52
52
|
socket() {
|
|
53
53
|
return new MockPartyClient(this.server)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
fetch(url: string, options: any) {
|
|
57
|
-
|
|
57
|
+
const baseUrl = url.includes('shard') ? '' :( '/parties/main/' + this.lobbyId )
|
|
58
|
+
return request(this.server, baseUrl + url, options)
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -68,7 +69,7 @@ class MockContext {
|
|
|
68
69
|
constructor(public room: MockPartyRoom, options: any = {}) {
|
|
69
70
|
const parties = options.parties || {}
|
|
70
71
|
for (let lobbyId in parties) {
|
|
71
|
-
this.parties.main.set(lobbyId, new MockLobby(parties[lobbyId](room)))
|
|
72
|
+
this.parties.main.set(lobbyId, new MockLobby(parties[lobbyId](room), lobbyId))
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
}
|
package/src/server.ts
CHANGED
|
@@ -245,6 +245,126 @@ export class Server implements Party.Server {
|
|
|
245
245
|
instance.$broadcast = (obj: any) => {
|
|
246
246
|
return this.broadcast(obj, instance)
|
|
247
247
|
}
|
|
248
|
+
instance.$sessionTransfer = async (userOrPublicId: any | string, targetRoomId: string) => {
|
|
249
|
+
let user: any;
|
|
250
|
+
let publicId: string | null = null;
|
|
251
|
+
|
|
252
|
+
const signal = this.getUsersProperty(instance);
|
|
253
|
+
if (!signal) {
|
|
254
|
+
console.error('[sessionTransfer] `users` property not defined in the room.');
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check if the first parameter is a string (publicId) or an object (user)
|
|
259
|
+
if (typeof userOrPublicId === 'string') {
|
|
260
|
+
publicId = userOrPublicId;
|
|
261
|
+
user = signal()[publicId];
|
|
262
|
+
if (!user) {
|
|
263
|
+
console.error(`[sessionTransfer] User with publicId ${publicId} not found.`);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
user = userOrPublicId;
|
|
268
|
+
const users = signal();
|
|
269
|
+
|
|
270
|
+
// Try to find the publicId by comparing object references
|
|
271
|
+
for (const [id, u] of Object.entries(users)) {
|
|
272
|
+
if (u === user) {
|
|
273
|
+
publicId = id;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// If not found by reference, try to find by user properties (fallback)
|
|
279
|
+
if (!publicId && user && typeof user === 'object') {
|
|
280
|
+
// Look for a unique identifier in the user object
|
|
281
|
+
for (const [id, u] of Object.entries(users)) {
|
|
282
|
+
if (u && typeof u === 'object') {
|
|
283
|
+
// Compare by constructor and other identifying properties
|
|
284
|
+
if (u.constructor === user.constructor) {
|
|
285
|
+
// Additional checks could be added here based on user structure
|
|
286
|
+
publicId = id;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!publicId) {
|
|
294
|
+
console.error('[sessionTransfer] User not found in users collection.', {
|
|
295
|
+
userType: user?.constructor?.name,
|
|
296
|
+
userKeys: user ? Object.keys(user) : 'null',
|
|
297
|
+
usersCount: Object.keys(users).length,
|
|
298
|
+
userIds: Object.keys(users)
|
|
299
|
+
});
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const sessions = await this.room.storage.list();
|
|
305
|
+
let userSession: any = null;
|
|
306
|
+
let privateId: string | null = null;
|
|
307
|
+
|
|
308
|
+
for (const [key, session] of sessions) {
|
|
309
|
+
if (key.startsWith('session:') && (session as any).publicId === publicId) {
|
|
310
|
+
userSession = session;
|
|
311
|
+
privateId = key.replace('session:', '');
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!userSession || !privateId) {
|
|
317
|
+
console.error(`[sessionTransfer] Session for publicId ${publicId} not found.`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const usersPropName = this.getUsersPropName(instance);
|
|
322
|
+
if (!usersPropName) {
|
|
323
|
+
console.error('[sessionTransfer] `users` property not defined in the room.');
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Create a snapshot of the user state
|
|
328
|
+
const userSnapshot = createStatesSnapshot(user);
|
|
329
|
+
|
|
330
|
+
const transferData = {
|
|
331
|
+
privateId,
|
|
332
|
+
userSnapshot,
|
|
333
|
+
sessionState: userSession.state,
|
|
334
|
+
publicId
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const targetRoomParty = this.room.context.parties.main.get(targetRoomId);
|
|
339
|
+
const response = await targetRoomParty.fetch('/session-transfer', {
|
|
340
|
+
method: 'POST',
|
|
341
|
+
body: JSON.stringify(transferData),
|
|
342
|
+
headers: {
|
|
343
|
+
'Content-Type': 'application/json'
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
throw new Error(`Transfer request failed: ${await response.text()}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const { transferToken } = await response.json();
|
|
352
|
+
|
|
353
|
+
// On success, remove user from current room
|
|
354
|
+
await this.deleteSession(privateId);
|
|
355
|
+
await this.room.storage.delete(`${usersPropName}.${publicId}`);
|
|
356
|
+
|
|
357
|
+
const currentUsers = signal();
|
|
358
|
+
if (currentUsers[publicId]) {
|
|
359
|
+
delete currentUsers[publicId];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return transferToken;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error(`[sessionTransfer] Failed to transfer session to room ${targetRoomId}:`, error);
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
248
368
|
|
|
249
369
|
// Sync callback: Broadcast changes to all clients
|
|
250
370
|
const syncCb = (values) => {
|
|
@@ -464,18 +584,32 @@ export class Server implements Party.Server {
|
|
|
464
584
|
// Check room guards
|
|
465
585
|
const roomGuards = subRoom.constructor['_roomGuards'] || [];
|
|
466
586
|
for (const guard of roomGuards) {
|
|
467
|
-
const isAuthorized = await guard(conn, ctx);
|
|
587
|
+
const isAuthorized = await guard(conn, ctx, this.room);
|
|
468
588
|
if (!isAuthorized) {
|
|
469
589
|
conn.close();
|
|
470
590
|
return;
|
|
471
591
|
}
|
|
472
592
|
}
|
|
473
593
|
|
|
594
|
+
// Handle session transfer
|
|
595
|
+
let transferToken = null;
|
|
596
|
+
if (ctx.request?.url) {
|
|
597
|
+
const url = new URL(ctx.request.url);
|
|
598
|
+
transferToken = url.searchParams.get('transferToken');
|
|
599
|
+
}
|
|
600
|
+
let transferData: any = null;
|
|
601
|
+
if (transferToken) {
|
|
602
|
+
transferData = await this.room.storage.get(`transfer:${transferToken}`);
|
|
603
|
+
if (transferData) {
|
|
604
|
+
await this.room.storage.delete(`transfer:${transferToken}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
474
608
|
// Check for existing session
|
|
475
609
|
const existingSession = await this.getSession(conn.id)
|
|
476
610
|
|
|
477
611
|
// Generate IDs
|
|
478
|
-
const publicId = existingSession?.publicId || generateShortUUID();
|
|
612
|
+
const publicId = existingSession?.publicId || transferData?.publicId || generateShortUUID();
|
|
479
613
|
|
|
480
614
|
let user = null;
|
|
481
615
|
const signal = this.getUsersProperty(subRoom);
|
|
@@ -486,10 +620,15 @@ export class Server implements Party.Server {
|
|
|
486
620
|
|
|
487
621
|
// Restore state if exists
|
|
488
622
|
if (!existingSession?.publicId) {
|
|
489
|
-
|
|
490
|
-
signal()[publicId]
|
|
491
|
-
|
|
492
|
-
|
|
623
|
+
// Check if we have a transferred user already restored
|
|
624
|
+
if (transferData?.restored && signal()[publicId]) {
|
|
625
|
+
user = signal()[publicId];
|
|
626
|
+
} else {
|
|
627
|
+
user = isClass(classType) ? new classType() : classType(conn, ctx);
|
|
628
|
+
signal()[publicId] = user;
|
|
629
|
+
const snapshot = createStatesSnapshot(user);
|
|
630
|
+
this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
|
|
631
|
+
}
|
|
493
632
|
}
|
|
494
633
|
else {
|
|
495
634
|
user = signal()[existingSession.publicId];
|
|
@@ -497,7 +636,9 @@ export class Server implements Party.Server {
|
|
|
497
636
|
|
|
498
637
|
// Only store new session if it doesn't exist
|
|
499
638
|
if (!existingSession) {
|
|
500
|
-
|
|
639
|
+
// Use the transferred privateId if available, otherwise use connection id
|
|
640
|
+
const sessionPrivateId = transferData?.privateId || conn.id;
|
|
641
|
+
await this.saveSession(sessionPrivateId, {
|
|
501
642
|
publicId
|
|
502
643
|
});
|
|
503
644
|
}
|
|
@@ -858,7 +999,7 @@ export class Server implements Party.Server {
|
|
|
858
999
|
* @method onClose
|
|
859
1000
|
* @async
|
|
860
1001
|
* @param {Party.Connection} conn - The connection object of the disconnecting user.
|
|
861
|
-
* @description Handles user disconnection, removing them from the room and triggering the onLeave event
|
|
1002
|
+
* @description Handles user disconnection, removing them from the room and triggering the onLeave event..
|
|
862
1003
|
* @returns {Promise<void>}
|
|
863
1004
|
*
|
|
864
1005
|
* @example
|
|
@@ -946,6 +1087,80 @@ export class Server implements Party.Server {
|
|
|
946
1087
|
return this.handleDirectRequest(req, res);
|
|
947
1088
|
}
|
|
948
1089
|
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* @method handleSessionRestore
|
|
1093
|
+
* @private
|
|
1094
|
+
* @async
|
|
1095
|
+
* @param {Party.Request} req - The HTTP request for session restore
|
|
1096
|
+
* @param {ServerResponse} res - The response object
|
|
1097
|
+
* @description Handles session restoration from transfer data, creates session from privateId
|
|
1098
|
+
* @returns {Promise<Response>} The response to return to the client
|
|
1099
|
+
*/
|
|
1100
|
+
private async handleSessionRestore(req: Party.Request, res: ServerResponse): Promise<Response> {
|
|
1101
|
+
try {
|
|
1102
|
+
const transferData = await req.json() as {
|
|
1103
|
+
privateId: string;
|
|
1104
|
+
userSnapshot?: any;
|
|
1105
|
+
sessionState?: any;
|
|
1106
|
+
publicId: string;
|
|
1107
|
+
};
|
|
1108
|
+
const { privateId, userSnapshot, sessionState, publicId } = transferData;
|
|
1109
|
+
|
|
1110
|
+
if (!privateId || !publicId) {
|
|
1111
|
+
return res.badRequest('Missing privateId or publicId in transfer data');
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const subRoom = await this.getSubRoom();
|
|
1115
|
+
if (!subRoom) {
|
|
1116
|
+
return res.serverError('Room not available');
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Create session from privateId
|
|
1120
|
+
await this.saveSession(privateId, {
|
|
1121
|
+
publicId,
|
|
1122
|
+
state: sessionState,
|
|
1123
|
+
created: Date.now(),
|
|
1124
|
+
connected: false // Will be set to true when user connects
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// If userSnapshot exists, restore user data
|
|
1128
|
+
if (userSnapshot) {
|
|
1129
|
+
const signal = this.getUsersProperty(subRoom);
|
|
1130
|
+
const usersPropName = this.getUsersPropName(subRoom);
|
|
1131
|
+
|
|
1132
|
+
if (signal && usersPropName) {
|
|
1133
|
+
const { classType } = signal.options;
|
|
1134
|
+
|
|
1135
|
+
// Create new user instance
|
|
1136
|
+
const user = isClass(classType) ? new classType() : classType();
|
|
1137
|
+
|
|
1138
|
+
// Load user data from snapshot
|
|
1139
|
+
load(user, userSnapshot, true);
|
|
1140
|
+
|
|
1141
|
+
// Add user to signal
|
|
1142
|
+
signal()[publicId] = user;
|
|
1143
|
+
|
|
1144
|
+
// Save user snapshot to storage
|
|
1145
|
+
await this.room.storage.put(`${usersPropName}.${publicId}`, userSnapshot);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Generate transfer token for the client to use when connecting
|
|
1150
|
+
const transferToken = generateShortUUID();
|
|
1151
|
+
await this.room.storage.put(`transfer:${transferToken}`, {
|
|
1152
|
+
privateId,
|
|
1153
|
+
publicId,
|
|
1154
|
+
restored: true
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
return res.success({ transferToken });
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
console.error('Error restoring session:', error);
|
|
1160
|
+
return res.serverError('Failed to restore session');
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
949
1164
|
/**
|
|
950
1165
|
* @method handleDirectRequest
|
|
951
1166
|
* @private
|
|
@@ -956,10 +1171,16 @@ export class Server implements Party.Server {
|
|
|
956
1171
|
*/
|
|
957
1172
|
private async handleDirectRequest(req: Party.Request, res: ServerResponse): Promise<Response> {
|
|
958
1173
|
const subRoom = await this.getSubRoom();
|
|
1174
|
+
|
|
959
1175
|
if (!subRoom) {
|
|
960
1176
|
return res.notFound();
|
|
961
1177
|
}
|
|
962
1178
|
|
|
1179
|
+
const url = new URL(req.url);
|
|
1180
|
+
if (url.pathname.endsWith('/session-transfer') && req.method === 'POST') {
|
|
1181
|
+
return this.handleSessionRestore(req, res);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
963
1184
|
// First try to match using the registered @Request handlers
|
|
964
1185
|
const response = await this.tryMatchRequestHandler(req, res, subRoom);
|
|
965
1186
|
if (response) {
|
|
@@ -1083,7 +1304,7 @@ export class Server implements Party.Server {
|
|
|
1083
1304
|
.replace(/\//g, '\\/') // Escape slashes
|
|
1084
1305
|
.replace(/:([^\/]+)/g, '([^/]+)'); // Convert :params to capture groups
|
|
1085
1306
|
|
|
1086
|
-
const pathRegex = new RegExp(`^${pathRegexString}
|
|
1307
|
+
const pathRegex = new RegExp(`^${pathRegexString}`);
|
|
1087
1308
|
return pathRegex.test(requestPath);
|
|
1088
1309
|
}
|
|
1089
1310
|
|
|
@@ -1111,7 +1332,7 @@ export class Server implements Party.Server {
|
|
|
1111
1332
|
.replace(/\//g, '\\/') // Escape slashes
|
|
1112
1333
|
.replace(/:([^\/]+)/g, '([^/]+)'); // Convert :params to capture groups
|
|
1113
1334
|
|
|
1114
|
-
const pathRegex = new RegExp(`^${pathRegexString}
|
|
1335
|
+
const pathRegex = new RegExp(`^${pathRegexString}`);
|
|
1115
1336
|
const matches = requestPath.match(pathRegex);
|
|
1116
1337
|
|
|
1117
1338
|
if (matches && matches.length > 1) {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type * as Party from "./types/party";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @description Factory function that creates a session guard with access to room storage
|
|
5
|
+
* @param {Party.Storage} storage - The room storage instance
|
|
6
|
+
* @returns {Function} - The guard function
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { createRequireSessionGuard } from "./session.guard";
|
|
11
|
+
*
|
|
12
|
+
* export class GameRoom {
|
|
13
|
+
* constructor(private room: Party.Room) {}
|
|
14
|
+
*
|
|
15
|
+
* @Action("sendMessage")
|
|
16
|
+
* @Guard([createRequireSessionGuard(this.room.storage)])
|
|
17
|
+
* async sendMessage(user: User, message: string, conn: Party.Connection) {
|
|
18
|
+
* // This action will only execute if the user has a valid session
|
|
19
|
+
* this.$broadcast({ type: "message", user, message });
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function createRequireSessionGuard(storage: Party.Storage) {
|
|
25
|
+
return async (sender: Party.Connection, value: any): Promise<boolean> => {
|
|
26
|
+
if (!sender || !sender.id) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Check if session exists in storage
|
|
32
|
+
const session = await storage.get(`session:${sender.id}`);
|
|
33
|
+
|
|
34
|
+
// Return false if no session found
|
|
35
|
+
if (!session) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Verify session has required properties
|
|
40
|
+
const typedSession = session as { publicId: string, created?: number, connected?: boolean };
|
|
41
|
+
if (!typedSession.publicId) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Session exists and is valid
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// If there's an error accessing storage, deny access
|
|
49
|
+
console.error('Error checking session in requireSession guard:', error);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @description Guard function that verifies if a user session exists (for room and request guards)
|
|
57
|
+
* @param {Party.Connection} sender - The connection object of the sender
|
|
58
|
+
* @param {any} value - The value/payload sent with the action or request
|
|
59
|
+
* @param {Party.Room} room - The room instance
|
|
60
|
+
* @returns {Promise<boolean>} - Returns true if session exists, false otherwise
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* import { requireSession } from "./session.guard";
|
|
65
|
+
*
|
|
66
|
+
* // For room guards
|
|
67
|
+
* @Room({
|
|
68
|
+
* path: "game-{id}",
|
|
69
|
+
* guards: [requireSession]
|
|
70
|
+
* })
|
|
71
|
+
* export class GameRoom {
|
|
72
|
+
* // Room implementation
|
|
73
|
+
* }
|
|
74
|
+
*
|
|
75
|
+
* // For request guards
|
|
76
|
+
* @Request({ path: '/api/data', method: 'GET' })
|
|
77
|
+
* @Guard([requireSession])
|
|
78
|
+
* async getData(req: Party.Request, res: ServerResponse) {
|
|
79
|
+
* // This request will only execute if the user has a valid session
|
|
80
|
+
* return res.success({ data: "protected data" });
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export const requireSession = async (sender: Party.Connection, value: any, room: Party.Room): Promise<boolean> => {
|
|
85
|
+
if (!sender || !sender.id) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Check if session exists in storage
|
|
91
|
+
const session = await room.storage.get(`session:${sender.id}`);
|
|
92
|
+
|
|
93
|
+
// Return false if no session found
|
|
94
|
+
if (!session) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Verify session has required properties
|
|
99
|
+
const typedSession = session as { publicId: string, created?: number, connected?: boolean };
|
|
100
|
+
if (!typedSession.publicId) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Session exists and is valid
|
|
105
|
+
return true;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// If there's an error accessing storage, deny access
|
|
108
|
+
console.error('Error checking session in requireSession guard:', error);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
};
|