@raclettejs/core 0.1.19 → 0.1.21

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.
Files changed (134) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/CONTRIBUTING.md +23 -0
  3. package/bin/cli.js +1 -1
  4. package/dev/README.md +4 -0
  5. package/dist/cli.js +135 -124
  6. package/dist/cli.js.map +4 -4
  7. package/dist/index.js +9 -8
  8. package/dist/index.js.map +4 -4
  9. package/package.json +12 -6
  10. package/raclette.default.config.yaml +60 -0
  11. package/services/backend/delay-api-debugger.sh +0 -0
  12. package/services/backend/package.json +1 -0
  13. package/services/backend/src/app.ts +4 -0
  14. package/services/backend/src/core/config/configService.ts +10 -1
  15. package/services/backend/src/core/events/eventEmitter.ts +32 -7
  16. package/services/backend/src/core/http/request-params.plugin.ts +2 -2
  17. package/services/backend/src/core/pluginSystem/pluginFastifyInstance.ts +4 -1
  18. package/services/backend/src/core/pluginSystem/pluginTypes.ts +3 -1
  19. package/services/backend/src/core/sockets/createSystemEventHandlers.ts +7 -0
  20. package/services/backend/src/core/sockets/index.ts +61 -13
  21. package/services/backend/src/core/sockets/socketManager.ts +452 -105
  22. package/services/backend/src/core/sockets/socketTypes.ts +13 -1
  23. package/services/backend/src/core/sockets/workSessionJoinPayload.ts +106 -0
  24. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.get.ts +3 -1
  25. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.getAll.ts +3 -1
  26. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.hardDelete.ts +16 -19
  27. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.patch.ts +3 -1
  28. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.post.ts +3 -1
  29. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.remove.ts +3 -1
  30. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.service.ts +2 -3
  31. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.get.ts +3 -1
  32. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.getAll.ts +3 -1
  33. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.hardDelete.ts +16 -19
  34. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.patch.ts +3 -1
  35. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.bulk.ts +3 -1
  36. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.ts +3 -1
  37. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.remove.ts +3 -1
  38. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.restore.ts +12 -15
  39. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/interactionLink.service.ts +2 -3
  40. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.get.ts +3 -1
  41. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.getAll.ts +4 -2
  42. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.hardDelete.ts +12 -17
  43. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.patch.ts +3 -1
  44. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.bulk.ts +3 -1
  45. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.ts +3 -1
  46. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.remove.ts +3 -1
  47. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.restore.ts +12 -15
  48. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.service.ts +1 -0
  49. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.get.ts +3 -1
  50. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.getAll.ts +4 -2
  51. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.hardDelete.ts +3 -1
  52. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.patch.ts +3 -1
  53. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.post.ts +4 -2
  54. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.remove.ts +3 -1
  55. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.get.ts +1 -0
  56. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.getAll.ts +1 -0
  57. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.hardDelete.ts +10 -15
  58. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.patch.ts +3 -1
  59. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.bulk.ts +3 -1
  60. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.ts +3 -1
  61. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.remove.ts +3 -1
  62. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.restore.ts +12 -15
  63. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/tag.service.ts +1 -2
  64. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.get.ts +1 -0
  65. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.getAll.ts +1 -0
  66. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.hardDelete.ts +10 -15
  67. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patch.ts +4 -6
  68. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patchLastProject.ts +4 -2
  69. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.post.ts +3 -1
  70. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.remove.ts +3 -4
  71. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.restore.ts +12 -15
  72. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/events/index.ts +45 -0
  73. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/helpers/crud.ts +12 -0
  74. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/helpers/payload.ts +21 -0
  75. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/index.ts +55 -0
  76. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/index.ts +19 -0
  77. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.get.ts +53 -0
  78. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.getAll.ts +39 -0
  79. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.hardDelete.ts +53 -0
  80. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.patch.ts +58 -0
  81. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.post.ts +53 -0
  82. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.remove.ts +52 -0
  83. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.model.ts +65 -0
  84. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.schema.ts +125 -0
  85. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.service.ts +351 -0
  86. package/services/backend/src/corePlugins/raclette__core/backend/index.ts +2 -0
  87. package/services/backend/src/corePlugins/raclette__core/frontend/generated-config.ts +159 -24
  88. package/services/backend/src/modules/authentication/index.ts +255 -4
  89. package/services/backend/src/modules/hooks/rate-limiter.ts +21 -0
  90. package/services/backend/src/types/custom-fastify.d.ts +17 -0
  91. package/services/backend/src/utils/request.utils.ts +4 -1
  92. package/services/backend/types/index.d.ts +15 -1
  93. package/services/backend/yarn.lock +13 -4
  94. package/services/frontend/index.html +1 -1
  95. package/services/frontend/provision/entrypoint.sh +0 -0
  96. package/services/frontend/src/core/lib/configService.ts +1 -1
  97. package/services/frontend/src/core/lib/dataHelper.ts +1 -1
  98. package/services/frontend/src/core/lib/eggs/snow.ts +49 -20
  99. package/services/frontend/src/core/lib/helpers/index.ts +1 -0
  100. package/services/frontend/src/core/lib/helpers/removeEmpty.ts +69 -0
  101. package/services/frontend/src/core/lib/httpClient.ts +19 -0
  102. package/services/frontend/src/core/main.ts +20 -10
  103. package/services/frontend/src/core/setup/socket.ts +4 -3
  104. package/services/frontend/src/core/setup/socketClient.ts +31 -3
  105. package/services/frontend/src/core/setup/socketEvents.ts +92 -8
  106. package/services/frontend/src/core/store/index.ts +5 -1
  107. package/services/frontend/src/core/store/reducers/compositions/index.ts +2 -2
  108. package/services/frontend/src/core/store/reducers/interactionLinks/index.ts +1 -1
  109. package/services/frontend/src/core/store/reducers/widgets/reducers.ts +1 -2
  110. package/services/frontend/src/core/store/types/index.ts +0 -1
  111. package/services/frontend/src/core/types/PluginRegistrar.ts +8 -6
  112. package/services/frontend/src/core/types/Store.ts +1 -0
  113. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +68 -8
  114. package/services/frontend/src/orchestrator/components/menu/UserMenu.vue +17 -5
  115. package/services/frontend/src/orchestrator/components/welcomeScreen/AdminWelcomeScreen.vue +1 -1
  116. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  117. package/services/frontend/src/orchestrator/composables/usePluginApi.ts +0 -3
  118. package/services/frontend/src/orchestrator/composables/useRouteState.ts +23 -4
  119. package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +166 -0
  120. package/services/frontend/src/orchestrator/helpers/index.ts +1 -0
  121. package/services/frontend/src/orchestrator/i18n/de-DE.json +1 -1
  122. package/services/frontend/src/orchestrator/i18n/en-EU.json +1 -1
  123. package/services/frontend/src/orchestrator/i18n/sk.json +1 -1
  124. package/services/frontend/src/orchestrator/index.ts +34 -4
  125. package/services/frontend/src/orchestrator/router/routeParserHelper.ts +106 -1
  126. package/services/frontend/src/orchestrator/types/Widgets.ts +2 -0
  127. package/services/frontend/types.ts +1 -0
  128. package/src/README.md +404 -0
  129. package/src/types.ts +19 -5
  130. package/templates/README.md +0 -0
  131. package/types/index.ts +18 -4
  132. package/services/backend/.gitignore +0 -17
  133. package/services/frontend/.gitignore +0 -35
  134. package/yarn.lock +0 -2937
@@ -1,8 +1,10 @@
1
1
  import type { FastifyInstance } from "fastify"
2
2
  import type { Socket } from "socket.io"
3
+ import { validate } from "uuid"
3
4
  import { createSystemEventHandlers } from "./createSystemEventHandlers"
4
5
  import configService from "@c/config/configService"
5
6
  import { callDataTypeOperation } from "@c/contracting/contractRegistrar"
7
+ import { buildWorkSessionJoinPayload } from "./workSessionJoinPayload"
6
8
 
7
9
  interface JoinPayload {
8
10
  roomId?: string
@@ -10,132 +12,441 @@ interface JoinPayload {
10
12
  tenantId?: string
11
13
  }
12
14
 
15
+ interface ProjectResponse {
16
+ _id: string
17
+ [key: string]: any
18
+ }
19
+
20
+ /**
21
+ * Check if socket is authenticated
22
+ */
23
+ const isSocketAuthenticated = (
24
+ socket: Socket,
25
+ requiresAuth: boolean,
26
+ ): boolean => {
27
+ if (!requiresAuth) return true
28
+ return socket.decoded_token !== undefined || socket.isWorkSession === true
29
+ }
30
+
31
+ /**
32
+ * Load project by ID or get main project
33
+ */
34
+ const loadProject = async (
35
+ fastify: FastifyInstance,
36
+ projectId?: string,
37
+ ): Promise<ProjectResponse | null> => {
38
+ try {
39
+ if (projectId) {
40
+ return await callDataTypeOperation(
41
+ "raclette__core",
42
+ "project",
43
+ "read",
44
+ fastify,
45
+ projectId,
46
+ )
47
+ }
48
+ return await callDataTypeOperation(
49
+ "raclette__core",
50
+ "project",
51
+ "readMain",
52
+ fastify,
53
+ )
54
+ } catch (error) {
55
+ fastify.log.warn(
56
+ `Failed to load project: ${error instanceof Error ? error.message : String(error)}`,
57
+ )
58
+ return null
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Load composition for work session
64
+ */
65
+ const loadWorkSessionComposition = async (
66
+ fastify: FastifyInstance,
67
+ workSession: any,
68
+ ): Promise<any | null> => {
69
+ // Use composition from workSession if available
70
+ if (workSession.composition) {
71
+ fastify.log.info(
72
+ `[Socket] Using shared composition from workSession ${workSession._id}`,
73
+ )
74
+ return workSession.composition
75
+ }
76
+
77
+ // Fallback: Load via compositionId
78
+ try {
79
+ const composition = await callDataTypeOperation(
80
+ "raclette__core",
81
+ "composition",
82
+ "read",
83
+ fastify,
84
+ workSession.compositionId,
85
+ )
86
+
87
+ if (!configService.isAutoSendEnabled("compositions") || !composition) {
88
+ return null
89
+ }
90
+
91
+ return composition
92
+ } catch (error) {
93
+ fastify.log.warn(
94
+ `Error loading composition for workSession: ${error instanceof Error ? error.message : String(error)}`,
95
+ )
96
+ return null
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Load user's active work sessions and join their rooms
102
+ */
103
+ const joinOwnedWorkSessions = async (
104
+ fastify: FastifyInstance,
105
+ socket: Socket,
106
+ userId: string,
107
+ ): Promise<void> => {
108
+ const now = new Date()
109
+
110
+ try {
111
+ const ownedSessions = await callDataTypeOperation(
112
+ "raclette__core",
113
+ "workSession",
114
+ "readAll",
115
+ fastify,
116
+ {
117
+ userId,
118
+ isActive: true,
119
+ },
120
+ )
121
+
122
+ const validSessions = Array.isArray(ownedSessions)
123
+ ? ownedSessions.filter((s: any) => {
124
+ if (!s.expiryDate) return false
125
+ return new Date(s.expiryDate) > now
126
+ })
127
+ : []
128
+
129
+ const ownedSessionIds = validSessions.map((s: any) => s._id)
130
+ socket.data.ownedSessions = ownedSessionIds
131
+
132
+ ownedSessionIds.forEach((sessionId: string) => {
133
+ socket.join(`session_${sessionId}`)
134
+ })
135
+
136
+ if (ownedSessionIds.length > 0) {
137
+ fastify.log.info(
138
+ `[Socket] User ${socket.id} joined ${ownedSessionIds.length} owned session rooms`,
139
+ )
140
+ }
141
+ } catch (error) {
142
+ fastify.log.warn(
143
+ `Error loading owned workSessions: ${error instanceof Error ? error.message : String(error)}`,
144
+ )
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Build response data for authenticated user join
150
+ */
151
+ const buildAuthenticatedUserResponse = async (
152
+ fastify: FastifyInstance,
153
+ project: ProjectResponse,
154
+ ): Promise<any> => {
155
+ const responseData: any = { project }
156
+
157
+ if (configService.isAutoSendEnabled("compositions")) {
158
+ responseData.compositions = await callDataTypeOperation(
159
+ "raclette__core",
160
+ "composition",
161
+ "readAll",
162
+ fastify,
163
+ { isDeleted: false },
164
+ )
165
+ }
166
+
167
+ if (configService.isAutoSendEnabled("interactionLinks")) {
168
+ responseData.interactionLinks = await callDataTypeOperation(
169
+ "raclette__core",
170
+ "interactionLink",
171
+ "readAll",
172
+ fastify,
173
+ { isDeleted: false },
174
+ )
175
+ }
176
+
177
+ if (configService.isAutoSendEnabled("projectConfig")) {
178
+ responseData.projectConfig = {}
179
+ }
180
+
181
+ const customData = configService.getCustomSocketData()
182
+ if (customData && typeof customData === "object") {
183
+ Object.entries(customData).forEach(([key, value]) => {
184
+ responseData[key] = value
185
+ })
186
+ }
187
+
188
+ return responseData
189
+ }
190
+
191
+ /**
192
+ * Validate work session (UUID, exists, active, not expired, interactionLink exists).
193
+ * Shared by socket auth middleware and handleWorkSessionJoin.
194
+ */
195
+ export const validateWorkSession = async (
196
+ fastify: FastifyInstance,
197
+ sessionId: string,
198
+ ): Promise<any> => {
199
+ fastify.log.debug(
200
+ `[WorkSession Validation] Starting validation for sessionId: ${sessionId}`,
201
+ )
202
+
203
+ // Validate UUID format
204
+ if (!validate(sessionId)) {
205
+ fastify.log.warn(
206
+ `[WorkSession Validation] Invalid session ID format: ${sessionId}`,
207
+ )
208
+ throw new Error("Invalid session ID format")
209
+ }
210
+
211
+ // Load work session
212
+ const workSession = await callDataTypeOperation(
213
+ "raclette__core",
214
+ "workSession",
215
+ "read",
216
+ fastify,
217
+ sessionId,
218
+ )
219
+
220
+ if (!workSession) {
221
+ throw new Error("Work session not found")
222
+ }
223
+
224
+ // Validate work session is active
225
+ if (!workSession.isActive) {
226
+ throw new Error("Work session is not active")
227
+ }
228
+
229
+ // Validate work session is not expired
230
+ const now = new Date()
231
+ const expiryDate = workSession.expiryDate
232
+ ? new Date(workSession.expiryDate)
233
+ : null
234
+
235
+ if (expiryDate && expiryDate <= now) {
236
+ throw new Error("Work session has expired")
237
+ }
238
+
239
+ // Validate composition exists
240
+ const composition = await callDataTypeOperation(
241
+ "raclette__core",
242
+ "composition",
243
+ "read",
244
+ fastify,
245
+ workSession.compositionId,
246
+ )
247
+
248
+ if (!composition) {
249
+ throw new Error("Composition not found for work session")
250
+ }
251
+
252
+ fastify.log.debug(
253
+ `[WorkSession Validation] Validation successful for sessionId: ${sessionId}`,
254
+ )
255
+ return workSession
256
+ }
257
+
258
+ /**
259
+ * Handle work session join
260
+ */
261
+ const handleWorkSessionJoin = async (
262
+ fastify: FastifyInstance,
263
+ socket: Socket,
264
+ ): Promise<void> => {
265
+ const workSession = socket.workSession
266
+ if (!workSession) {
267
+ socket.emit("error", {
268
+ error: { msg: "Work session not found" },
269
+ })
270
+ return
271
+ }
272
+
273
+ const sessionId = workSession._id
274
+ fastify.log.info(
275
+ `[WorkSession Join] Handling join for socket: ${socket.id}, sessionId: ${sessionId}`,
276
+ )
277
+
278
+ // Re-validate work session (similar to login validating credentials)
279
+ try {
280
+ fastify.log.debug(
281
+ `[WorkSession Join] Re-validating work session before join`,
282
+ )
283
+ const validatedSession = await validateWorkSession(fastify, sessionId)
284
+ // Use validated session (in case it was updated)
285
+ Object.assign(workSession, validatedSession)
286
+ fastify.log.debug(`[WorkSession Join] Work session validated successfully`)
287
+ } catch (error: any) {
288
+ fastify.log.error(
289
+ `[WorkSession Join] Work session validation failed: ${error.message}, stack: ${error.stack}`,
290
+ )
291
+ socket.emit("error", {
292
+ error: { msg: error.message || "Work session validation failed" },
293
+ })
294
+ return
295
+ }
296
+
297
+ fastify.log.debug(
298
+ `[WorkSession Join] Joining socket to room: session_${sessionId}`,
299
+ )
300
+ socket.join(`session_${sessionId}`)
301
+ socket.data.session = sessionId
302
+
303
+ fastify.log.debug(`[WorkSession Join] Loading composition for work session`)
304
+ const composition = await loadWorkSessionComposition(fastify, workSession)
305
+ if (!composition) {
306
+ fastify.log.warn(
307
+ `[WorkSession Join] No composition available for session: ${sessionId}`,
308
+ )
309
+ socket.emit("error", {
310
+ error: { msg: "No composition available for this session" },
311
+ })
312
+ return
313
+ }
314
+ fastify.log.debug(`[WorkSession Join] Composition loaded`)
315
+
316
+ const joinPayloadData = buildWorkSessionJoinPayload(workSession, composition)
317
+
318
+ // Create JWT token for work session (analogous to login token creation)
319
+ const now = new Date()
320
+ fastify.log.info(
321
+ `[WorkSession Join] Creating JWT token for work session: ${sessionId}`,
322
+ )
323
+ const tokenPayload = {
324
+ type: "workSession",
325
+ sessionId: sessionId,
326
+ userId: workSession.userId,
327
+ permissions: workSession.permissions || 4, // Default to read-only (4)
328
+ iat: Math.floor(Date.now() / 1000),
329
+ }
330
+
331
+ fastify.log.debug(
332
+ `[WorkSession Join] Token payload: ${JSON.stringify(tokenPayload)}`,
333
+ )
334
+
335
+ // Token expires when work session expires (similar to login using config expiry)
336
+ const expiryDate = workSession.expiryDate
337
+ ? new Date(workSession.expiryDate)
338
+ : null
339
+ const expiresInSeconds = expiryDate
340
+ ? Math.max(0, Math.floor((expiryDate.getTime() - now.getTime()) / 1000))
341
+ : 7 * 24 * 60 * 60 // Default to 7 days if no expiry
342
+
343
+ fastify.log.debug(
344
+ `[WorkSession Join] Token expiry: ${expiresInSeconds}s (${expiresInSeconds / 3600}h), expiryDate: ${expiryDate?.toISOString() || "none"}`,
345
+ )
346
+
347
+ const token = fastify.jwt.sign(tokenPayload, {
348
+ expiresIn: expiresInSeconds > 0 ? `${expiresInSeconds}s` : "7d", // Fallback to 7d if invalid
349
+ })
350
+ const decoded = fastify.jwt.decode(token)
351
+
352
+ fastify.log.info(
353
+ `[WorkSession Join] JWT token created successfully for sessionId: ${sessionId}, token length: ${token.length}, expiresIn: ${expiresInSeconds}s`,
354
+ )
355
+ fastify.log.debug(
356
+ `[WorkSession Join] Decoded token: ${JSON.stringify(decoded)}`,
357
+ )
358
+
359
+ const responseData = {
360
+ ...joinPayloadData,
361
+ token,
362
+ decoded,
363
+ }
364
+
365
+ fastify.log.info(
366
+ `[WorkSession Join] Emitting joinConfirmation to socket: ${socket.id}`,
367
+ )
368
+ socket.emit("joinConfirmation", responseData)
369
+ fastify.log.info(
370
+ `[WorkSession Join] Work session join complete: socket=${socket.id}, sessionId=${sessionId}, room=session_${sessionId}, tokenCreated=${!!token}`,
371
+ )
372
+ }
373
+
374
+ /**
375
+ * Handle authenticated user join
376
+ */
377
+ const handleAuthenticatedUserJoin = async (
378
+ fastify: FastifyInstance,
379
+ socket: Socket,
380
+ payload: JoinPayload,
381
+ ): Promise<void> => {
382
+ const { roomId: spaceId } = payload
383
+ const project = await loadProject(fastify, spaceId)
384
+
385
+ if (!project) {
386
+ socket.emit("error", {
387
+ error: {
388
+ msg: spaceId
389
+ ? "Space not found or access denied"
390
+ : "No suiting space found. Something seems wrongly configured!",
391
+ },
392
+ })
393
+ return
394
+ }
395
+
396
+ socket.join(project._id)
397
+ socket.data = socket.data || {}
398
+ socket.data.project = project._id
399
+
400
+ // Join user's owned work sessions
401
+ const decodedToken = socket.decoded_token as any
402
+ const userId = decodedToken?._id
403
+ if (userId) {
404
+ await joinOwnedWorkSessions(fastify, socket, userId)
405
+ }
406
+
407
+ const responseData = await buildAuthenticatedUserResponse(fastify, project)
408
+ socket.emit("joinConfirmation", responseData)
409
+ fastify.log.info(`[Socket] Socket ${socket.id} joined room ${project._id}`)
410
+ }
411
+
412
+ /**
413
+ * Register socket event handlers
414
+ */
13
415
  const registerSocketEvents = (fastify: FastifyInstance): void => {
14
416
  const { wss } = fastify
15
417
  const eventbusHandler = createSystemEventHandlers(fastify)
16
-
17
418
  const requiresAuth = configService.isAuthRequired()
18
419
 
19
420
  wss.on("connection", (socket: Socket) => {
20
- const isAuthenticated = requiresAuth
21
- ? socket.decoded_token !== undefined
22
- : true
421
+ const isAuthenticated = isSocketAuthenticated(socket, requiresAuth)
23
422
 
24
- fastify.log.warn(`[socket] connected ${socket.id}`)
423
+ fastify.log.info(`[Socket] Connected ${socket.id}`)
25
424
  socket.emit("socketEstablished", { id: socket.id })
26
425
 
27
- if (!isAuthenticated && configService.isAuthRequired()) {
28
- fastify.log.warn(`[socket] unauthenticated connection ${socket.id}`)
426
+ if (!isAuthenticated && requiresAuth) {
427
+ fastify.log.warn(`[Socket] Unauthenticated connection ${socket.id}`)
29
428
  socket.emit("authenticationRequired", {
30
429
  message: "Authentication required",
31
430
  })
32
- // Don't disconnect immediately to allow frontend to handle login flow
33
431
  }
34
432
 
35
433
  socket.on("join", async (payload: JoinPayload) => {
36
- // Check authentication if required
37
- if (configService.isAuthRequired() && !isAuthenticated) {
434
+ if (requiresAuth && !isAuthenticated) {
38
435
  socket.emit("authenticationFailed", {
39
436
  message: "Authentication required to join room",
40
437
  })
41
-
42
438
  return
43
439
  }
44
440
 
45
- const { token: _token, roomId: spaceId } = payload
46
-
47
- // Find project either by roomId or default
48
- let project
49
-
50
- if (spaceId) {
51
- try {
52
- project = await callDataTypeOperation(
53
- "raclette__core",
54
- "project",
55
- "read",
56
- fastify,
57
- spaceId,
58
- )
59
- } catch (error) {
60
- socket.emit("error", {
61
- error: { msg: "Space not found or access denied" },
62
- })
63
- return
64
- }
441
+ if (socket.isWorkSession === true && socket.workSession) {
442
+ await handleWorkSessionJoin(fastify, socket)
65
443
  } else {
66
- try {
67
- project = await callDataTypeOperation(
68
- "raclette__core",
69
- "project",
70
- "readMain",
71
- fastify,
72
- )
73
- } catch (error) {
74
- socket.emit("error", {
75
- error: {
76
- msg: "No suiting space found. Something seems wrongly configured!",
77
- },
78
- })
79
- return
80
- }
81
- }
82
-
83
- socket.join(project._id)
84
- socket.data.project = project._id
85
-
86
- // Prepare response data with project always included
87
- const responseData: any = { project }
88
-
89
- // Add compositions based on config
90
- if (configService.isAutoSendEnabled("compositions")) {
91
- responseData.compositions = await callDataTypeOperation(
92
- "raclette__core",
93
- "composition",
94
- "readAll",
95
- fastify,
96
- { isDeleted: false },
97
- )
98
- }
99
-
100
- // Add interaction links based on config
101
- if (configService.isAutoSendEnabled("interactionLinks")) {
102
- responseData.interactionLinks = await callDataTypeOperation(
103
- "raclette__core",
104
- "interactionLink",
105
- "readAll",
106
- fastify,
107
- { isDeleted: false },
108
- )
109
- }
110
-
111
- // Add project config based on config
112
- if (configService.isAutoSendEnabled("projectConfig")) {
113
- try {
114
- responseData.projectConfig = {}
115
- } catch (error) {
116
- fastify.log.error(
117
- `Error loading project config: ${error instanceof Error ? error.message : String(error)}`,
118
- )
119
- }
444
+ await handleAuthenticatedUserJoin(fastify, socket, payload)
120
445
  }
121
-
122
- // Add any custom static data from the config
123
- const customData = configService.getCustomSocketData()
124
-
125
- if (customData && typeof customData === "object") {
126
- Object.entries(customData).forEach(([key, value]) => {
127
- responseData[key] = value
128
- })
129
- }
130
-
131
- socket.emit("joinConfirmation", responseData)
132
- fastify.log.info(
133
- `[Socket] socket ${socket.id} joined room ${project._id}`,
134
- )
135
446
  })
136
447
 
137
448
  socket.on("disconnect", () => {
138
- fastify.log.warn(`[Socket] disconnected ${socket.id}`)
449
+ fastify.log.info(`[Socket] Disconnected ${socket.id}`)
139
450
  })
140
451
 
141
452
  socket.on("leave", (room: string) => {
@@ -149,11 +460,11 @@ const registerSocketEvents = (fastify: FastifyInstance): void => {
149
460
  })
150
461
 
151
462
  wss.on("connection_error", (error: Error) => {
152
- fastify.log.error({ msg: "[Socket]", error })
463
+ fastify.log.error({ msg: "[Socket] Connection error", error })
153
464
  })
154
465
 
155
- // Register event handlers
156
- const events = {
466
+ // Register system event handlers
467
+ const systemEvents = {
157
468
  broadcastDataUpdated: eventbusHandler.broadcastDataUpdatedHandler,
158
469
  broadcastDataCreated: eventbusHandler.broadcastDataCreatedHandler,
159
470
  broadcastDataDeleted: eventbusHandler.broadcastDataDeletedHandler,
@@ -174,10 +485,46 @@ const registerSocketEvents = (fastify: FastifyInstance): void => {
174
485
  broadcastNewVersionAvailable: eventbusHandler.broadcastNewVersionAvailable,
175
486
  }
176
487
 
177
- // Register standard event handlers
178
- Object.entries(events).forEach(([event, handler]) => {
488
+ Object.entries(systemEvents).forEach(([event, handler]) => {
179
489
  fastify.eventbus.on(event, handler)
180
490
  })
491
+
492
+ // Set up socket broadcasting for plugin events via EventEmitter hook
493
+ setupPluginEventSocketBroadcasting(fastify)
494
+ }
495
+
496
+ /**
497
+ * Set up socket broadcasting for plugin-prefixed events
498
+ * Registers a broadcast hook in the EventEmitter that automatically
499
+ * broadcasts plugin events (containing "--") to the project room
500
+ */
501
+ const setupPluginEventSocketBroadcasting = (fastify: FastifyInstance): void => {
502
+ fastify.eventbus.setBroadcastHook((eventName: string, data: unknown) => {
503
+ // Broadcast to project room (fire-and-forget)
504
+ callDataTypeOperation("raclette__core", "project", "readMain", fastify)
505
+ .then((mainProject) => {
506
+ if (mainProject?._id) {
507
+ if (data?.workSessionId) {
508
+ fastify.wss.in(data.workSessionId).emit(eventName, data)
509
+ fastify.log.debug(
510
+ `[Socket] Broadcasted plugin event ${eventName} to workSessionId room ${data.workSessionId}`,
511
+ )
512
+ } else {
513
+ fastify.wss.in(mainProject._id).emit(eventName, data)
514
+ fastify.log.debug(
515
+ `[Socket] Broadcasted plugin event ${eventName} to project room ${mainProject._id}`,
516
+ )
517
+ }
518
+ } else {
519
+ throw new Error("no project for broadcast")
520
+ }
521
+ })
522
+ .catch((error: unknown) => {
523
+ const errorMessage =
524
+ error instanceof Error ? error.message : String(error)
525
+ fastify.log.warn(`Failed to send broadcast. ${errorMessage}`)
526
+ })
527
+ })
181
528
  }
182
529
 
183
530
  export default registerSocketEvents
@@ -8,12 +8,24 @@ declare module "socket.io" {
8
8
  ServerSideEvents extends EventsMap = DefaultEventsMap,
9
9
  SocketData = any,
10
10
  > {
11
- decoded_token?: VerifyPayloadType
11
+ decoded_token?: VerifyPayloadType & { _id?: string }
12
12
  user?: {
13
13
  _id: string
14
14
  account: any
15
15
  [key: string]: any
16
16
  }
17
+ workSession?: {
18
+ _id: string
19
+ userId: string
20
+ accessType: string
21
+ compositionId: string
22
+ expiryDate: Date
23
+ isActive: boolean
24
+ permissions: number
25
+ pluginData: Record<string, any>
26
+ [key: string]: any
27
+ }
28
+ isWorkSession?: boolean
17
29
  }
18
30
  }
19
31