@studious-lms/server 1.4.2 → 1.4.4

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 (152) hide show
  1. package/.coderabbit.yaml +4 -1
  2. package/.vscode/settings.json +3 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +68 -29
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/fileUpload.d.ts +10 -2
  7. package/dist/lib/fileUpload.d.ts.map +1 -1
  8. package/dist/lib/fileUpload.js +38 -6
  9. package/dist/lib/fileUpload.js.map +1 -1
  10. package/dist/middleware/auth.d.ts.map +1 -1
  11. package/dist/middleware/auth.js +18 -5
  12. package/dist/middleware/auth.js.map +1 -1
  13. package/dist/models/auth.d.ts +19 -14
  14. package/dist/models/auth.d.ts.map +1 -1
  15. package/dist/models/auth.js +53 -30
  16. package/dist/models/auth.js.map +1 -1
  17. package/dist/models/class.d.ts +3 -1
  18. package/dist/models/class.d.ts.map +1 -1
  19. package/dist/models/class.js +16 -4
  20. package/dist/models/class.js.map +1 -1
  21. package/dist/models/notification.d.ts +11 -10
  22. package/dist/models/notification.d.ts.map +1 -1
  23. package/dist/models/notification.js +13 -9
  24. package/dist/models/notification.js.map +1 -1
  25. package/dist/models/worksheet.d.ts +379 -7
  26. package/dist/models/worksheet.d.ts.map +1 -1
  27. package/dist/models/worksheet.js +168 -2
  28. package/dist/models/worksheet.js.map +1 -1
  29. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  30. package/dist/pipelines/aiLabChat.js +11 -3
  31. package/dist/pipelines/aiLabChat.js.map +1 -1
  32. package/dist/pipelines/aiLabChatContract.d.ts +3 -3
  33. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  34. package/dist/pipelines/aiNewtonChat.js +25 -14
  35. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  36. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  37. package/dist/pipelines/gradeWorksheet.js +10 -3
  38. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  39. package/dist/pipelines/pdfToWorksheet.d.ts +12 -0
  40. package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -0
  41. package/dist/pipelines/pdfToWorksheet.js +155 -0
  42. package/dist/pipelines/pdfToWorksheet.js.map +1 -0
  43. package/dist/routers/_app.d.ts +264 -16
  44. package/dist/routers/_app.d.ts.map +1 -1
  45. package/dist/routers/class.d.ts +2 -1
  46. package/dist/routers/class.d.ts.map +1 -1
  47. package/dist/routers/class.js +3 -3
  48. package/dist/routers/class.js.map +1 -1
  49. package/dist/routers/labChat.d.ts +6 -0
  50. package/dist/routers/labChat.d.ts.map +1 -1
  51. package/dist/routers/labChat.js +5 -3
  52. package/dist/routers/labChat.js.map +1 -1
  53. package/dist/routers/message.d.ts +31 -0
  54. package/dist/routers/message.d.ts.map +1 -1
  55. package/dist/routers/message.js +27 -4
  56. package/dist/routers/message.js.map +1 -1
  57. package/dist/routers/newtonChat.d.ts +6 -0
  58. package/dist/routers/newtonChat.d.ts.map +1 -1
  59. package/dist/routers/newtonChat.js +5 -4
  60. package/dist/routers/newtonChat.js.map +1 -1
  61. package/dist/routers/notifications.d.ts +9 -2
  62. package/dist/routers/notifications.d.ts.map +1 -1
  63. package/dist/routers/notifications.js +4 -4
  64. package/dist/routers/notifications.js.map +1 -1
  65. package/dist/routers/worksheet.d.ts +78 -5
  66. package/dist/routers/worksheet.d.ts.map +1 -1
  67. package/dist/routers/worksheet.js +126 -17
  68. package/dist/routers/worksheet.js.map +1 -1
  69. package/dist/services/auth.d.ts +1 -1
  70. package/dist/services/auth.d.ts.map +1 -1
  71. package/dist/services/auth.js +19 -12
  72. package/dist/services/auth.js.map +1 -1
  73. package/dist/services/class.d.ts +1 -1
  74. package/dist/services/class.d.ts.map +1 -1
  75. package/dist/services/class.js +6 -3
  76. package/dist/services/class.js.map +1 -1
  77. package/dist/services/file.d.ts.map +1 -1
  78. package/dist/services/file.js +7 -2
  79. package/dist/services/file.js.map +1 -1
  80. package/dist/services/labChat.d.ts +6 -0
  81. package/dist/services/labChat.d.ts.map +1 -1
  82. package/dist/services/labChat.js +30 -18
  83. package/dist/services/labChat.js.map +1 -1
  84. package/dist/services/message.d.ts +30 -0
  85. package/dist/services/message.d.ts.map +1 -1
  86. package/dist/services/message.js +97 -21
  87. package/dist/services/message.js.map +1 -1
  88. package/dist/services/newtonChat.d.ts +6 -1
  89. package/dist/services/newtonChat.d.ts.map +1 -1
  90. package/dist/services/newtonChat.js +36 -18
  91. package/dist/services/newtonChat.js.map +1 -1
  92. package/dist/services/notification.d.ts +12 -5
  93. package/dist/services/notification.d.ts.map +1 -1
  94. package/dist/services/notification.js +24 -9
  95. package/dist/services/notification.js.map +1 -1
  96. package/dist/services/studentProgress.d.ts.map +1 -1
  97. package/dist/services/studentProgress.js +6 -3
  98. package/dist/services/studentProgress.js.map +1 -1
  99. package/dist/services/worksheet.d.ts +36 -15
  100. package/dist/services/worksheet.d.ts.map +1 -1
  101. package/dist/services/worksheet.js +186 -54
  102. package/dist/services/worksheet.js.map +1 -1
  103. package/dist/trpc.d.ts.map +1 -1
  104. package/dist/trpc.js +11 -17
  105. package/dist/trpc.js.map +1 -1
  106. package/dist/utils/inference.d.ts +10 -1
  107. package/dist/utils/inference.d.ts.map +1 -1
  108. package/dist/utils/inference.js +186 -50
  109. package/dist/utils/inference.js.map +1 -1
  110. package/package.json +1 -1
  111. package/prisma/migrations/20260415103500_add_session_type/migration.sql +33 -0
  112. package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +19 -0
  113. package/prisma/schema.prisma +27 -1
  114. package/scripts/test-pre-push.ts +6 -2
  115. package/src/index.ts +83 -30
  116. package/src/lib/fileUpload.ts +55 -5
  117. package/src/middleware/auth.ts +19 -4
  118. package/src/models/auth.ts +59 -29
  119. package/src/models/class.ts +15 -4
  120. package/src/models/notification.ts +13 -7
  121. package/src/models/worksheet.ts +203 -0
  122. package/src/pipelines/aiLabChat.ts +9 -1
  123. package/src/pipelines/aiNewtonChat.ts +24 -14
  124. package/src/pipelines/gradeWorksheet.ts +8 -0
  125. package/src/pipelines/pdfToWorksheet.ts +177 -0
  126. package/src/routers/class.ts +2 -2
  127. package/src/routers/labChat.ts +3 -1
  128. package/src/routers/message.ts +38 -1
  129. package/src/routers/newtonChat.ts +4 -2
  130. package/src/routers/notifications.ts +2 -2
  131. package/src/routers/worksheet.ts +174 -19
  132. package/src/services/auth.ts +26 -12
  133. package/src/services/class.ts +4 -1
  134. package/src/services/file.ts +6 -0
  135. package/src/services/labChat.ts +42 -18
  136. package/src/services/message.ts +163 -18
  137. package/src/services/newtonChat.ts +49 -18
  138. package/src/services/notification.ts +27 -8
  139. package/src/services/studentProgress.ts +4 -1
  140. package/src/services/worksheet.ts +252 -51
  141. package/src/trpc.ts +12 -18
  142. package/src/utils/inference.ts +280 -56
  143. package/tests/globalSetup.ts +13 -4
  144. package/tests/pipelines/pdfToWorksheet.test.ts +176 -0
  145. package/tests/routers/auth.test.ts +120 -17
  146. package/tests/routers/class.test.ts +7 -1
  147. package/tests/routers/message.test.ts +236 -9
  148. package/tests/routers/newtonChat.test.ts +81 -0
  149. package/tests/routers/notification.test.ts +12 -0
  150. package/tests/routers/worksheet.test.ts +194 -0
  151. package/tests/setup.ts +22 -5
  152. package/tests/utils/inference.test.ts +255 -0
@@ -5,10 +5,13 @@
5
5
  import { TRPCError } from "@trpc/server";
6
6
  import { GenerationStatus, type WorksheetQuestionType } from "@prisma/client";
7
7
  import { prisma } from "../lib/prisma.js";
8
+ import { getFileUrl } from "../lib/fileUpload.js";
8
9
  import { cancelGradePipeline, regradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
10
+ import { pdfToWorksheet } from "../pipelines/pdfToWorksheet.js";
9
11
  import {
10
- findWorksheetById,
11
12
  findWorksheetByIdMinimal,
13
+ findWorksheetByIdForClassMember,
14
+ findWorksheetByIdForTeacher,
12
15
  findWorksheetsByClassId,
13
16
  createWorksheet,
14
17
  updateWorksheet,
@@ -17,30 +20,63 @@ import {
17
20
  createWorksheetQuestion,
18
21
  updateWorksheetQuestion,
19
22
  deleteWorksheetQuestion,
20
- findSubmissionById,
23
+ findSubmissionByIdForWorksheetAccess,
21
24
  findOrCreateWorksheetResponse,
22
- findWorksheetResponseWithResponses,
23
- findWorksheetQuestionById,
25
+ findWorksheetResponseByIdForTeacher,
26
+ findWorksheetResponseWithResponsesForStudent,
27
+ findWorksheetQuestionByIdForTeacher,
28
+ findWorksheetQuestionByIdInWorksheet,
24
29
  updateStudentQuestionProgress,
25
30
  createStudentQuestionProgress,
26
31
  findStudentQuestionProgress,
27
32
  updateStudentQuestionProgressForGrading,
28
33
  createStudentQuestionProgressForGrading,
29
34
  createComment,
35
+ findStudentQuestionProgressByIdForTeacher,
36
+ findStudentQuestionProgressByIdForTeacherAndResponse,
30
37
  } from "../models/worksheet.js";
31
38
 
32
39
  type MCQOptions = { id: string; text: string; isCorrect: boolean }[];
33
40
 
41
+ const sanitizeQuestionOptionsForStudent = (options: unknown) => {
42
+ if (!Array.isArray(options)) return options;
43
+
44
+ return options.map((option) => {
45
+ if (!option || typeof option !== "object") return option;
46
+
47
+ const { isCorrect: _isCorrect, ...rest } = option as Record<string, unknown>;
48
+ return rest;
49
+ });
50
+ };
51
+
52
+ const sanitizeWorksheetForStudent = <T extends {
53
+ questions: Array<Record<string, unknown>>;
54
+ }>(worksheet: T) => ({
55
+ ...worksheet,
56
+ questions: worksheet.questions.map((question) => ({
57
+ ...question,
58
+ answer: "",
59
+ markScheme: null,
60
+ options: sanitizeQuestionOptionsForStudent(question.options),
61
+ })),
62
+ });
63
+
34
64
  /** Get worksheet with questions and mark schemes. */
35
- export async function getWorksheet(worksheetId: string) {
36
- const worksheet = await findWorksheetById(worksheetId);
37
- if (!worksheet) {
65
+ export async function getWorksheet(worksheetId: string, userId: string) {
66
+ const teacherWorksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
67
+ if (teacherWorksheet) {
68
+ return teacherWorksheet;
69
+ }
70
+
71
+ const studentWorksheet = await findWorksheetByIdForClassMember(worksheetId, userId);
72
+ if (!studentWorksheet) {
38
73
  throw new TRPCError({
39
- code: "NOT_FOUND",
40
- message: "Worksheet not found",
74
+ code: "FORBIDDEN",
75
+ message: "You do not have access to this worksheet",
41
76
  });
42
77
  }
43
- return worksheet;
78
+
79
+ return sanitizeWorksheetForStudent(studentWorksheet);
44
80
  }
45
81
 
46
82
  /** Check if worksheet exists. */
@@ -59,12 +95,27 @@ export async function listWorksheets(classId: string) {
59
95
 
60
96
  export async function updateWorksheetRecord(
61
97
  worksheetId: string,
98
+ userId: string,
62
99
  data: { name?: string }
63
100
  ) {
101
+ const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
102
+ if (!worksheet) {
103
+ throw new TRPCError({
104
+ code: "FORBIDDEN",
105
+ message: "Only class teachers can update worksheets",
106
+ });
107
+ }
64
108
  return updateWorksheet(worksheetId, data);
65
109
  }
66
110
 
67
- export async function deleteWorksheetRecord(worksheetId: string) {
111
+ export async function deleteWorksheetRecord(worksheetId: string, userId: string) {
112
+ const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
113
+ if (!worksheet) {
114
+ throw new TRPCError({
115
+ code: "FORBIDDEN",
116
+ message: "Only class teachers can delete worksheets",
117
+ });
118
+ }
68
119
  return deleteWorksheet(worksheetId);
69
120
  }
70
121
 
@@ -72,8 +123,68 @@ export async function createWorksheetRecord(classId: string, name: string) {
72
123
  return createWorksheet({ classId, name });
73
124
  }
74
125
 
126
+ /**
127
+ * Create a worksheet by converting a PDF (URL) into structured questions.
128
+ * Uses the `pdfToWorksheet` pipeline to do inference + persistence.
129
+ */
130
+ export async function createWorksheetFromPdf(
131
+ classId: string,
132
+ url: string
133
+ ) {
134
+ const created = await pdfToWorksheet(url, classId);
135
+ if (!created) {
136
+ throw new TRPCError({
137
+ code: "INTERNAL_SERVER_ERROR",
138
+ message: "Failed to create worksheet from PDF",
139
+ });
140
+ }
141
+ return created;
142
+ }
143
+
144
+ /**
145
+ * Create a worksheet from a PDF that was uploaded through the direct-upload flow.
146
+ */
147
+ export async function createWorksheetFromUploadedPdf(
148
+ classId: string,
149
+ fileId: string,
150
+ userId: string
151
+ ) {
152
+ const file = await prisma.file.findFirst({
153
+ where: {
154
+ id: fileId,
155
+ userId,
156
+ uploadStatus: "COMPLETED",
157
+ },
158
+ select: {
159
+ id: true,
160
+ path: true,
161
+ type: true,
162
+ },
163
+ });
164
+
165
+ if (!file) {
166
+ throw new TRPCError({
167
+ code: "NOT_FOUND",
168
+ message: "Uploaded file not found or not ready",
169
+ });
170
+ }
171
+
172
+ const isPdf =
173
+ file.type === "application/pdf" || file.path.toLowerCase().endsWith(".pdf");
174
+ if (!isPdf) {
175
+ throw new TRPCError({
176
+ code: "BAD_REQUEST",
177
+ message: "Only PDF files can be converted into worksheets",
178
+ });
179
+ }
180
+
181
+ const signedUrl = await getFileUrl(file.path);
182
+ return createWorksheetFromPdf(classId, signedUrl);
183
+ }
184
+
75
185
  export async function addQuestionToWorksheet(
76
186
  worksheetId: string,
187
+ userId: string,
77
188
  data: {
78
189
  question: string;
79
190
  answer: string;
@@ -83,11 +194,11 @@ export async function addQuestionToWorksheet(
83
194
  type: WorksheetQuestionType;
84
195
  }
85
196
  ) {
86
- const worksheet = await findWorksheetByIdMinimal(worksheetId);
197
+ const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
87
198
  if (!worksheet) {
88
199
  throw new TRPCError({
89
- code: "NOT_FOUND",
90
- message: "Worksheet not found",
200
+ code: "FORBIDDEN",
201
+ message: "Only class teachers can modify worksheet questions",
91
202
  });
92
203
  }
93
204
  return createWorksheetQuestion({
@@ -98,15 +209,16 @@ export async function addQuestionToWorksheet(
98
209
 
99
210
  export async function reorderWorksheetQuestions(
100
211
  worksheetId: string,
212
+ userId: string,
101
213
  movedId: string,
102
214
  position: "before" | "after",
103
215
  targetId: string
104
216
  ) {
105
- const worksheet = await findWorksheetByIdMinimal(worksheetId);
217
+ const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
106
218
  if (!worksheet) {
107
219
  throw new TRPCError({
108
- code: "NOT_FOUND",
109
- message: "Worksheet not found",
220
+ code: "FORBIDDEN",
221
+ message: "Only class teachers can reorder worksheet questions",
110
222
  });
111
223
  }
112
224
 
@@ -158,6 +270,7 @@ export async function reorderWorksheetQuestions(
158
270
  export async function updateWorksheetQuestionRecord(
159
271
  worksheetId: string,
160
272
  questionId: string,
273
+ userId: string,
161
274
  data: {
162
275
  question?: string;
163
276
  answer?: string;
@@ -167,11 +280,11 @@ export async function updateWorksheetQuestionRecord(
167
280
  type?: WorksheetQuestionType;
168
281
  }
169
282
  ) {
170
- const worksheet = await findWorksheetByIdMinimal(worksheetId);
171
- if (!worksheet) {
283
+ const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);
284
+ if (!question || question.worksheetId !== worksheetId) {
172
285
  throw new TRPCError({
173
- code: "NOT_FOUND",
174
- message: "Worksheet not found",
286
+ code: "FORBIDDEN",
287
+ message: "Only class teachers can update worksheet questions",
175
288
  });
176
289
  }
177
290
  return updateWorksheetQuestion(questionId, data);
@@ -179,13 +292,14 @@ export async function updateWorksheetQuestionRecord(
179
292
 
180
293
  export async function deleteWorksheetQuestionRecord(
181
294
  worksheetId: string,
182
- questionId: string
295
+ questionId: string,
296
+ userId: string
183
297
  ) {
184
- const worksheet = await findWorksheetByIdMinimal(worksheetId);
185
- if (!worksheet) {
298
+ const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);
299
+ if (!question || question.worksheetId !== worksheetId) {
186
300
  throw new TRPCError({
187
- code: "NOT_FOUND",
188
- message: "Worksheet not found",
301
+ code: "FORBIDDEN",
302
+ message: "Only class teachers can delete worksheet questions",
189
303
  });
190
304
  }
191
305
  return deleteWorksheetQuestion(questionId);
@@ -193,13 +307,27 @@ export async function deleteWorksheetQuestionRecord(
193
307
 
194
308
  export async function getWorksheetSubmission(
195
309
  worksheetId: string,
196
- submissionId: string
310
+ submissionId: string,
311
+ userId: string
197
312
  ) {
198
- const submission = await findSubmissionById(submissionId);
313
+ const worksheet = await findWorksheetByIdForClassMember(worksheetId, userId);
314
+ if (!worksheet) {
315
+ throw new TRPCError({
316
+ code: "FORBIDDEN",
317
+ message: "You do not have access to this worksheet",
318
+ });
319
+ }
320
+
321
+ const submission = await findSubmissionByIdForWorksheetAccess(
322
+ submissionId,
323
+ worksheetId,
324
+ worksheet.classId,
325
+ userId
326
+ );
199
327
  if (!submission) {
200
328
  throw new TRPCError({
201
- code: "NOT_FOUND",
202
- message: "Submission not found",
329
+ code: "FORBIDDEN",
330
+ message: "You do not have access to this worksheet submission",
203
331
  });
204
332
  }
205
333
  return findOrCreateWorksheetResponse(
@@ -215,25 +343,36 @@ export async function answerWorksheetQuestion(
215
343
  response: string,
216
344
  userId: string
217
345
  ) {
218
- const worksheetResponse = await findWorksheetResponseWithResponses(
346
+ const worksheetResponse = await findWorksheetResponseWithResponsesForStudent(
219
347
  worksheetResponseId,
348
+ userId,
220
349
  questionId
221
350
  );
222
351
  if (!worksheetResponse) {
352
+ throw new TRPCError({
353
+ code: "FORBIDDEN",
354
+ message: "You can only answer your own worksheet responses",
355
+ });
356
+ }
357
+
358
+ const question = await findWorksheetQuestionByIdInWorksheet(
359
+ questionId,
360
+ worksheetResponse.worksheetId
361
+ );
362
+ if (!question) {
223
363
  throw new TRPCError({
224
364
  code: "NOT_FOUND",
225
- message: "Worksheet response not found",
365
+ message: "Worksheet question not found",
226
366
  });
227
367
  }
228
368
 
229
- const question = await findWorksheetQuestionById(questionId);
230
369
  const isMarkableByAlgo =
231
- question?.type === "MULTIPLE_CHOICE" || question?.type === "TRUE_FALSE";
232
- const marksAwardedIfCorrect = question?.points || 0;
370
+ question.type === "MULTIPLE_CHOICE" || question.type === "TRUE_FALSE";
371
+ const marksAwardedIfCorrect = question.points || 0;
233
372
  const correctAnswer = isMarkableByAlgo
234
- ? question?.type === "MULTIPLE_CHOICE"
235
- ? (question?.options as MCQOptions)?.find((o) => o.isCorrect)?.id
236
- : question?.answer?.toString()
373
+ ? question.type === "MULTIPLE_CHOICE"
374
+ ? (question.options as MCQOptions)?.find((o) => o.isCorrect)?.id
375
+ : question.answer?.toString()
237
376
  : null;
238
377
 
239
378
  const existingResponse = worksheetResponse.responses[0];
@@ -273,21 +412,46 @@ export async function answerWorksheetQuestion(
273
412
 
274
413
  export async function cancelGrading(
275
414
  worksheetResponseId: string,
276
- progressId: string
415
+ progressId: string,
416
+ userId: string
277
417
  ) {
418
+ const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(
419
+ progressId,
420
+ worksheetResponseId,
421
+ userId
422
+ );
423
+ if (!progress) {
424
+ throw new TRPCError({
425
+ code: "FORBIDDEN",
426
+ message: "Only class teachers can cancel worksheet grading",
427
+ });
428
+ }
278
429
  return cancelGradePipeline(worksheetResponseId, progressId);
279
430
  }
280
431
 
281
432
  export async function regradeQuestion(
282
433
  worksheetResponseId: string,
283
- progressId: string
434
+ progressId: string,
435
+ userId: string
284
436
  ) {
437
+ const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(
438
+ progressId,
439
+ worksheetResponseId,
440
+ userId
441
+ );
442
+ if (!progress) {
443
+ throw new TRPCError({
444
+ code: "FORBIDDEN",
445
+ message: "Only class teachers can regrade worksheet answers",
446
+ });
447
+ }
285
448
  return regradeWorksheetPipeline(worksheetResponseId, progressId);
286
449
  }
287
450
 
288
451
  export async function gradeAnswer(
289
452
  questionId: string,
290
453
  studentWorksheetResponseId: string,
454
+ userId: string,
291
455
  data: {
292
456
  responseId?: string;
293
457
  response?: string;
@@ -297,7 +461,44 @@ export async function gradeAnswer(
297
461
  points?: number;
298
462
  }
299
463
  ) {
464
+ const worksheetResponse = await findWorksheetResponseByIdForTeacher(
465
+ studentWorksheetResponseId,
466
+ userId
467
+ );
468
+ if (!worksheetResponse) {
469
+ throw new TRPCError({
470
+ code: "FORBIDDEN",
471
+ message: "Only class teachers can grade worksheet answers",
472
+ });
473
+ }
474
+
475
+ const question = await findWorksheetQuestionByIdInWorksheet(
476
+ questionId,
477
+ worksheetResponse.worksheetId
478
+ );
479
+ if (!question) {
480
+ throw new TRPCError({
481
+ code: "NOT_FOUND",
482
+ message: "Worksheet question not found",
483
+ });
484
+ }
485
+
300
486
  if (data.responseId) {
487
+ const existingProgress = await findStudentQuestionProgressByIdForTeacher(
488
+ data.responseId,
489
+ userId
490
+ );
491
+ if (
492
+ !existingProgress ||
493
+ existingProgress.studentWorksheetResponseId !== studentWorksheetResponseId ||
494
+ existingProgress.questionId !== questionId
495
+ ) {
496
+ throw new TRPCError({
497
+ code: "FORBIDDEN",
498
+ message: "Only class teachers can grade worksheet answers",
499
+ });
500
+ }
501
+
301
502
  return updateStudentQuestionProgressForGrading(data.responseId, {
302
503
  isCorrect: data.isCorrect,
303
504
  feedback: data.feedback,
@@ -306,17 +507,6 @@ export async function gradeAnswer(
306
507
  });
307
508
  }
308
509
 
309
- const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
310
- where: { id: studentWorksheetResponseId },
311
- select: { studentId: true },
312
- });
313
- if (!worksheetResponse) {
314
- throw new TRPCError({
315
- code: "NOT_FOUND",
316
- message: "Student worksheet response not found",
317
- });
318
- }
319
-
320
510
  const existing = await findStudentQuestionProgress(
321
511
  worksheetResponse.studentId,
322
512
  questionId,
@@ -350,6 +540,17 @@ export async function addCommentToResponse(
350
540
  comment: string,
351
541
  authorId: string
352
542
  ) {
543
+ const progress = await findStudentQuestionProgressByIdForTeacher(
544
+ responseId,
545
+ authorId
546
+ );
547
+ if (!progress) {
548
+ throw new TRPCError({
549
+ code: "FORBIDDEN",
550
+ message: "Only class teachers can comment on worksheet responses",
551
+ });
552
+ }
553
+
353
554
  return createComment({
354
555
  studentQuestionProgressId: responseId,
355
556
  content: comment,
package/src/trpc.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  import { initTRPC, TRPCError } from '@trpc/server';
2
2
  import { ZodError } from 'zod';
3
3
  import { logger } from './utils/logger.js';
4
- import { prisma } from './lib/prisma.js';
5
4
  import { createLoggingMiddleware } from './middleware/logging.js';
6
5
  import { createAuthMiddleware } from './middleware/auth.js';
7
6
  import { Request, Response } from 'express';
8
7
  import { z } from 'zod';
9
8
  import { handlePrismaError, PrismaErrorInfo } from './utils/prismaErrorHandler.js';
10
- import { generalLimiter } from './middleware/security.js';
9
+ import { findUserBySessionToken } from './models/auth.js';
11
10
 
12
11
  interface CreateContextOptions {
13
12
  req: Request;
@@ -26,22 +25,17 @@ export type Context = {
26
25
 
27
26
  export const createTRPCContext = async (opts: CreateContextOptions): Promise<Context> => {
28
27
  const { req, res } = opts;
29
-
30
- // Get user from session/token
31
- const token = req.headers.authorization?.split(' ')[1];
32
- const user = token ? await prisma.user.findFirst({
33
- where: {
34
- sessions: {
35
- some: {
36
- id: token
37
- }
38
- },
39
- },
40
- select: {
41
- id: true,
42
- }
43
- }) : null;
44
-
28
+
29
+ const userHeader = req.headers["x-user"];
30
+ const tokenFromUserHeader = typeof userHeader === "string" ? userHeader : userHeader?.[0];
31
+ const authorizationHeader = req.headers.authorization;
32
+ const tokenFromAuthorization =
33
+ typeof authorizationHeader === "string" && authorizationHeader.startsWith("Bearer ")
34
+ ? authorizationHeader.slice(7)
35
+ : null;
36
+ const token = tokenFromUserHeader ?? tokenFromAuthorization;
37
+ const user = token ? await findUserBySessionToken(token) : null;
38
+
45
39
  return {
46
40
  req,
47
41
  res,