@citolab/qti-backend-firebase 0.0.3

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 (111) hide show
  1. package/LICENSE.md +674 -0
  2. package/dist/api/app-specific/base/application-specific-base.d.ts +45 -0
  3. package/dist/api/app-specific/base/application-specific-base.d.ts.map +1 -0
  4. package/dist/api/app-specific/base/application-specific-base.js +503 -0
  5. package/dist/api/app-specific/base/application-specific-base.js.map +1 -0
  6. package/dist/api/app-specific/base/cheerio-helper.d.ts +5 -0
  7. package/dist/api/app-specific/base/cheerio-helper.d.ts.map +1 -0
  8. package/dist/api/app-specific/base/cheerio-helper.js +16 -0
  9. package/dist/api/app-specific/base/cheerio-helper.js.map +1 -0
  10. package/dist/api/app-specific/baseImplementation.d.ts +6 -0
  11. package/dist/api/app-specific/baseImplementation.d.ts.map +1 -0
  12. package/dist/api/app-specific/baseImplementation.js +11 -0
  13. package/dist/api/app-specific/baseImplementation.js.map +1 -0
  14. package/dist/api/app-specific/index.d.ts +4 -0
  15. package/dist/api/app-specific/index.d.ts.map +1 -0
  16. package/dist/api/app-specific/index.js +4 -0
  17. package/dist/api/app-specific/index.js.map +1 -0
  18. package/dist/api/app-specific/interface/IApplicationSpecific.d.ts +31 -0
  19. package/dist/api/app-specific/interface/IApplicationSpecific.d.ts.map +1 -0
  20. package/dist/api/app-specific/interface/IApplicationSpecific.js +2 -0
  21. package/dist/api/app-specific/interface/IApplicationSpecific.js.map +1 -0
  22. package/dist/api/app-specific/specific.d.ts +1 -0
  23. package/dist/api/app-specific/specific.d.ts.map +1 -0
  24. package/dist/api/app-specific/specific.js +31 -0
  25. package/dist/api/app-specific/specific.js.map +1 -0
  26. package/dist/api/index.d.ts +3 -0
  27. package/dist/api/index.d.ts.map +1 -0
  28. package/dist/api/index.js +15 -0
  29. package/dist/api/index.js.map +1 -0
  30. package/dist/api/qti-data.d.ts +4 -0
  31. package/dist/api/qti-data.d.ts.map +1 -0
  32. package/dist/api/qti-data.js +622 -0
  33. package/dist/api/qti-data.js.map +1 -0
  34. package/dist/api/qti-resources.d.ts +4 -0
  35. package/dist/api/qti-resources.d.ts.map +1 -0
  36. package/dist/api/qti-resources.js +348 -0
  37. package/dist/api/qti-resources.js.map +1 -0
  38. package/dist/api/qti-teacher.d.ts +6 -0
  39. package/dist/api/qti-teacher.d.ts.map +1 -0
  40. package/dist/api/qti-teacher.js +807 -0
  41. package/dist/api/qti-teacher.js.map +1 -0
  42. package/dist/api/qti-tools.d.ts +3 -0
  43. package/dist/api/qti-tools.d.ts.map +1 -0
  44. package/dist/api/qti-tools.js +450 -0
  45. package/dist/api/qti-tools.js.map +1 -0
  46. package/dist/console.d.ts +2 -0
  47. package/dist/console.d.ts.map +1 -0
  48. package/dist/console.js +30 -0
  49. package/dist/console.js.map +1 -0
  50. package/dist/express-test.d.ts +2 -0
  51. package/dist/express-test.d.ts.map +1 -0
  52. package/dist/express-test.js +19 -0
  53. package/dist/express-test.js.map +1 -0
  54. package/dist/helpers/ci-bootstap.d.ts +4 -0
  55. package/dist/helpers/ci-bootstap.d.ts.map +1 -0
  56. package/dist/helpers/ci-bootstap.js +96 -0
  57. package/dist/helpers/ci-bootstap.js.map +1 -0
  58. package/dist/helpers/database.d.ts +110 -0
  59. package/dist/helpers/database.d.ts.map +1 -0
  60. package/dist/helpers/database.js +151 -0
  61. package/dist/helpers/database.js.map +1 -0
  62. package/dist/helpers/db.d.ts +3 -0
  63. package/dist/helpers/db.d.ts.map +1 -0
  64. package/dist/helpers/db.js +10 -0
  65. package/dist/helpers/db.js.map +1 -0
  66. package/dist/helpers/endpoint-helpers.d.ts +33 -0
  67. package/dist/helpers/endpoint-helpers.d.ts.map +1 -0
  68. package/dist/helpers/endpoint-helpers.js +149 -0
  69. package/dist/helpers/endpoint-helpers.js.map +1 -0
  70. package/dist/helpers/firebase.d.ts +7 -0
  71. package/dist/helpers/firebase.d.ts.map +1 -0
  72. package/dist/helpers/firebase.js +15 -0
  73. package/dist/helpers/firebase.js.map +1 -0
  74. package/dist/helpers/index.d.ts +11 -0
  75. package/dist/helpers/index.d.ts.map +1 -0
  76. package/dist/helpers/index.js +11 -0
  77. package/dist/helpers/index.js.map +1 -0
  78. package/dist/helpers/local-helpers.d.ts +2 -0
  79. package/dist/helpers/local-helpers.d.ts.map +1 -0
  80. package/dist/helpers/local-helpers.js +11 -0
  81. package/dist/helpers/local-helpers.js.map +1 -0
  82. package/dist/helpers/logic.d.ts +73 -0
  83. package/dist/helpers/logic.d.ts.map +1 -0
  84. package/dist/helpers/logic.js +759 -0
  85. package/dist/helpers/logic.js.map +1 -0
  86. package/dist/helpers/package-upload.d.ts +16 -0
  87. package/dist/helpers/package-upload.d.ts.map +1 -0
  88. package/dist/helpers/package-upload.js +160 -0
  89. package/dist/helpers/package-upload.js.map +1 -0
  90. package/dist/helpers/package.d.ts +32 -0
  91. package/dist/helpers/package.d.ts.map +1 -0
  92. package/dist/helpers/package.js +373 -0
  93. package/dist/helpers/package.js.map +1 -0
  94. package/dist/helpers/request-headers.d.ts +27 -0
  95. package/dist/helpers/request-headers.d.ts.map +1 -0
  96. package/dist/helpers/request-headers.js +144 -0
  97. package/dist/helpers/request-headers.js.map +1 -0
  98. package/dist/helpers/storage.d.ts +10 -0
  99. package/dist/helpers/storage.d.ts.map +1 -0
  100. package/dist/helpers/storage.js +132 -0
  101. package/dist/helpers/storage.js.map +1 -0
  102. package/dist/helpers/utils.d.ts +42 -0
  103. package/dist/helpers/utils.d.ts.map +1 -0
  104. package/dist/helpers/utils.js +228 -0
  105. package/dist/helpers/utils.js.map +1 -0
  106. package/dist/main.d.ts +2 -0
  107. package/dist/main.d.ts.map +1 -0
  108. package/dist/main.js +14 -0
  109. package/dist/main.js.map +1 -0
  110. package/package.json +101 -0
  111. package/readme.md +11 -0
@@ -0,0 +1,807 @@
1
+ import { authenticateTeacher, getAppId } from "../helpers/request-headers";
2
+ import { createSession, createSessionByIdentifications, deleteStudent, getApplication, getItemsStats, getPlannedStudents, resetSession, updateItemStatResponseScore, updateStudent, updateStudentSessionState, } from "./../helpers/logic";
3
+ import { getDatabase } from "../helpers/db";
4
+ import { getFirestore } from "../helpers/firebase";
5
+ import { body, param, query } from "express-validator";
6
+ import { generalRateLimit, handleValidationErrors, heavyOperationLimit, sendError, sendSuccess, validateAssessmentId, } from "../helpers/endpoint-helpers";
7
+ import { dateId, createCode } from "../helpers";
8
+ import { BaseImplementation } from "./app-specific/baseImplementation";
9
+ // Teacher authentication middleware
10
+ export const requireTeacherAuth = async (req, res, next) => {
11
+ try {
12
+ const result = await authenticateTeacher(req, res);
13
+ if (!result || result.handled) {
14
+ return sendError(res, 401, "Teacher authentication required");
15
+ }
16
+ req.user = result.user;
17
+ req.appId = getAppId(req);
18
+ next();
19
+ }
20
+ catch (error) {
21
+ console.error("Teacher authentication error:", error);
22
+ return sendError(res, 500, "Authentication failed");
23
+ }
24
+ };
25
+ const checkCode = async (db, groupCode) => {
26
+ try {
27
+ const firestore = getFirestore();
28
+ const doc = await firestore.doc(db.GROUP_DELIVERY.DOC(groupCode)).get();
29
+ return doc.exists;
30
+ }
31
+ catch (error) {
32
+ console.error("Error checking code:", error);
33
+ return false;
34
+ }
35
+ };
36
+ const createDelivery = async (db) => {
37
+ try {
38
+ let groupCode = createCode(4);
39
+ let exists = await checkCode(db, groupCode);
40
+ while (exists || groupCode.toLowerCase() === "aaaa") {
41
+ groupCode = createCode(4);
42
+ exists = await checkCode(db, groupCode);
43
+ }
44
+ return groupCode;
45
+ }
46
+ catch (error) {
47
+ console.error("Error creating activity:", error);
48
+ return null;
49
+ }
50
+ };
51
+ // Validation helpers
52
+ const validateCode = (code) => {
53
+ return (typeof code === "string" &&
54
+ code.length > 0 &&
55
+ code.length <= 50 &&
56
+ /^[a-zA-Z0-9_-]+$/.test(code));
57
+ };
58
+ const validateTarget = (target) => {
59
+ return target === "teacher" || target === "reviewer";
60
+ };
61
+ const parseScore = (scoreValue) => {
62
+ if (scoreValue === null || scoreValue === undefined || scoreValue === "") {
63
+ return null;
64
+ }
65
+ if (typeof scoreValue === "number") {
66
+ return isFinite(scoreValue) ? scoreValue : null;
67
+ }
68
+ if (typeof scoreValue === "string") {
69
+ // Valid integer (positive or negative)
70
+ if (/^-?\d+$/.test(scoreValue)) {
71
+ return parseInt(scoreValue, 10);
72
+ }
73
+ // Valid float
74
+ if (/^-?\d*\.\d+$/.test(scoreValue)) {
75
+ return parseFloat(scoreValue);
76
+ }
77
+ }
78
+ return null;
79
+ };
80
+ export function addQtiTeacherEndpoints(app, specificImplementation = new BaseImplementation()) {
81
+ /**
82
+ * @openapi
83
+ * /teacher/startGroupDelivery:
84
+ * post:
85
+ * tags:
86
+ * - Teacher
87
+ * summary: Create a new group delivery/activity
88
+ * description: Creates a new activity code for group delivery sessions.
89
+ * requestBody:
90
+ * required: true
91
+ * content:
92
+ * application/json:
93
+ * schema:
94
+ * type: object
95
+ * required:
96
+ * - assessmentId
97
+ * properties:
98
+ * assessmentId:
99
+ * type: string
100
+ * description: The ID of the assessment for this group delivery
101
+ * responses:
102
+ * 200:
103
+ * description: Returns the created activity code
104
+ * content:
105
+ * application/json:
106
+ * schema:
107
+ * type: object
108
+ * properties:
109
+ * groupCode:
110
+ * type: string
111
+ * description: The generated activity code
112
+ * assessmentId:
113
+ * type: string
114
+ * description: The assessment ID
115
+ * 401:
116
+ * description: Unauthorized
117
+ * 500:
118
+ * description: Internal server error
119
+ */
120
+ app.post("/teacher/startGroupDelivery", [
121
+ heavyOperationLimit,
122
+ body("assessmentId").notEmpty().withMessage("Assessment ID is required"),
123
+ handleValidationErrors,
124
+ requireTeacherAuth,
125
+ ], async (req, res) => {
126
+ try {
127
+ const { assessmentId } = req.body;
128
+ const db = getDatabase(req.appId);
129
+ const code = await createDelivery(db);
130
+ if (!code) {
131
+ return sendError(res, 500, "Failed to create activity code");
132
+ }
133
+ const firestore = getFirestore();
134
+ // Create the activity document
135
+ await firestore.doc(db.GROUP_DELIVERY.DOC(code)).set({
136
+ groupCode: code,
137
+ assessmentId,
138
+ created: new Date().toISOString(),
139
+ teacherId: req.user.uid,
140
+ appId: req.appId,
141
+ });
142
+ return sendSuccess(res, { groupCode: code, assessmentId }, "Group delivery created successfully");
143
+ }
144
+ catch (error) {
145
+ console.error("Error creating group delivery:", error);
146
+ return sendError(res, 500, "Failed to create group delivery");
147
+ }
148
+ });
149
+ /**
150
+ * @openapi
151
+ * /teacher/assessments:
152
+ * get:
153
+ * tags:
154
+ * - Teacher
155
+ * summary: Get all assessments
156
+ * responses:
157
+ * 200:
158
+ * description: Returns all assessments
159
+ * 401:
160
+ * description: Unauthorized
161
+ * 404:
162
+ * description: Application not found
163
+ * 500:
164
+ * description: Internal server error
165
+ */
166
+ app.get("/teacher/assessments", [generalRateLimit, requireTeacherAuth], async (req, res) => {
167
+ try {
168
+ const db = getDatabase(req.appId);
169
+ const application = await getApplication(db);
170
+ if (!application) {
171
+ return sendError(res, 404, "Application not found");
172
+ }
173
+ return sendSuccess(res, { assessments: application.assessments });
174
+ }
175
+ catch (error) {
176
+ console.error("Error retrieving assessments:", error);
177
+ return sendError(res, 500, "Failed to retrieve assessments");
178
+ }
179
+ });
180
+ /**
181
+ * @openapi
182
+ * /teacher/students:
183
+ * get:
184
+ * tags:
185
+ * - Teacher
186
+ * summary: Get planned sessions
187
+ * responses:
188
+ * 200:
189
+ * description: Returns planned sessions
190
+ * 401:
191
+ * description: Unauthorized
192
+ * 500:
193
+ * description: Internal server error
194
+ */
195
+ app.get("/teacher/students", [generalRateLimit, requireTeacherAuth], async (req, res) => {
196
+ try {
197
+ const db = getDatabase(req.appId);
198
+ const studentSessionInfos = await getPlannedStudents(db, req.user.uid);
199
+ return sendSuccess(res, studentSessionInfos);
200
+ }
201
+ catch (error) {
202
+ console.error("Error retrieving planned students:", error);
203
+ return sendError(res, 500, "Failed to retrieve planned students");
204
+ }
205
+ });
206
+ app.get("/teacher/assessment/:assessmentId/itemStats", [
207
+ generalRateLimit,
208
+ param("assessmentId").custom((value) => {
209
+ if (!validateAssessmentId(value)) {
210
+ throw new Error("Invalid assessment ID format");
211
+ }
212
+ return true;
213
+ }),
214
+ query("role").optional().isIn(["teacher", "reviewer"]),
215
+ handleValidationErrors,
216
+ requireTeacherAuth, // You might want to make this more generic
217
+ ], async (req, res) => {
218
+ try {
219
+ const { assessmentId } = req.params;
220
+ const role = req.query.role || "teacher"; // default to teacher
221
+ const db = getDatabase(req.appId);
222
+ const studentSessionInfos = await getItemsStats(db, req.user.uid, assessmentId, role);
223
+ return sendSuccess(res, studentSessionInfos);
224
+ }
225
+ catch (error) {
226
+ console.error("Error retrieving item stats:", error);
227
+ return sendError(res, 500, "Failed to retrieve item statistics");
228
+ }
229
+ });
230
+ /**
231
+ * @openapi
232
+ * /teacher/log:
233
+ * post:
234
+ * tags:
235
+ * - Teacher
236
+ * summary: Log teacher activity
237
+ * description: Logs teacher activity data for debugging and analytics purposes.
238
+ * requestBody:
239
+ * required: true
240
+ * content:
241
+ * application/json:
242
+ * schema:
243
+ * type: object
244
+ * required:
245
+ * - type
246
+ * - data
247
+ * properties:
248
+ * type:
249
+ * type: string
250
+ * minLength: 1
251
+ * maxLength: 100
252
+ * description: The type/category of the log entry
253
+ * data:
254
+ * description: Any data to be logged
255
+ * responses:
256
+ * 200:
257
+ * description: Successfully logged the data.
258
+ * 400:
259
+ * description: Invalid request data
260
+ * 401:
261
+ * description: Unauthorized
262
+ * 500:
263
+ * description: Internal server error
264
+ */
265
+ app.post("/teacher/log", [
266
+ generalRateLimit,
267
+ body("type")
268
+ .isString()
269
+ .trim()
270
+ .isLength({ min: 1, max: 100 })
271
+ .withMessage("Type must be a string between 1-100 characters"),
272
+ body("data").exists().withMessage("Data field is required"),
273
+ handleValidationErrors,
274
+ requireTeacherAuth,
275
+ ], async (req, res) => {
276
+ try {
277
+ const { type, data } = req.body;
278
+ const db = getDatabase(req.appId);
279
+ // Create log entry with timestamp and teacher ID
280
+ const logEntry = {
281
+ type,
282
+ data,
283
+ timestamp: new Date().toISOString(),
284
+ teacherId: req.user.uid,
285
+ appId: req.appId,
286
+ };
287
+ const firestore = getFirestore();
288
+ await firestore
289
+ .doc(db.TEACHER.LOG.DOC(req.user.uid, dateId()))
290
+ .set(logEntry);
291
+ return sendSuccess(res, undefined, "Log entry created successfully");
292
+ }
293
+ catch (error) {
294
+ console.error("Error creating log entry:", error);
295
+ return sendError(res, 500, "Failed to create log entry");
296
+ }
297
+ });
298
+ /**
299
+ * @openapi
300
+ * /teacher/plan:
301
+ * post:
302
+ * tags:
303
+ * - Teacher
304
+ * summary: Plan sessions
305
+ * requestBody:
306
+ * required: true
307
+ * content:
308
+ * application/json:
309
+ * schema:
310
+ * type: object
311
+ * properties:
312
+ * count:
313
+ * type: number
314
+ * minimum: 1
315
+ * maximum: 100
316
+ * assessmentIds:
317
+ * type: array
318
+ * items:
319
+ * type: string
320
+ * maxItems: 20
321
+ * responses:
322
+ * 200:
323
+ * description: Returns planned sessions
324
+ * 400:
325
+ * description: Invalid request data
326
+ * 401:
327
+ * description: Unauthorized
328
+ * 404:
329
+ * description: Application not found
330
+ * 500:
331
+ * description: Internal server error
332
+ */
333
+ app.post("/teacher/plan", [
334
+ heavyOperationLimit,
335
+ body("count")
336
+ .optional()
337
+ .isInt({ min: 1, max: 100 })
338
+ .withMessage("Count must be between 1 and 100"),
339
+ body("assessmentIds")
340
+ .optional()
341
+ .isArray({ max: 20 })
342
+ .withMessage("Assessment IDs must be an array with max 20 items"),
343
+ body("assessmentIds.*")
344
+ .optional()
345
+ .custom((value) => {
346
+ if (!validateAssessmentId(value)) {
347
+ throw new Error("Invalid assessment ID format");
348
+ }
349
+ return true;
350
+ }),
351
+ handleValidationErrors,
352
+ requireTeacherAuth,
353
+ ], async (req, res) => {
354
+ try {
355
+ const { count = 1, assessmentIds = [] } = req.body;
356
+ const db = getDatabase(req.appId);
357
+ const application = await getApplication(db);
358
+ if (!application) {
359
+ return sendError(res, 404, "Application not found");
360
+ }
361
+ const assessmentsToPlan = application.assessments.filter((a) => {
362
+ return (assessmentIds.length === 0 || assessmentIds.includes(a.assessmentId));
363
+ });
364
+ if (assessmentIds.length > 0 && assessmentsToPlan.length === 0) {
365
+ return sendError(res, 400, "No valid assessments found for the provided IDs");
366
+ }
367
+ const studentSessionInfos = await createSession(req.user.uid, db, count, assessmentsToPlan);
368
+ return sendSuccess(res, studentSessionInfos, "Sessions planned successfully");
369
+ }
370
+ catch (error) {
371
+ console.error("Error planning sessions:", error);
372
+ return sendError(res, 500, "Failed to plan sessions");
373
+ }
374
+ });
375
+ /**
376
+ * @openapi
377
+ * /teacher/planByIdentification:
378
+ * post:
379
+ * tags:
380
+ * - Teacher
381
+ * summary: Plan sessions by identification e.g. student number
382
+ * requestBody:
383
+ * required: true
384
+ * content:
385
+ * application/json:
386
+ * schema:
387
+ * type: object
388
+ * required:
389
+ * - identifications
390
+ * properties:
391
+ * identifications:
392
+ * type: array
393
+ * items:
394
+ * type: string
395
+ * minItems: 1
396
+ * maxItems: 100
397
+ * assessmentIds:
398
+ * type: array
399
+ * items:
400
+ * type: string
401
+ * maxItems: 20
402
+ * responses:
403
+ * 200:
404
+ * description: Returns planned sessions
405
+ * 400:
406
+ * description: Invalid request data
407
+ * 401:
408
+ * description: Unauthorized
409
+ * 404:
410
+ * description: Application not found
411
+ * 500:
412
+ * description: Internal server error
413
+ */
414
+ app.post("/teacher/planByIdentification", [
415
+ heavyOperationLimit,
416
+ body("identifications")
417
+ .isArray({ min: 1, max: 100 })
418
+ .withMessage("Identifications must be an array with 1-100 items"),
419
+ body("identifications.*")
420
+ .isString()
421
+ .trim()
422
+ .isLength({ min: 1, max: 100 })
423
+ .withMessage("Each identification must be 1-100 characters"),
424
+ body("assessmentIds")
425
+ .optional()
426
+ .isArray({ max: 20 })
427
+ .withMessage("Assessment IDs must be an array with max 20 items"),
428
+ body("assessmentIds.*")
429
+ .optional()
430
+ .custom((value) => {
431
+ if (!validateAssessmentId(value)) {
432
+ throw new Error("Invalid assessment ID format");
433
+ }
434
+ return true;
435
+ }),
436
+ handleValidationErrors,
437
+ requireTeacherAuth,
438
+ ], async (req, res) => {
439
+ try {
440
+ const { identifications, assessmentIds = [] } = req.body;
441
+ const db = getDatabase(req.appId);
442
+ const application = await getApplication(db);
443
+ if (!application) {
444
+ return sendError(res, 404, "Application not found");
445
+ }
446
+ const assessmentsToPlan = application.assessments.filter((a) => {
447
+ return (assessmentIds.length === 0 || assessmentIds.includes(a.assessmentId));
448
+ });
449
+ if (assessmentIds.length > 0 && assessmentsToPlan.length === 0) {
450
+ return sendError(res, 400, "No valid assessments found for the provided IDs");
451
+ }
452
+ const studentSessionInfos = await createSessionByIdentifications(req.user.uid, db, identifications.map((i) => ({ identifier: i })), "not_started", assessmentsToPlan);
453
+ return sendSuccess(res, studentSessionInfos, "Sessions planned by identification successfully");
454
+ }
455
+ catch (error) {
456
+ console.error("Error planning sessions by identification:", error);
457
+ return sendError(res, 500, "Failed to plan sessions by identification");
458
+ }
459
+ });
460
+ /**
461
+ * @openapi
462
+ * /teacher/assessment/:assessmentId/itemStats/:itemIdentifier:
463
+ * post:
464
+ * tags:
465
+ * - Teacher
466
+ * summary: Set score for an item response
467
+ * parameters:
468
+ * - in: path
469
+ * name: itemIdentifier
470
+ * required: true
471
+ * schema:
472
+ * type: string
473
+ * - in: path
474
+ * name: assessmentId
475
+ * required: true
476
+ * schema:
477
+ * type: string
478
+ * requestBody:
479
+ * required: true
480
+ * content:
481
+ * application/json:
482
+ * schema:
483
+ * type: object
484
+ * required:
485
+ * - responseId
486
+ * properties:
487
+ * responseId:
488
+ * type: string
489
+ * scoreExternal:
490
+ * oneOf:
491
+ * - type: number
492
+ * - type: string
493
+ * - type: 'null'
494
+ * target:
495
+ * type: string
496
+ * enum: [teacher, reviewer]
497
+ * responses:
498
+ * 200:
499
+ * description: Score updated successfully
500
+ * 400:
501
+ * description: Invalid request data
502
+ * 401:
503
+ * description: Unauthorized
504
+ * 500:
505
+ * description: Internal server error
506
+ */
507
+ app.post("/teacher/assessment/:assessmentId/itemStats/:itemIdentifier", [
508
+ generalRateLimit,
509
+ param("itemIdentifier")
510
+ .isString()
511
+ .trim()
512
+ .isLength({ min: 1, max: 100 })
513
+ .matches(/^[a-zA-Z0-9_.-]+$/)
514
+ .withMessage("Item identifier must be alphanumeric with dots, dashes and underscores only"),
515
+ param("assessmentId").custom((value) => {
516
+ if (!validateAssessmentId(value)) {
517
+ throw new Error("Invalid assessment ID format");
518
+ }
519
+ return true;
520
+ }),
521
+ body("responseId")
522
+ .isString()
523
+ .trim()
524
+ .isLength({ min: 1, max: 100 })
525
+ .withMessage("Response ID is required and must be 1-100 characters"),
526
+ body("target")
527
+ .optional()
528
+ .custom((value) => {
529
+ if (value && !validateTarget(value)) {
530
+ throw new Error('Target must be either "teacher" or "reviewer"');
531
+ }
532
+ return true;
533
+ }),
534
+ handleValidationErrors,
535
+ requireTeacherAuth,
536
+ ], async (req, res) => {
537
+ try {
538
+ const { assessmentId, itemIdentifier } = req.params;
539
+ const { responseId, scoreExternal: scoreRawValue, target = "teacher", } = req.body;
540
+ if (!validateTarget(target)) {
541
+ return sendError(res, 400, 'Invalid target. Must be "teacher" or "reviewer"');
542
+ }
543
+ const scoreExternal = parseScore(scoreRawValue);
544
+ const db = getDatabase(req.appId);
545
+ await updateItemStatResponseScore(db, req.user.uid, assessmentId, itemIdentifier, responseId, scoreExternal, target);
546
+ return sendSuccess(res, undefined, "Score updated successfully");
547
+ }
548
+ catch (error) {
549
+ console.error("Error updating item score:", error);
550
+ return sendError(res, 500, "Failed to update item score");
551
+ }
552
+ });
553
+ /**
554
+ * @openapi
555
+ * /teacher/access:
556
+ * post:
557
+ * tags:
558
+ * - Teacher
559
+ * summary: Check if the teacher has access
560
+ * description: Authenticates the teacher and checks if they have access.
561
+ * responses:
562
+ * 200:
563
+ * description: Returns if the teacher has access.
564
+ * 401:
565
+ * description: Unauthorized
566
+ */
567
+ app.post("/teacher/access", [generalRateLimit, requireTeacherAuth], async (req, res) => {
568
+ return sendSuccess(res, { hasAccess: true });
569
+ });
570
+ /**
571
+ * @openapi
572
+ * /teacher/session/update:
573
+ * post:
574
+ * tags:
575
+ * - Teacher
576
+ * summary: Update a student session
577
+ * description: Updates the session information for a student.
578
+ * requestBody:
579
+ * required: true
580
+ * content:
581
+ * application/json:
582
+ * schema:
583
+ * type: object
584
+ * required:
585
+ * - assessmentId
586
+ * - code
587
+ * - session
588
+ * properties:
589
+ * assessmentId:
590
+ * type: string
591
+ * code:
592
+ * type: string
593
+ * session:
594
+ * $ref: '#/components/schemas/SessionInfoTeacher'
595
+ * responses:
596
+ * 200:
597
+ * description: Successfully updated the session.
598
+ * 400:
599
+ * description: Invalid request data
600
+ * 401:
601
+ * description: Unauthorized
602
+ * 500:
603
+ * description: Internal server error
604
+ */
605
+ app.post("/teacher/session/update", [
606
+ generalRateLimit,
607
+ body("assessmentId").custom((value) => {
608
+ if (!validateAssessmentId(value)) {
609
+ throw new Error("Invalid assessment ID format");
610
+ }
611
+ return true;
612
+ }),
613
+ body("code").custom((value) => {
614
+ if (!validateCode(value)) {
615
+ throw new Error("Invalid code format");
616
+ }
617
+ return true;
618
+ }),
619
+ body("session").isObject().withMessage("Session must be an object"),
620
+ handleValidationErrors,
621
+ requireTeacherAuth,
622
+ ], async (req, res) => {
623
+ try {
624
+ const { assessmentId, code, session } = req.body;
625
+ const db = getDatabase(req.appId);
626
+ const firestore = getFirestore();
627
+ const batch = firestore.batch();
628
+ await Promise.all([
629
+ specificImplementation.updatePlannedSession(req.user.uid, code, session, batch),
630
+ updateStudentSessionState(db, code, session.assessmentId, session.sessionState, batch),
631
+ ]);
632
+ await batch.commit();
633
+ return sendSuccess(res, undefined, "Session updated successfully");
634
+ }
635
+ catch (error) {
636
+ console.error("Error updating session:", error);
637
+ return sendError(res, 500, "Failed to update session");
638
+ }
639
+ });
640
+ /**
641
+ * @openapi
642
+ * /teacher/session/{code}:
643
+ * delete:
644
+ * tags:
645
+ * - Teacher
646
+ * summary: Delete a students session
647
+ * description: Deletes a specific session for a student.
648
+ * parameters:
649
+ * - in: path
650
+ * name: code
651
+ * required: true
652
+ * schema:
653
+ * type: string
654
+ * description: The session code.
655
+ * responses:
656
+ * 200:
657
+ * description: Successfully deleted the session.
658
+ * 400:
659
+ * description: Invalid code format
660
+ * 401:
661
+ * description: Unauthorized
662
+ * 500:
663
+ * description: Internal server error
664
+ */
665
+ app.delete("/teacher/session/:code", [
666
+ generalRateLimit,
667
+ param("code").custom((value) => {
668
+ if (!validateCode(value)) {
669
+ throw new Error("Invalid code format");
670
+ }
671
+ return true;
672
+ }),
673
+ handleValidationErrors,
674
+ requireTeacherAuth,
675
+ ], async (req, res) => {
676
+ try {
677
+ const { code } = req.params;
678
+ const db = getDatabase(req.appId);
679
+ await deleteStudent(db, req.user.uid, code);
680
+ return sendSuccess(res, undefined, "Session deleted successfully");
681
+ }
682
+ catch (error) {
683
+ console.error("Error deleting session:", error);
684
+ return sendError(res, 500, "Failed to delete session");
685
+ }
686
+ });
687
+ /**
688
+ * @openapi
689
+ * /teacher/session/reset:
690
+ * post:
691
+ * tags:
692
+ * - Teacher
693
+ * summary: Reset a teacher's session
694
+ * description: Resets the session information for a student.
695
+ * requestBody:
696
+ * required: true
697
+ * content:
698
+ * application/json:
699
+ * schema:
700
+ * type: object
701
+ * required:
702
+ * - code
703
+ * - assessmentId
704
+ * properties:
705
+ * code:
706
+ * type: string
707
+ * assessmentId:
708
+ * type: string
709
+ * responses:
710
+ * 200:
711
+ * description: Successfully reset the session.
712
+ * 400:
713
+ * description: Invalid request data
714
+ * 401:
715
+ * description: Unauthorized
716
+ * 500:
717
+ * description: Internal server error
718
+ */
719
+ app.post("/teacher/session/reset", [
720
+ generalRateLimit,
721
+ body("code").custom((value) => {
722
+ if (!validateCode(value)) {
723
+ throw new Error("Invalid code format");
724
+ }
725
+ return true;
726
+ }),
727
+ body("assessmentId").custom((value) => {
728
+ if (!validateAssessmentId(value)) {
729
+ throw new Error("Invalid assessment ID format");
730
+ }
731
+ return true;
732
+ }),
733
+ handleValidationErrors,
734
+ requireTeacherAuth,
735
+ ], async (req, res) => {
736
+ try {
737
+ const { code, assessmentId } = req.body;
738
+ const db = getDatabase(req.appId);
739
+ await resetSession(db, req.user.uid, code, assessmentId);
740
+ return sendSuccess(res, undefined, "Session reset successfully");
741
+ }
742
+ catch (error) {
743
+ console.error("Error resetting session:", error);
744
+ return sendError(res, 500, "Failed to reset session");
745
+ }
746
+ });
747
+ /**
748
+ * @openapi
749
+ * /teacher/student/update:
750
+ * post:
751
+ * tags:
752
+ * - Teacher
753
+ * summary: Update student information
754
+ * description: Updates student information for a specific session.
755
+ * requestBody:
756
+ * required: true
757
+ * content:
758
+ * application/json:
759
+ * schema:
760
+ * type: object
761
+ * required:
762
+ * - code
763
+ * - studentId
764
+ * properties:
765
+ * code:
766
+ * type: string
767
+ * studentId:
768
+ * type: string
769
+ * responses:
770
+ * 200:
771
+ * description: Successfully updated student information.
772
+ * 400:
773
+ * description: Invalid request data
774
+ * 401:
775
+ * description: Unauthorized
776
+ * 500:
777
+ * description: Internal server error
778
+ */
779
+ app.post("/teacher/student/update", [
780
+ generalRateLimit,
781
+ body("code").custom((value) => {
782
+ if (!validateCode(value)) {
783
+ throw new Error("Invalid code format");
784
+ }
785
+ return true;
786
+ }),
787
+ body("identification")
788
+ .isString()
789
+ .trim()
790
+ .isLength({ min: 1, max: 100 })
791
+ .withMessage("identification must be 1-100 characters"),
792
+ handleValidationErrors,
793
+ requireTeacherAuth,
794
+ ], async (req, res) => {
795
+ try {
796
+ const { code, identification } = req.body;
797
+ const db = getDatabase(req.appId);
798
+ await updateStudent(db, req.user.uid, code, identification);
799
+ return sendSuccess(res, undefined, "Student updated successfully");
800
+ }
801
+ catch (error) {
802
+ console.error("Error updating student:", error);
803
+ return sendError(res, 500, "Failed to update student");
804
+ }
805
+ });
806
+ }
807
+ //# sourceMappingURL=qti-teacher.js.map