@raclettejs/core 0.1.19 → 0.1.20
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/CHANGELOG.md +17 -0
- package/bin/cli.js +1 -1
- package/dev/README.md +4 -0
- package/dist/cli.js +135 -124
- package/dist/cli.js.map +4 -4
- package/dist/index.js +9 -8
- package/dist/index.js.map +4 -4
- package/package.json +9 -5
- package/services/backend/delay-api-debugger.sh +0 -0
- package/services/backend/package.json +1 -0
- package/services/backend/src/app.ts +4 -0
- package/services/backend/src/core/config/configService.ts +10 -1
- package/services/backend/src/core/events/eventEmitter.ts +32 -7
- package/services/backend/src/core/http/request-params.plugin.ts +2 -2
- package/services/backend/src/core/pluginSystem/pluginFastifyInstance.ts +4 -1
- package/services/backend/src/core/pluginSystem/pluginTypes.ts +3 -1
- package/services/backend/src/core/sockets/createSystemEventHandlers.ts +7 -0
- package/services/backend/src/core/sockets/index.ts +61 -13
- package/services/backend/src/core/sockets/socketManager.ts +452 -105
- package/services/backend/src/core/sockets/socketTypes.ts +13 -1
- package/services/backend/src/core/sockets/workSessionJoinPayload.ts +106 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.get.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.getAll.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.hardDelete.ts +16 -19
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.patch.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.post.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.remove.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.service.ts +2 -3
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.get.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.getAll.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.hardDelete.ts +16 -19
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.patch.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.bulk.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.remove.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.restore.ts +12 -15
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/interactionLink.service.ts +2 -3
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.get.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.getAll.ts +4 -2
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.hardDelete.ts +12 -17
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.patch.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.bulk.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.remove.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.restore.ts +12 -15
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.service.ts +1 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.get.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.getAll.ts +4 -2
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.hardDelete.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.patch.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.post.ts +4 -2
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.remove.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.get.ts +1 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.getAll.ts +1 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.hardDelete.ts +10 -15
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.patch.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.bulk.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.remove.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.restore.ts +12 -15
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/tag.service.ts +1 -2
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.get.ts +1 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.getAll.ts +1 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.hardDelete.ts +10 -15
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patch.ts +4 -6
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patchLastProject.ts +4 -2
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.post.ts +3 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.remove.ts +3 -4
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.restore.ts +12 -15
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/events/index.ts +45 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/helpers/crud.ts +12 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/helpers/payload.ts +21 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/index.ts +55 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/index.ts +19 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.get.ts +53 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.getAll.ts +39 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.hardDelete.ts +53 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.patch.ts +58 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.post.ts +53 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.remove.ts +52 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.model.ts +65 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.schema.ts +125 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.service.ts +351 -0
- package/services/backend/src/corePlugins/raclette__core/backend/index.ts +2 -0
- package/services/backend/src/corePlugins/raclette__core/frontend/generated-config.ts +159 -24
- package/services/backend/src/modules/authentication/index.ts +255 -4
- package/services/backend/src/modules/hooks/rate-limiter.ts +21 -0
- package/services/backend/src/types/custom-fastify.d.ts +17 -0
- package/services/backend/src/utils/request.utils.ts +4 -1
- package/services/backend/types/index.d.ts +15 -1
- package/services/backend/yarn.lock +13 -4
- package/services/frontend/index.html +1 -1
- package/services/frontend/provision/entrypoint.sh +0 -0
- package/services/frontend/src/core/lib/configService.ts +1 -1
- package/services/frontend/src/core/lib/dataHelper.ts +1 -1
- package/services/frontend/src/core/lib/eggs/snow.ts +49 -20
- package/services/frontend/src/core/lib/helpers/index.ts +1 -0
- package/services/frontend/src/core/lib/helpers/removeEmpty.ts +69 -0
- package/services/frontend/src/core/lib/httpClient.ts +19 -0
- package/services/frontend/src/core/main.ts +20 -10
- package/services/frontend/src/core/setup/socket.ts +4 -3
- package/services/frontend/src/core/setup/socketClient.ts +31 -3
- package/services/frontend/src/core/setup/socketEvents.ts +92 -8
- package/services/frontend/src/core/store/index.ts +5 -1
- package/services/frontend/src/core/store/reducers/compositions/index.ts +2 -2
- package/services/frontend/src/core/store/reducers/interactionLinks/index.ts +1 -1
- package/services/frontend/src/core/store/reducers/widgets/reducers.ts +1 -2
- package/services/frontend/src/core/store/types/index.ts +0 -1
- package/services/frontend/src/core/types/PluginRegistrar.ts +8 -6
- package/services/frontend/src/core/types/Store.ts +1 -0
- package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +68 -8
- package/services/frontend/src/orchestrator/components/menu/UserMenu.vue +17 -5
- package/services/frontend/src/orchestrator/components/welcomeScreen/AdminWelcomeScreen.vue +1 -1
- package/services/frontend/src/orchestrator/composables/index.ts +1 -0
- package/services/frontend/src/orchestrator/composables/usePluginApi.ts +0 -3
- package/services/frontend/src/orchestrator/composables/useRouteState.ts +23 -4
- package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +166 -0
- package/services/frontend/src/orchestrator/helpers/index.ts +1 -0
- package/services/frontend/src/orchestrator/i18n/de-DE.json +1 -1
- package/services/frontend/src/orchestrator/i18n/en-EU.json +1 -1
- package/services/frontend/src/orchestrator/i18n/sk.json +1 -1
- package/services/frontend/src/orchestrator/index.ts +34 -4
- package/services/frontend/src/orchestrator/router/routeParserHelper.ts +106 -1
- package/services/frontend/src/orchestrator/types/Widgets.ts +2 -0
- package/services/frontend/types.ts +1 -0
- package/src/README.md +404 -0
- package/src/types.ts +19 -5
- package/templates/README.md +0 -0
- package/types/index.ts +18 -4
- package/services/backend/.gitignore +0 -17
- package/services/frontend/.gitignore +0 -35
- 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.
|
|
423
|
+
fastify.log.info(`[Socket] Connected ${socket.id}`)
|
|
25
424
|
socket.emit("socketEstablished", { id: socket.id })
|
|
26
425
|
|
|
27
|
-
if (!isAuthenticated &&
|
|
28
|
-
fastify.log.warn(`[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|