@citolab/qti-backend-firebase 0.0.3 → 0.0.5

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 (78) hide show
  1. package/dist/api/app-specific/base/application-specific-base.d.ts +11 -20
  2. package/dist/api/app-specific/base/application-specific-base.d.ts.map +1 -1
  3. package/dist/api/app-specific/base/application-specific-base.js +73 -88
  4. package/dist/api/app-specific/base/application-specific-base.js.map +1 -1
  5. package/dist/api/app-specific/baseImplementation.d.ts +2 -3
  6. package/dist/api/app-specific/baseImplementation.d.ts.map +1 -1
  7. package/dist/api/app-specific/baseImplementation.js +2 -6
  8. package/dist/api/app-specific/baseImplementation.js.map +1 -1
  9. package/dist/api/app-specific/index.d.ts +1 -0
  10. package/dist/api/app-specific/index.d.ts.map +1 -1
  11. package/dist/api/app-specific/index.js +1 -0
  12. package/dist/api/app-specific/index.js.map +1 -1
  13. package/dist/api/app-specific/interface/IApplicationSpecific.d.ts +10 -20
  14. package/dist/api/app-specific/interface/IApplicationSpecific.d.ts.map +1 -1
  15. package/dist/api/qti-data.d.ts.map +1 -1
  16. package/dist/api/qti-data.js +619 -271
  17. package/dist/api/qti-data.js.map +1 -1
  18. package/dist/api/qti-resources.d.ts.map +1 -1
  19. package/dist/api/qti-resources.js +134 -131
  20. package/dist/api/qti-resources.js.map +1 -1
  21. package/dist/api/qti-teacher.d.ts.map +1 -1
  22. package/dist/api/qti-teacher.js +563 -424
  23. package/dist/api/qti-teacher.js.map +1 -1
  24. package/dist/api/qti-tools.d.ts.map +1 -1
  25. package/dist/api/qti-tools.js +213 -41
  26. package/dist/api/qti-tools.js.map +1 -1
  27. package/dist/helpers/database.d.ts +35 -24
  28. package/dist/helpers/database.d.ts.map +1 -1
  29. package/dist/helpers/database.js +47 -36
  30. package/dist/helpers/database.js.map +1 -1
  31. package/dist/helpers/db.d.ts +1 -1
  32. package/dist/helpers/db.d.ts.map +1 -1
  33. package/dist/helpers/db.js +3 -7
  34. package/dist/helpers/db.js.map +1 -1
  35. package/dist/helpers/endpoint-helpers.d.ts +2 -2
  36. package/dist/helpers/endpoint-helpers.d.ts.map +1 -1
  37. package/dist/helpers/endpoint-helpers.js +59 -17
  38. package/dist/helpers/endpoint-helpers.js.map +1 -1
  39. package/dist/helpers/excel-helper.d.ts +29 -0
  40. package/dist/helpers/excel-helper.d.ts.map +1 -0
  41. package/dist/helpers/excel-helper.js +150 -0
  42. package/dist/helpers/excel-helper.js.map +1 -0
  43. package/dist/helpers/general.d.ts +15 -0
  44. package/dist/helpers/general.d.ts.map +1 -0
  45. package/dist/helpers/general.js +148 -0
  46. package/dist/helpers/general.js.map +1 -0
  47. package/dist/helpers/index.d.ts +2 -0
  48. package/dist/helpers/index.d.ts.map +1 -1
  49. package/dist/helpers/index.js +2 -0
  50. package/dist/helpers/index.js.map +1 -1
  51. package/dist/helpers/logic.d.ts +100 -41
  52. package/dist/helpers/logic.d.ts.map +1 -1
  53. package/dist/helpers/logic.js +523 -413
  54. package/dist/helpers/logic.js.map +1 -1
  55. package/dist/helpers/package-upload.d.ts.map +1 -1
  56. package/dist/helpers/package-upload.js +2 -3
  57. package/dist/helpers/package-upload.js.map +1 -1
  58. package/dist/helpers/package.d.ts +3 -4
  59. package/dist/helpers/package.d.ts.map +1 -1
  60. package/dist/helpers/package.js +12 -17
  61. package/dist/helpers/package.js.map +1 -1
  62. package/dist/helpers/request-headers.d.ts +4 -0
  63. package/dist/helpers/request-headers.d.ts.map +1 -1
  64. package/dist/helpers/request-headers.js +2 -2
  65. package/dist/helpers/request-headers.js.map +1 -1
  66. package/dist/helpers/resource-cache.d.ts +35 -0
  67. package/dist/helpers/resource-cache.d.ts.map +1 -0
  68. package/dist/helpers/resource-cache.js +171 -0
  69. package/dist/helpers/resource-cache.js.map +1 -0
  70. package/dist/helpers/storage.d.ts +2 -2
  71. package/dist/helpers/storage.d.ts.map +1 -1
  72. package/dist/helpers/storage.js +16 -15
  73. package/dist/helpers/storage.js.map +1 -1
  74. package/dist/helpers/utils.d.ts +4 -3
  75. package/dist/helpers/utils.d.ts.map +1 -1
  76. package/dist/helpers/utils.js +73 -51
  77. package/dist/helpers/utils.js.map +1 -1
  78. package/package.json +12 -9
@@ -1,22 +1,23 @@
1
- import { checkAssessmentCode, getPlannedStudent, getStartableAssessments, getStudentSessionInfo, getTeacherIdByCode, isStartable, updateStudentSessionState, } from "./../helpers/logic";
1
+ import { createSessionForDelivery, getDelivery, getSession, getTestsetSessions, getTestsetSessionWithSessions, updateSessionState, processFeedbackSubmission, validateFeedbackData, saveFeedback, } from "./../helpers/logic";
2
2
  import { dateId } from "./../helpers/utils";
3
- import { getAssessmentId, getName } from "./../helpers/request-headers";
3
+ import { Router } from "express";
4
4
  import NodeCache from "node-cache";
5
- import { getDatabase, getStudentGroupSessionInfo } from "../helpers";
6
- import { getFirestore, getStorage } from "../helpers/firebase";
5
+ import { getFirestore } from "../helpers/firebase";
7
6
  import { body, param } from "express-validator";
8
- import { errorHandler, generalRateLimit, handleValidationErrors, heavyOperationLimit, imageUploadConfig, requireAuth, sendError, sendSuccess, } from "../helpers/endpoint-helpers";
7
+ import { errorHandler, generalRateLimit, handleValidationErrors, requireAuth, requireAuthType, sendError, sendSuccess, } from "../helpers/endpoint-helpers";
9
8
  import { BaseImplementation } from "./app-specific/baseImplementation";
9
+ import { getDatabase } from "../helpers";
10
10
  export function addQtiDataEndpoints(app, specificImplementation = new BaseImplementation()) {
11
+ const dataRouter = Router();
11
12
  const cache = new NodeCache({
12
13
  stdTTL: 300, // 5 minutes
13
14
  checkperiod: 60, // Check for expired keys every minute
14
15
  });
15
16
  // Apply global error handler
16
- app.use(errorHandler);
17
+ dataRouter.use(errorHandler);
17
18
  /**
18
19
  * @openapi
19
- * /checkCode:
20
+ * /session/start:
20
21
  * post:
21
22
  * tags:
22
23
  * - Session
@@ -44,7 +45,7 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
44
45
  * 500:
45
46
  * description: Internal server error
46
47
  */
47
- app.post("/checkCode", [
48
+ dataRouter.post("/session/start", [
48
49
  generalRateLimit,
49
50
  body("code")
50
51
  .isString()
@@ -52,13 +53,27 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
52
53
  .isLength({ min: 1, max: 50 })
53
54
  .withMessage("Code must be between 1 and 50 characters"),
54
55
  handleValidationErrors,
55
- requireAuth,
56
+ requireAuthType("student"),
56
57
  ], async (req, res) => {
57
58
  try {
58
59
  const { code } = req.body;
59
- const db = getDatabase(req.appId);
60
- const sessionInfo = await getStudentSessionInfo(code.toUpperCase(), db, req.user.uid);
60
+ const db = getDatabase();
61
+ const sessionInfo = await getSession(code.toUpperCase(), db, req.user.uid);
62
+ // we need to store the mapping between userId and session code.
63
+ // so it is stored in the database and cache
61
64
  if (sessionInfo) {
65
+ const uppercaseCode = code.toUpperCase();
66
+ await cache.set(req.user.uid, {
67
+ code: uppercaseCode,
68
+ teacherId: sessionInfo?.teacherId,
69
+ deliveryId: sessionInfo?.deliveryId,
70
+ });
71
+ const fs = getFirestore();
72
+ await fs.doc(db.AUTH.DOC(req.user.uid)).set({
73
+ code: uppercaseCode,
74
+ teacherId: sessionInfo?.teacherId,
75
+ deliveryId: sessionInfo?.deliveryId,
76
+ });
62
77
  return sendSuccess(res, { ...sessionInfo, userId: req.user.uid });
63
78
  }
64
79
  else {
@@ -72,7 +87,7 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
72
87
  });
73
88
  /**
74
89
  * @openapi
75
- * /checkGroupCode:
90
+ * /delivery/start:
76
91
  * post:
77
92
  * tags:
78
93
  * - Session
@@ -106,13 +121,13 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
106
121
  * 500:
107
122
  * description: Internal server error
108
123
  */
109
- app.post("/checkGroupCode", [
124
+ dataRouter.post("/delivery/session/start", [
110
125
  generalRateLimit,
111
- body("groupCode")
126
+ body("code")
112
127
  .isString()
113
128
  .trim()
114
129
  .isLength({ min: 1, max: 50 })
115
- .withMessage("Group code must be between 1 and 50 characters"),
130
+ .withMessage("Delivery code must be between 1 and 50 characters"),
116
131
  body("studentIdentification")
117
132
  .optional()
118
133
  .isString()
@@ -120,14 +135,26 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
120
135
  .isLength({ max: 100 })
121
136
  .withMessage("Student identification must not exceed 100 characters"),
122
137
  handleValidationErrors,
123
- requireAuth,
138
+ requireAuthType("student"),
124
139
  ], async (req, res) => {
125
140
  try {
126
- const { groupCode, studentIdentification } = req.body;
127
- const db = getDatabase(req.appId);
128
- const sessionInfo = await getStudentGroupSessionInfo(groupCode.toUpperCase(), db, req.user.uid, studentIdentification);
129
- if (sessionInfo) {
130
- return sendSuccess(res, { ...sessionInfo, userId: req.user.uid });
141
+ const { code, studentIdentification } = req.body;
142
+ const db = getDatabase();
143
+ const delivery = await getDelivery(code.toUpperCase(), db);
144
+ // create a session based on the delivery.
145
+ // Create a unique code for the session to be able to restart the session and give it an unique id
146
+ if (!delivery) {
147
+ return sendError(res, 404, "Delivery not found");
148
+ }
149
+ const session = await createSessionForDelivery(db, req.user.uid, delivery);
150
+ const uppercaseCode = code.toUpperCase();
151
+ await cache.set(req.user.uid, uppercaseCode);
152
+ const fs = getFirestore();
153
+ await fs.doc(db.AUTH.DOC(req.user.uid)).set({
154
+ code: uppercaseCode,
155
+ });
156
+ if (session) {
157
+ return sendSuccess(res, session);
131
158
  }
132
159
  else {
133
160
  return sendSuccess(res, null);
@@ -139,70 +166,76 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
139
166
  }
140
167
  });
141
168
  /**
142
- * POST /feedback
143
- * Endpoint to submit user feedback with optional screenshot
169
+ * @openapi
170
+ * /testsetSession/start:
171
+ * post:
172
+ * tags:
173
+ * - Session
174
+ * summary: Use to login by testset session code. Retrieves testset session with associated test sessions.
175
+ * description: Checks if the provided testset session code is valid and retrieves the testset session information along with all associated test sessions.
176
+ * requestBody:
177
+ * required: true
178
+ * content:
179
+ * application/json:
180
+ * schema:
181
+ * type: object
182
+ * required:
183
+ * - code
184
+ * properties:
185
+ * code:
186
+ * type: string
187
+ * description: The testset session code.
188
+ * minLength: 1
189
+ * maxLength: 50
190
+ * responses:
191
+ * 200:
192
+ * description: Successfully retrieved testset session information with associated test sessions or null if not found.
193
+ * 400:
194
+ * description: Validation error
195
+ * 401:
196
+ * description: Authentication failed
197
+ * 404:
198
+ * description: Testset session not found
199
+ * 500:
200
+ * description: Internal server error
144
201
  */
145
- app.post("/feedback", [
146
- heavyOperationLimit,
147
- requireAuth,
148
- imageUploadConfig.single("screenshot"),
149
- body("type")
202
+ dataRouter.post("/testsetSession/start", [
203
+ generalRateLimit,
204
+ body("code")
150
205
  .isString()
151
206
  .trim()
152
207
  .isLength({ min: 1, max: 50 })
153
- .withMessage("Type is required and must be between 1 and 50 characters"),
154
- body("description")
155
- .isString()
156
- .trim()
157
- .isLength({ min: 1, max: 2000 })
158
- .withMessage("Description is required and must be between 1 and 2000 characters"),
159
- body("feedbackId")
160
- .isString()
161
- .trim()
162
- .isLength({ min: 1, max: 100 })
163
- .withMessage("Feedback ID is required"),
164
- body("email")
165
- .optional()
166
- .isEmail()
167
- .withMessage("Must be a valid email address"),
168
- body("pageUrl").optional().isURL().withMessage("Must be a valid URL"),
208
+ .withMessage("Testset session code must be between 1 and 50 characters"),
169
209
  handleValidationErrors,
210
+ requireAuthType("student"),
170
211
  ], async (req, res) => {
171
212
  try {
172
- const { type, description, feedbackId, email, pageUrl } = req.body;
173
- const userId = req.user.uid;
174
- const file = req.file;
175
- // Create feedback document in Firestore
176
- const firestore = getFirestore();
177
- const db = getDatabase(req.appId);
178
- const firebaseDocPath = db.FEEDBACK.DOC(`${feedbackId}_${userId}`);
179
- const feedbackRef = firestore.doc(firebaseDocPath);
180
- await feedbackRef.set({
181
- type,
182
- description,
183
- userId,
184
- email: email || null,
185
- pageUrl: pageUrl || null,
186
- hasScreenshot: !!file,
187
- createdAt: new Date().toISOString(),
188
- });
189
- // If there's a screenshot, upload it to Firebase Storage
190
- if (file) {
191
- const storage = getStorage();
192
- const bucket = storage.bucket();
193
- const storagePath = firebaseDocPath.replace(/^\//, "");
194
- const storageFile = bucket.file(`${storagePath}.png`);
195
- await storageFile.save(file.buffer, {
196
- metadata: {
197
- contentType: file.mimetype,
198
- },
199
- });
213
+ const { code } = req.body;
214
+ const db = getDatabase();
215
+ const uppercaseCode = code.toUpperCase();
216
+ // Get the testset session with populated sessions
217
+ const result = await getTestsetSessionWithSessions(uppercaseCode, db, req.user.uid);
218
+ if (!result) {
219
+ return sendError(res, 404, "Testset session not found");
200
220
  }
201
- return sendSuccess(res, undefined, "Feedback submitted successfully");
221
+ // Store the mapping between userId and testset session code
222
+ await cache.set(req.user.uid, {
223
+ code: uppercaseCode,
224
+ teacherId: result?.teacherId,
225
+ testsetId: result?.testsetId,
226
+ startableCodes: result?.sessionIds || [],
227
+ });
228
+ const fs = getFirestore();
229
+ await fs.doc(db.AUTH.DOC(req.user.uid)).set({
230
+ code: uppercaseCode,
231
+ teacherId: result?.teacherId,
232
+ testsetId: result?.testsetId,
233
+ });
234
+ return sendSuccess(res, result);
202
235
  }
203
236
  catch (error) {
204
- console.error("Error submitting feedback:", error);
205
- return sendError(res, 500, "Failed to submit feedback");
237
+ console.error("Error in testsetSession start:", error);
238
+ return sendError(res, 500, "Failed to start testset session");
206
239
  }
207
240
  });
208
241
  /**
@@ -240,7 +273,7 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
240
273
  * 500:
241
274
  * description: Internal server error
242
275
  */
243
- app.post("/student/log", [
276
+ dataRouter.post("/student/log", [
244
277
  generalRateLimit,
245
278
  body("type")
246
279
  .isString()
@@ -249,23 +282,23 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
249
282
  .withMessage("Type must be a string between 1-100 characters"),
250
283
  body("data").exists().withMessage("Data field is required"),
251
284
  handleValidationErrors,
252
- requireAuth,
285
+ requireAuthType("student"),
253
286
  ], async (req, res) => {
254
287
  try {
255
288
  const { type, data } = req.body;
256
- const db = getDatabase(req.appId);
257
- // Create log entry with timestamp and student ID
258
- const logEntry = {
289
+ const db = getDatabase();
290
+ const { code, teacherId, deliveryId } = await getSessionInfoFromLoggedInUser(req);
291
+ let logEntry = {
292
+ createdAt: new Date().getTime(),
293
+ createdBy: req.user.uid,
259
294
  type,
260
- data,
261
- timestamp: new Date().toISOString(),
262
- studentId: req.user.uid,
263
- appId: req.appId,
295
+ data: data,
264
296
  };
297
+ if (code) {
298
+ logEntry = { ...logEntry, code, teacherId, deliveryId };
299
+ }
265
300
  const firestore = getFirestore();
266
- await firestore
267
- .doc(db.STUDENT.LOG.DOC(req.user.uid, dateId()))
268
- .set(logEntry);
301
+ await firestore.doc(db.SESSION.LOG.DOC(code, dateId())).set(logEntry);
269
302
  return sendSuccess(res, undefined, "Log entry created successfully");
270
303
  }
271
304
  catch (error) {
@@ -275,195 +308,166 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
275
308
  });
276
309
  /**
277
310
  * @openapi
278
- * /assessment/checkCode:
311
+ * /student/log:
279
312
  * post:
280
313
  * tags:
281
- * - Session
282
- * summary: Use to login by code. The code is used to identify the assessment.
283
- * description: Checks if the provided assessment code is valid and retrieves session information for the student.
314
+ * - Student
315
+ * summary: Log student activity
316
+ * description: Logs student activity data for debugging and analytics purposes.
284
317
  * requestBody:
285
318
  * required: true
286
319
  * content:
287
320
  * application/json:
288
321
  * schema:
289
322
  * type: object
323
+ * required:
324
+ * - type
325
+ * - data
290
326
  * properties:
291
- * code:
292
- * type: string
293
- * description: The assessment code.
294
- * identification:
327
+ * type:
295
328
  * type: string
296
- * description: Student identification.
329
+ * minLength: 1
330
+ * maxLength: 100
331
+ * description: The type/category of the log entry
332
+ * data:
333
+ * description: Any data to be logged
297
334
  * responses:
298
335
  * 200:
299
- * description: Successfully retrieved session information.
300
- * 404:
301
- * description: Assessment not found
336
+ * description: Successfully logged the data.
337
+ * 400:
338
+ * description: Invalid request data
339
+ * 401:
340
+ * description: Unauthorized
302
341
  * 500:
303
- * description: Error creating session
342
+ * description: Internal server error
304
343
  */
305
- app.post("/assessment/checkCode", [
344
+ dataRouter.post("/student/log", [
306
345
  generalRateLimit,
307
- body("code")
308
- .isString()
309
- .trim()
310
- .isLength({ min: 1, max: 50 })
311
- .withMessage("Code is required"),
312
- body("identification")
313
- .optional()
346
+ body("type")
314
347
  .isString()
315
348
  .trim()
316
- .isLength({ max: 100 }),
349
+ .isLength({ min: 1, max: 100 })
350
+ .withMessage("Type must be a string between 1-100 characters"),
351
+ body("data").exists().withMessage("Data field is required"),
317
352
  handleValidationErrors,
318
- requireAuth,
353
+ requireAuthType("student"),
319
354
  ], async (req, res) => {
320
355
  try {
321
- const { code, identification } = req.body;
322
- const db = getDatabase(req.appId);
323
- const assessmentInfo = await checkAssessmentCode(db, code.toUpperCase());
324
- if (!assessmentInfo) {
325
- return sendError(res, 404, "Assessment not found");
326
- }
327
- if (assessmentInfo.isDemo) {
328
- const demoInfo = {
329
- appId: req.appId,
330
- code,
331
- currentAssessmentId: assessmentInfo.assessmentId,
332
- sessions: [
333
- {
334
- assessmentId: assessmentInfo.assessmentId,
335
- assessmentName: assessmentInfo.name,
336
- sessionState: "not_started",
337
- packageId: assessmentInfo.packageId,
338
- },
339
- ],
340
- teacherId: assessmentInfo.teacherId || "general",
341
- isDemo: true,
342
- };
343
- return sendSuccess(res, demoInfo);
344
- }
345
- const studentInfo = await specificImplementation.createSessionInfoForStudent({
346
- userId: req.user.uid,
347
- assessmentId: assessmentInfo.assessmentId,
348
- identification,
349
- metadata: {
350
- isDemo: assessmentInfo.isDemo,
351
- },
352
- });
353
- if (!studentInfo) {
354
- return sendError(res, 500, "Error creating session");
355
- }
356
- return sendSuccess(res, studentInfo);
356
+ const { type, data } = req.body;
357
+ const db = getDatabase();
358
+ const { code, teacherId, deliveryId } = await getSessionInfoFromLoggedInUser(req);
359
+ const logEntry = {
360
+ createdAt: new Date().getTime(),
361
+ createdBy: req.user.uid,
362
+ type,
363
+ data: data && code ? { ...data, code, teacherId, deliveryId } : data,
364
+ };
365
+ const firestore = getFirestore();
366
+ await firestore.doc(db.SESSION.LOG.DOC(code, dateId())).set(logEntry);
367
+ return sendSuccess(res, undefined, "Log entry created successfully");
357
368
  }
358
369
  catch (error) {
359
- console.error("Error in assessment checkCode:", error);
360
- return sendError(res, 500, "Failed to check assessment code");
370
+ console.error("Error creating log entry:", error);
371
+ return sendError(res, 500, "Failed to create log entry");
361
372
  }
362
373
  });
363
374
  /**
364
375
  * @openapi
365
- * /session/start:
366
- * post:
376
+ * /session/info:
377
+ * get:
367
378
  * tags:
368
379
  * - Session
369
- * summary: Start a session
370
- * description: Starts a session for a student with the given assessment ID and identification.
380
+ * summary: Get session info
381
+ * description: Retrieves session information for the authenticated student.
371
382
  */
372
- app.post("/session/start", [
373
- generalRateLimit,
374
- body("assessmentId")
375
- .isString()
376
- .trim()
377
- .isLength({ min: 1 })
378
- .withMessage("Assessment ID is required"),
379
- body("identification").optional().isString().trim(),
380
- body("metadata").optional().isObject(),
381
- handleValidationErrors,
382
- requireAuth,
383
- ], async (req, res) => {
383
+ dataRouter.get("/session/info", [generalRateLimit, requireAuth], async (req, res) => {
384
384
  try {
385
- const { metadata, assessmentId, identification } = req.body;
386
- const db = getDatabase(req.appId);
387
- const assessments = await getStartableAssessments(db);
388
- const assessment = assessments.find((a) => a.assessmentId === assessmentId);
389
- if (!assessment) {
390
- return sendError(res, 404, "Assessment not found");
391
- }
392
- const validMetadata = metadata && typeof metadata === "object" ? metadata : undefined;
393
- let sessionInfo = await specificImplementation.getSessionInfoForStudent(req.user.uid, assessmentId, identification);
394
- const canStart = isStartable(assessment);
395
- if (!sessionInfo && canStart) {
396
- sessionInfo =
397
- await specificImplementation.createSessionInfoForStudent({
398
- userId: req.user.uid,
399
- identification,
400
- assessmentId: assessmentId,
401
- metadata: validMetadata,
402
- });
403
- }
404
- if ((sessionInfo && canStart) || sessionInfo?.isDemo) {
405
- return sendSuccess(res, { ...sessionInfo, userId: req.user.uid });
385
+ const { code } = await getSessionInfoFromLoggedInUser(req);
386
+ const fs = getFirestore();
387
+ const db = getDatabase();
388
+ const sessionRef = fs.doc(db.SESSION.DOC(code));
389
+ const sessionSnapshot = await sessionRef.get();
390
+ const session = sessionSnapshot.exists
391
+ ? sessionSnapshot.data()
392
+ : null;
393
+ if (session) {
394
+ return sendSuccess(res, session);
406
395
  }
407
396
  else {
408
- return sendSuccess(res, null);
397
+ return sendError(res, 404, "Session not found");
409
398
  }
410
399
  }
411
400
  catch (error) {
412
- console.error("Error starting session:", error);
413
- return sendError(res, 500, "Failed to start session");
401
+ console.error("Error getting session info:", error);
402
+ return sendError(res, 500, "Failed to get session info");
414
403
  }
415
404
  });
416
405
  /**
417
406
  * @openapi
418
- * /session/info:
407
+ * /testsetSession/info:
419
408
  * get:
420
409
  * tags:
421
410
  * - Session
422
- * summary: Get session info
423
- * description: Retrieves session information for the authenticated student.
411
+ * summary: Get testset session info
412
+ * description: Retrieves testset session information with associated test sessions for the authenticated student.
413
+ * responses:
414
+ * 200:
415
+ * description: Successfully retrieved testset session information with associated test sessions.
416
+ * 401:
417
+ * description: Authentication failed
418
+ * 404:
419
+ * description: Testset session not found
420
+ * 500:
421
+ * description: Internal server error
424
422
  */
425
- app.get("/session/info", [generalRateLimit, requireAuth], async (req, res) => {
423
+ dataRouter.get("/testsetSession/info", [generalRateLimit, requireAuth], async (req, res) => {
426
424
  try {
427
- const assessmentId = getAssessmentId(req);
428
- const identification = getName(req);
429
- const sessionInfo = await specificImplementation.getSessionInfoForStudent(req.user.uid, assessmentId, identification);
430
- if (sessionInfo) {
431
- return sendSuccess(res, sessionInfo);
425
+ const db = getDatabase();
426
+ const { code: uppercaseCode } = await getSessionInfoFromLoggedInUser(req);
427
+ if (!uppercaseCode) {
428
+ return sendError(res, 404, "No active session found for user");
432
429
  }
433
- else {
434
- return sendError(res, 404, "Session not found");
430
+ // Get the testset session with populated sessions
431
+ const result = await getTestsetSessionWithSessions(uppercaseCode, db, req.user.uid);
432
+ if (!result) {
433
+ return sendError(res, 404, "Testset session not found");
435
434
  }
435
+ return sendSuccess(res, result);
436
436
  }
437
437
  catch (error) {
438
- console.error("Error getting session info:", error);
439
- return sendError(res, 500, "Failed to get session info");
438
+ console.error("Error getting testset session info:", error);
439
+ return sendError(res, 500, "Failed to get testset session info");
440
440
  }
441
441
  });
442
442
  /**
443
443
  * @openapi
444
- * /session/{assessmentId}/context:
444
+ * /session/{code}/context:
445
445
  * get:
446
446
  * tags:
447
447
  * - Session
448
448
  * summary: Get session context
449
- * description: Retrieves session context for a specific assessment.
449
+ * description: Retrieves session context for a specific session.
450
450
  */
451
- app.get("/session/:assessmentId/context", [
451
+ dataRouter.get("/session/:code/context", [
452
452
  generalRateLimit,
453
- param("assessmentId")
453
+ param("code")
454
454
  .isString()
455
455
  .trim()
456
456
  .isLength({ min: 1 })
457
- .withMessage("Assessment ID is required"),
457
+ .withMessage("Session code is required"),
458
458
  handleValidationErrors,
459
- requireAuth,
459
+ requireAuthType("student"),
460
460
  ], async (req, res) => {
461
461
  try {
462
- const { assessmentId } = req.params;
463
- const db = getDatabase(req.appId);
464
- const code = await specificImplementation.getCodeByUserId(req.user.uid, assessmentId);
462
+ const { code } = req.params;
463
+ const db = getDatabase();
464
+ const uppercaseCode = code.toUpperCase();
465
+ const { startableCodes } = await getSessionInfoFromLoggedInUser(req);
466
+ if (!startableCodes.includes(uppercaseCode)) {
467
+ return sendError(res, 403, "Code mismatch");
468
+ }
465
469
  const firestore = getFirestore();
466
- const sessionRef = firestore.doc(db.STUDENT.SESSION.DOC(code, assessmentId));
470
+ const sessionRef = firestore.doc(db.SESSION.TEST_CONTEXT(code));
467
471
  const context = await sessionRef.get();
468
472
  return sendSuccess(res, context.exists ? context.data() : null);
469
473
  }
@@ -474,34 +478,38 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
474
478
  });
475
479
  /**
476
480
  * @openapi
477
- * /session/{assessmentId}/context:
481
+ * /session/{code}/context:
478
482
  * post:
479
483
  * tags:
480
484
  * - Session
481
485
  * summary: Update session context
482
486
  * description: Updates session context for a specific assessment.
483
487
  */
484
- app.post("/session/:assessmentId/context", [
488
+ dataRouter.post("/session/:code/context", [
485
489
  generalRateLimit,
486
- param("assessmentId")
490
+ param("code")
487
491
  .isString()
488
492
  .trim()
489
493
  .isLength({ min: 1 })
490
- .withMessage("Assessment ID is required"),
494
+ .withMessage("code is required"),
491
495
  body("state").optional().isString(),
492
496
  body("items").optional().isArray(),
493
497
  handleValidationErrors,
494
- requireAuth,
498
+ requireAuthType("student"),
495
499
  ], async (req, res) => {
496
500
  try {
497
- const { assessmentId } = req.params;
501
+ const { code: routeCode } = req.params;
498
502
  const testContext = req.body;
499
- const db = getDatabase(req.appId);
500
- const code = await specificImplementation.getCodeByUserId(req.user.uid, assessmentId);
501
- const cacheKey = `test-context-${req.appId}-${code}-${assessmentId}`;
502
- let previousState = cache.get(cacheKey);
503
+ const db = getDatabase();
504
+ const code = routeCode.toUpperCase();
505
+ const { code: codeRetrievedFromUser, teacherId, deliveryId, startableCodes, } = await getSessionInfoFromLoggedInUser(req);
506
+ if (!startableCodes.includes(code)) {
507
+ return sendError(res, 403, "Code mismatch");
508
+ }
509
+ const sessionKey = `test-context-${code}`;
510
+ let previousState = cache.get(sessionKey);
503
511
  const firestore = getFirestore();
504
- const sessionRef = firestore.doc(db.STUDENT.SESSION.DOC(code, assessmentId));
512
+ const sessionRef = firestore.doc(db.SESSION.TEST_CONTEXT(code));
505
513
  if (!previousState) {
506
514
  const previousContext = await sessionRef.get();
507
515
  if (previousContext.exists) {
@@ -511,14 +519,22 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
511
519
  }
512
520
  const batch = firestore.batch();
513
521
  if (previousState !== testContext.state) {
514
- cache.set(cacheKey, testContext.state, 60 * 60 * 1000); // 1 hour
515
- const teacherId = await getTeacherIdByCode(db, code);
522
+ cache.set(sessionKey, testContext.state, 60 * 60 * 1000); // 1 hour
523
+ const teacherId = await getTeacherIdBySessionCode(code);
516
524
  if (!teacherId) {
517
525
  return sendError(res, 404, "Teacher not found for this code");
518
526
  }
527
+ // Get session to extract assessmentId
528
+ const sessionDocRef = firestore.doc(db.SESSION.DOC(code));
529
+ const sessionDoc = await sessionDocRef.get();
530
+ const session = sessionDoc.data();
531
+ const assessmentId = session?.assessmentId;
532
+ if (!assessmentId) {
533
+ return sendError(res, 404, "Assessment ID not found for this session");
534
+ }
519
535
  await Promise.all([
520
- specificImplementation.testContextToTeacher(code, assessmentId, testContext, batch),
521
- updateStudentSessionState(db, code, assessmentId, testContext?.state ?? "not_started", batch),
536
+ specificImplementation.testContextToTeacher(code, assessmentId, deliveryId, teacherId, testContext, batch),
537
+ updateSessionState(db, code, testContext?.state ?? "not_started", batch),
522
538
  specificImplementation.updateItemStats(teacherId, assessmentId, code, testContext.items, "teacher", batch),
523
539
  ]);
524
540
  }
@@ -531,53 +547,235 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
531
547
  return sendError(res, 500, "Failed to update session context");
532
548
  }
533
549
  });
550
+ const getSessionInfoFromLoggedInUser = async (req) => {
551
+ if (cache.get(req.user.uid)) {
552
+ return cache.get(req.user.uid);
553
+ }
554
+ const db = getDatabase();
555
+ const fs = getFirestore();
556
+ const sessionRef = fs.doc(db.AUTH.DOC(req.user.uid));
557
+ const data = await sessionRef.get().then((doc) => doc.data());
558
+ const { code, teacherId, deliveryId, testsetId } = data || {};
559
+ // Build list of startable codes
560
+ const startableCodes = [];
561
+ // If we have a code from sessionRef, add it to startable codes
562
+ if (code) {
563
+ startableCodes.push(code);
564
+ }
565
+ // If no session found in sessionRef, check testsetsessions using testsetId
566
+ if (testsetId) {
567
+ try {
568
+ const testsetSession = await getTestsetSessions(code, db);
569
+ if (testsetSession && testsetSession.sessionIds) {
570
+ startableCodes.push(...testsetSession.sessionIds);
571
+ }
572
+ }
573
+ catch (error) {
574
+ console.error("Error fetching testset sessions:", error);
575
+ }
576
+ }
577
+ const result = { code, teacherId, deliveryId, testsetId, startableCodes };
578
+ cache.set(req.user.uid, result);
579
+ return result;
580
+ };
581
+ const getTeacherIdBySessionCode = async (code) => {
582
+ if (cache.get(code)) {
583
+ return cache.get(code);
584
+ }
585
+ const db = getDatabase();
586
+ const fs = getFirestore();
587
+ const sessionRef = fs.doc(db.SESSION.DOC(code));
588
+ const sessionDoc = await sessionRef.get();
589
+ const teacherId = sessionDoc.data()?.teacherId;
590
+ cache.set(code, teacherId);
591
+ return teacherId;
592
+ };
534
593
  /**
535
594
  * @openapi
536
- * /session/{assessmentId}/sessionState:
537
- * post:
595
+ * /session/{code}/update:
596
+ * put:
538
597
  * tags:
539
598
  * - Session
540
- * summary: Update session state
541
- * description: Updates the session state for a specific session.
599
+ * summary: Update student session information
600
+ * description: Updates specific fields of a student session.
601
+ * parameters:
602
+ * - in: path
603
+ * name: code
604
+ * required: true
605
+ * schema:
606
+ * type: string
607
+ * description: The session code
608
+ * requestBody:
609
+ * required: true
610
+ * content:
611
+ * application/json:
612
+ * schema:
613
+ * type: object
614
+ * description: Partial session data to update
615
+ * responses:
616
+ * 200:
617
+ * description: Successfully updated session information.
618
+ * 403:
619
+ * description: Code mismatch
620
+ * 500:
621
+ * description: Internal server error
542
622
  */
543
- app.post("/session/:assessmentId/sessionState", [
623
+ dataRouter.put("/session/:code/update", [
544
624
  generalRateLimit,
545
- param("assessmentId")
625
+ param("code")
546
626
  .isString()
547
627
  .trim()
548
628
  .isLength({ min: 1 })
549
- .withMessage("Assessment ID is required"),
550
- body("sessionState")
629
+ .withMessage("Session code is required"),
630
+ handleValidationErrors,
631
+ requireAuthType("student"),
632
+ ], async (req, res) => {
633
+ try {
634
+ const { code } = req.params;
635
+ const data = req.body;
636
+ const db = getDatabase();
637
+ const uppercaseCode = code.toUpperCase();
638
+ const { code: codeRetrievedFromUser, startableCodes } = await getSessionInfoFromLoggedInUser(req);
639
+ if (!startableCodes.includes(uppercaseCode)) {
640
+ return sendError(res, 403, "Code mismatch");
641
+ }
642
+ const firestore = getFirestore();
643
+ const sessionRef = firestore.doc(db.SESSION.DOC(uppercaseCode));
644
+ await sessionRef.update(data);
645
+ return sendSuccess(res, undefined, "Session updated successfully");
646
+ }
647
+ catch (error) {
648
+ console.error("Error updating session:", error);
649
+ return sendError(res, 500, "Failed to update session");
650
+ }
651
+ });
652
+ /**
653
+ * @openapi
654
+ * /assessment/code/{code}:
655
+ * get:
656
+ * tags:
657
+ * - Assessment
658
+ * summary: Get assessment by code
659
+ * description: Retrieves assessment information by code.
660
+ * parameters:
661
+ * - in: path
662
+ * name: code
663
+ * required: true
664
+ * schema:
665
+ * type: string
666
+ * description: The assessment code
667
+ * responses:
668
+ * 200:
669
+ * description: Successfully retrieved assessment information.
670
+ * 404:
671
+ * description: Assessment not found
672
+ * 500:
673
+ * description: Internal server error
674
+ */
675
+ dataRouter.get("/assessment/code/:code", [
676
+ generalRateLimit,
677
+ param("code")
551
678
  .isString()
552
679
  .trim()
553
680
  .isLength({ min: 1 })
554
- .withMessage("Session state is required"),
681
+ .withMessage("Assessment code is required"),
555
682
  handleValidationErrors,
556
- requireAuth,
683
+ requireAuthType("student"),
557
684
  ], async (req, res) => {
558
685
  try {
559
- const { assessmentId } = req.params;
560
- const { sessionState } = req.body;
561
- const db = getDatabase(req.appId);
562
- const code = await specificImplementation.getCodeByUserId(req.user.uid, assessmentId);
563
- const firestore = getFirestore();
564
- const batch = firestore.batch();
565
- await updateStudentSessionState(db, code, assessmentId, sessionState, batch);
566
- const teacherId = await getTeacherIdByCode(db, code);
567
- if (!teacherId) {
568
- return sendError(res, 404, "Teacher not found for this code");
569
- }
570
- const studentSessions = await getPlannedStudent(db, teacherId, code);
571
- const session = studentSessions.sessions.find((s) => s.assessmentId === assessmentId);
572
- if (session) {
573
- await specificImplementation.updatePlannedSession(teacherId, code, { ...session, sessionState }, batch);
686
+ const { code } = req.params;
687
+ const db = getDatabase();
688
+ // This would need to be implemented based on your database structure
689
+ // For now, returning a placeholder implementation
690
+ return sendError(res, 501, "Assessment by code endpoint not yet implemented");
691
+ }
692
+ catch (error) {
693
+ console.error("Error getting assessment by code:", error);
694
+ return sendError(res, 500, "Failed to get assessment");
695
+ }
696
+ });
697
+ /**
698
+ * @openapi
699
+ * /currentUser:
700
+ * get:
701
+ * tags:
702
+ * - User
703
+ * summary: Get current user information
704
+ * description: Retrieves information about the currently authenticated user.
705
+ * responses:
706
+ * 200:
707
+ * description: Successfully retrieved user information.
708
+ * 401:
709
+ * description: Unauthorized
710
+ * 500:
711
+ * description: Internal server error
712
+ */
713
+ dataRouter.get("/currentUser", [generalRateLimit, requireAuth], async (req, res) => {
714
+ try {
715
+ // Return basic user info from the authenticated request
716
+ const userInfo = {
717
+ userId: req.user.uid,
718
+ // Add other user properties as needed
719
+ };
720
+ return sendSuccess(res, userInfo);
721
+ }
722
+ catch (error) {
723
+ console.error("Error getting current user:", error);
724
+ return sendError(res, 500, "Failed to get current user");
725
+ }
726
+ });
727
+ /**
728
+ * @openapi
729
+ * /session/{code}/score:
730
+ * post:
731
+ * tags:
732
+ * - Session
733
+ * summary: Score items in a session
734
+ * description: Scores all items in a session and returns the updated test context.
735
+ * parameters:
736
+ * - in: path
737
+ * name: code
738
+ * required: true
739
+ * schema:
740
+ * type: string
741
+ * description: The session code
742
+ * responses:
743
+ * 200:
744
+ * description: Successfully scored items and returned test context.
745
+ * 403:
746
+ * description: Code mismatch
747
+ * 500:
748
+ * description: Internal server error
749
+ */
750
+ dataRouter.post("/session/:code/score", [
751
+ generalRateLimit,
752
+ param("code")
753
+ .isString()
754
+ .trim()
755
+ .isLength({ min: 1 })
756
+ .withMessage("Session code is required"),
757
+ handleValidationErrors,
758
+ requireAuthType("student"),
759
+ ], async (req, res) => {
760
+ try {
761
+ const { code } = req.params;
762
+ const db = getDatabase();
763
+ const uppercaseCode = code.toUpperCase();
764
+ const { code: codeRetrievedFromUser, startableCodes } = await getSessionInfoFromLoggedInUser(req);
765
+ if (!startableCodes.includes(uppercaseCode)) {
766
+ return sendError(res, 403, "Code mismatch");
574
767
  }
575
- await batch.commit();
576
- return sendSuccess(res, undefined, "Session state updated successfully");
768
+ // This would need to implement the scoring logic
769
+ // For now, returning a placeholder implementation
770
+ const firestore = getFirestore();
771
+ const sessionRef = firestore.doc(db.SESSION.DOC(uppercaseCode));
772
+ const sessionDoc = await sessionRef.get();
773
+ const testContext = sessionDoc.data();
774
+ return sendSuccess(res, testContext);
577
775
  }
578
776
  catch (error) {
579
- console.error("Error updating session state:", error);
580
- return sendError(res, 500, "Failed to update session state");
777
+ console.error("Error scoring items:", error);
778
+ return sendError(res, 500, "Failed to score items");
581
779
  }
582
780
  });
583
781
  /**
@@ -586,37 +784,187 @@ export function addQtiDataEndpoints(app, specificImplementation = new BaseImplem
586
784
  * post:
587
785
  * tags:
588
786
  * - Session
589
- * summary: Log session data
590
- * description: Logs session data for a specific assessment.
787
+ * summary: Log action for a session
788
+ * description: Logs an action with payload for a specific session.
789
+ * parameters:
790
+ * - in: path
791
+ * name: assessmentId
792
+ * required: true
793
+ * schema:
794
+ * type: string
795
+ * description: The session/assessment identifier
796
+ * requestBody:
797
+ * required: true
798
+ * content:
799
+ * application/json:
800
+ * schema:
801
+ * type: object
802
+ * properties:
803
+ * type:
804
+ * type: string
805
+ * description: The action type
806
+ * payload:
807
+ * description: The action payload
808
+ * time:
809
+ * type: string
810
+ * description: The timestamp
811
+ * createdBy:
812
+ * type: string
813
+ * description: User ID who created the log
814
+ * responses:
815
+ * 200:
816
+ * description: Successfully logged the action.
817
+ * 500:
818
+ * description: Internal server error
591
819
  */
592
- app.post("/session/:assessmentId/log", [
820
+ dataRouter.post("/session/:assessmentId/log", [
593
821
  generalRateLimit,
594
822
  param("assessmentId")
595
823
  .isString()
596
824
  .trim()
597
825
  .isLength({ min: 1 })
598
826
  .withMessage("Assessment ID is required"),
599
- body("logData").isObject().withMessage("Log data must be an object"),
827
+ body("type")
828
+ .isString()
829
+ .trim()
830
+ .isLength({ min: 1 })
831
+ .withMessage("Type is required"),
600
832
  handleValidationErrors,
601
- requireAuth,
833
+ requireAuthType("student"),
602
834
  ], async (req, res) => {
603
835
  try {
604
836
  const { assessmentId } = req.params;
605
- const db = getDatabase(req.appId);
606
- const code = await specificImplementation.getCodeByUserId(req.user.uid, assessmentId);
837
+ const { type, payload, time, createdBy } = req.body;
838
+ const db = getDatabase();
839
+ const logEntry = {
840
+ type,
841
+ payload,
842
+ time: time || dateId(),
843
+ createdBy: createdBy || req.user.uid,
844
+ createdAt: new Date().getTime(),
845
+ };
607
846
  const firestore = getFirestore();
608
- const sessionRef = firestore.doc(db.STUDENT.SESSION.LOG(code, assessmentId, dateId()));
609
- await sessionRef.set({
610
- ...req.body,
611
- timestamp: new Date().toISOString(),
612
- userId: req.user.uid,
613
- });
614
- return sendSuccess(res, undefined, "Log data saved successfully");
847
+ await firestore
848
+ .doc(db.SESSION.LOG.DOC(assessmentId, logEntry.time))
849
+ .set(logEntry);
850
+ return sendSuccess(res, undefined, "Action logged successfully");
851
+ }
852
+ catch (error) {
853
+ console.error("Error logging action:", error);
854
+ return sendError(res, 500, "Failed to log action");
855
+ }
856
+ });
857
+ /**
858
+ * @openapi
859
+ * /checkCode:
860
+ * post:
861
+ * tags:
862
+ * - Session
863
+ * summary: Check if a code is valid and get session info
864
+ * description: Checks if the provided code is valid and retrieves session information.
865
+ * requestBody:
866
+ * required: true
867
+ * content:
868
+ * application/json:
869
+ * schema:
870
+ * type: object
871
+ * properties:
872
+ * code:
873
+ * type: string
874
+ * description: The session code.
875
+ * responses:
876
+ * 200:
877
+ * description: Successfully retrieved session information or null if not found.
878
+ * 500:
879
+ * description: Internal server error
880
+ */
881
+ dataRouter.post("/checkCode", [
882
+ generalRateLimit,
883
+ body("code")
884
+ .isString()
885
+ .trim()
886
+ .isLength({ min: 1, max: 50 })
887
+ .withMessage("Code must be between 1 and 50 characters"),
888
+ handleValidationErrors,
889
+ requireAuthType("student"),
890
+ ], async (req, res) => {
891
+ try {
892
+ const { code } = req.body;
893
+ const db = getDatabase();
894
+ const sessionInfo = await getSession(code.toUpperCase(), db, req.user.uid);
895
+ return sendSuccess(res, sessionInfo);
896
+ }
897
+ catch (error) {
898
+ console.error("Error in checkCode:", error);
899
+ return sendError(res, 500, "Failed to check code");
900
+ }
901
+ });
902
+ /**
903
+ * @openapi
904
+ * /feedback:
905
+ * post:
906
+ * tags:
907
+ * - Student
908
+ * summary: Submit student feedback
909
+ * description: Endpoint for students to submit feedback with optional screenshot
910
+ * requestBody:
911
+ * required: true
912
+ * content:
913
+ * multipart/form-data:
914
+ * schema:
915
+ * type: object
916
+ * required:
917
+ * - type
918
+ * - description
919
+ * - feedbackId
920
+ * properties:
921
+ * type:
922
+ * type: string
923
+ * description: Type of feedback
924
+ * description:
925
+ * type: string
926
+ * description: Feedback description
927
+ * feedbackId:
928
+ * type: string
929
+ * description: Unique feedback identifier
930
+ * email:
931
+ * type: string
932
+ * description: User email (optional)
933
+ * pageUrl:
934
+ * type: string
935
+ * description: URL of the page where feedback was submitted
936
+ * screenshot:
937
+ * type: string
938
+ * format: binary
939
+ * description: Optional screenshot file
940
+ * responses:
941
+ * 200:
942
+ * description: Feedback submitted successfully
943
+ * 400:
944
+ * description: Missing required fields or validation error
945
+ * 401:
946
+ * description: Authentication required
947
+ * 500:
948
+ * description: Server error
949
+ */
950
+ dataRouter.post("/feedback", [generalRateLimit, requireAuthType("student")], async (req, res) => {
951
+ try {
952
+ const userId = req.user.uid;
953
+ const { data, fileBuffer } = await processFeedbackSubmission(req);
954
+ // Validate the feedback data
955
+ const validationError = validateFeedbackData(data);
956
+ if (validationError) {
957
+ return sendError(res, 400, validationError);
958
+ }
959
+ // Save feedback with student user type
960
+ await saveFeedback(data, userId, 'student', fileBuffer);
961
+ return sendSuccess(res, undefined, "Feedback submitted successfully");
615
962
  }
616
963
  catch (error) {
617
- console.error("Error logging session data:", error);
618
- return sendError(res, 500, "Failed to log session data");
964
+ console.error("Error submitting student feedback:", error);
965
+ return sendError(res, 500, "An unexpected error occurred");
619
966
  }
620
967
  });
968
+ app.use("/", dataRouter);
621
969
  }
622
970
  //# sourceMappingURL=qti-data.js.map