@studious-lms/server 1.4.1 → 1.4.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 (248) hide show
  1. package/.coderabbit.yaml +4 -1
  2. package/.env.example +6 -0
  3. package/.env.test.example +2 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +101 -76
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/config/cors.d.ts +16 -0
  8. package/dist/lib/config/cors.d.ts.map +1 -0
  9. package/dist/lib/config/cors.js +75 -0
  10. package/dist/lib/config/cors.js.map +1 -0
  11. package/dist/lib/config/env.d.ts +14 -0
  12. package/dist/lib/config/env.d.ts.map +1 -1
  13. package/dist/lib/config/env.js +9 -2
  14. package/dist/lib/config/env.js.map +1 -1
  15. package/dist/lib/fileUpload.d.ts +10 -2
  16. package/dist/lib/fileUpload.d.ts.map +1 -1
  17. package/dist/lib/fileUpload.js +38 -6
  18. package/dist/lib/fileUpload.js.map +1 -1
  19. package/dist/lib/prisma.d.ts +14 -2
  20. package/dist/lib/prisma.d.ts.map +1 -1
  21. package/dist/lib/prisma.js +27 -8
  22. package/dist/lib/prisma.js.map +1 -1
  23. package/dist/middleware/auth.d.ts.map +1 -1
  24. package/dist/middleware/auth.js +18 -5
  25. package/dist/middleware/auth.js.map +1 -1
  26. package/dist/middleware/security.d.ts.map +1 -1
  27. package/dist/middleware/security.js +3 -3
  28. package/dist/middleware/security.js.map +1 -1
  29. package/dist/models/agenda.d.ts +16 -16
  30. package/dist/models/announcement.d.ts +59 -23
  31. package/dist/models/announcement.d.ts.map +1 -1
  32. package/dist/models/assignment.d.ts +363 -276
  33. package/dist/models/assignment.d.ts.map +1 -1
  34. package/dist/models/attendance.d.ts +63 -21
  35. package/dist/models/attendance.d.ts.map +1 -1
  36. package/dist/models/auth.d.ts +115 -26
  37. package/dist/models/auth.d.ts.map +1 -1
  38. package/dist/models/auth.js +53 -30
  39. package/dist/models/auth.js.map +1 -1
  40. package/dist/models/class.d.ts +115 -65
  41. package/dist/models/class.d.ts.map +1 -1
  42. package/dist/models/class.js +16 -4
  43. package/dist/models/class.js.map +1 -1
  44. package/dist/models/comment.d.ts +52 -16
  45. package/dist/models/comment.d.ts.map +1 -1
  46. package/dist/models/conversation.d.ts +46 -16
  47. package/dist/models/conversation.d.ts.map +1 -1
  48. package/dist/models/event.d.ts +107 -53
  49. package/dist/models/event.d.ts.map +1 -1
  50. package/dist/models/file.d.ts +213 -165
  51. package/dist/models/file.d.ts.map +1 -1
  52. package/dist/models/folder.d.ts +161 -77
  53. package/dist/models/folder.d.ts.map +1 -1
  54. package/dist/models/labChat.d.ts +73 -31
  55. package/dist/models/labChat.d.ts.map +1 -1
  56. package/dist/models/marketing.d.ts +25 -7
  57. package/dist/models/marketing.d.ts.map +1 -1
  58. package/dist/models/message.d.ts +31 -13
  59. package/dist/models/message.d.ts.map +1 -1
  60. package/dist/models/newtonChat.d.ts +34 -10
  61. package/dist/models/newtonChat.d.ts.map +1 -1
  62. package/dist/models/notification.d.ts +29 -10
  63. package/dist/models/notification.d.ts.map +1 -1
  64. package/dist/models/notification.js +13 -9
  65. package/dist/models/notification.js.map +1 -1
  66. package/dist/models/section.d.ts +71 -23
  67. package/dist/models/section.d.ts.map +1 -1
  68. package/dist/models/user.d.ts +27 -9
  69. package/dist/models/user.d.ts.map +1 -1
  70. package/dist/models/worksheet.d.ts +606 -106
  71. package/dist/models/worksheet.d.ts.map +1 -1
  72. package/dist/models/worksheet.js +168 -2
  73. package/dist/models/worksheet.js.map +1 -1
  74. package/dist/pipelines/aiLabChat.d.ts +22 -2
  75. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  76. package/dist/pipelines/aiLabChat.js +133 -95
  77. package/dist/pipelines/aiLabChat.js.map +1 -1
  78. package/dist/pipelines/aiLabChatContract.d.ts +25 -25
  79. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  80. package/dist/pipelines/aiNewtonChat.js +25 -14
  81. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  82. package/dist/pipelines/gradeWorksheet.d.ts +8 -8
  83. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  84. package/dist/pipelines/gradeWorksheet.js +12 -5
  85. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  86. package/dist/pipelines/labChatPrompt.d.ts +27 -0
  87. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  88. package/dist/pipelines/labChatPrompt.js +143 -69
  89. package/dist/pipelines/labChatPrompt.js.map +1 -1
  90. package/dist/routers/_app.d.ts +1579 -1257
  91. package/dist/routers/_app.d.ts.map +1 -1
  92. package/dist/routers/agenda.d.ts +16 -16
  93. package/dist/routers/announcement.d.ts +19 -19
  94. package/dist/routers/assignment.d.ts +307 -291
  95. package/dist/routers/assignment.d.ts.map +1 -1
  96. package/dist/routers/assignment.js +3 -2
  97. package/dist/routers/assignment.js.map +1 -1
  98. package/dist/routers/attendance.d.ts +7 -7
  99. package/dist/routers/auth.d.ts +1 -1
  100. package/dist/routers/class.d.ts +79 -72
  101. package/dist/routers/class.d.ts.map +1 -1
  102. package/dist/routers/class.js +3 -3
  103. package/dist/routers/class.js.map +1 -1
  104. package/dist/routers/comment.d.ts +6 -6
  105. package/dist/routers/conversation.d.ts +11 -11
  106. package/dist/routers/event.d.ts +35 -35
  107. package/dist/routers/file.d.ts +12 -12
  108. package/dist/routers/folder.d.ts +54 -54
  109. package/dist/routers/labChat.d.ts +18 -12
  110. package/dist/routers/labChat.d.ts.map +1 -1
  111. package/dist/routers/labChat.js +5 -3
  112. package/dist/routers/labChat.js.map +1 -1
  113. package/dist/routers/marketing.d.ts +2 -2
  114. package/dist/routers/message.d.ts +33 -2
  115. package/dist/routers/message.d.ts.map +1 -1
  116. package/dist/routers/message.js +27 -4
  117. package/dist/routers/message.js.map +1 -1
  118. package/dist/routers/newtonChat.d.ts +7 -1
  119. package/dist/routers/newtonChat.d.ts.map +1 -1
  120. package/dist/routers/newtonChat.js +5 -4
  121. package/dist/routers/newtonChat.js.map +1 -1
  122. package/dist/routers/notifications.d.ts +13 -6
  123. package/dist/routers/notifications.d.ts.map +1 -1
  124. package/dist/routers/notifications.js +4 -4
  125. package/dist/routers/notifications.js.map +1 -1
  126. package/dist/routers/section.d.ts +7 -7
  127. package/dist/routers/studentProgress.d.ts +86 -0
  128. package/dist/routers/studentProgress.d.ts.map +1 -1
  129. package/dist/routers/studentProgress.js +14 -4
  130. package/dist/routers/studentProgress.js.map +1 -1
  131. package/dist/routers/user.d.ts +1 -1
  132. package/dist/routers/worksheet.d.ts +64 -62
  133. package/dist/routers/worksheet.d.ts.map +1 -1
  134. package/dist/routers/worksheet.js +18 -16
  135. package/dist/routers/worksheet.js.map +1 -1
  136. package/dist/seedDatabase.d.ts +1 -1
  137. package/dist/services/agenda.d.ts +16 -16
  138. package/dist/services/announcement.d.ts +8 -8
  139. package/dist/services/assignment.d.ts +299 -283
  140. package/dist/services/assignment.d.ts.map +1 -1
  141. package/dist/services/assignment.js +24 -5
  142. package/dist/services/assignment.js.map +1 -1
  143. package/dist/services/attendance.d.ts +7 -7
  144. package/dist/services/auth.d.ts +2 -2
  145. package/dist/services/auth.d.ts.map +1 -1
  146. package/dist/services/auth.js +19 -12
  147. package/dist/services/auth.js.map +1 -1
  148. package/dist/services/class.d.ts +74 -68
  149. package/dist/services/class.d.ts.map +1 -1
  150. package/dist/services/class.js +6 -3
  151. package/dist/services/class.js.map +1 -1
  152. package/dist/services/comment.d.ts +6 -6
  153. package/dist/services/conversation.d.ts +11 -11
  154. package/dist/services/event.d.ts +31 -31
  155. package/dist/services/file.d.ts +12 -12
  156. package/dist/services/file.d.ts.map +1 -1
  157. package/dist/services/file.js +7 -2
  158. package/dist/services/file.js.map +1 -1
  159. package/dist/services/folder.d.ts +52 -52
  160. package/dist/services/labChat.d.ts +18 -12
  161. package/dist/services/labChat.d.ts.map +1 -1
  162. package/dist/services/labChat.js +30 -18
  163. package/dist/services/labChat.js.map +1 -1
  164. package/dist/services/marketing.d.ts +2 -2
  165. package/dist/services/message.d.ts +30 -0
  166. package/dist/services/message.d.ts.map +1 -1
  167. package/dist/services/message.js +97 -21
  168. package/dist/services/message.js.map +1 -1
  169. package/dist/services/newtonChat.d.ts +6 -1
  170. package/dist/services/newtonChat.d.ts.map +1 -1
  171. package/dist/services/newtonChat.js +36 -18
  172. package/dist/services/newtonChat.js.map +1 -1
  173. package/dist/services/notification.d.ts +16 -9
  174. package/dist/services/notification.d.ts.map +1 -1
  175. package/dist/services/notification.js +24 -9
  176. package/dist/services/notification.js.map +1 -1
  177. package/dist/services/section.d.ts +6 -6
  178. package/dist/services/studentProgress.d.ts +75 -0
  179. package/dist/services/studentProgress.d.ts.map +1 -1
  180. package/dist/services/studentProgress.js +300 -107
  181. package/dist/services/studentProgress.js.map +1 -1
  182. package/dist/services/worksheet.d.ts +64 -64
  183. package/dist/services/worksheet.d.ts.map +1 -1
  184. package/dist/services/worksheet.js +138 -54
  185. package/dist/services/worksheet.js.map +1 -1
  186. package/dist/trpc.d.ts.map +1 -1
  187. package/dist/trpc.js +11 -17
  188. package/dist/trpc.js.map +1 -1
  189. package/dist/utils/inference.d.ts +9 -12
  190. package/dist/utils/inference.d.ts.map +1 -1
  191. package/dist/utils/inference.js +135 -95
  192. package/dist/utils/inference.js.map +1 -1
  193. package/package.json +1 -1
  194. package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +14 -0
  195. package/prisma/migrations/20260415103500_add_session_type/migration.sql +33 -0
  196. package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +19 -0
  197. package/prisma/schema.prisma +41 -1
  198. package/scripts/test-pre-push.ts +6 -2
  199. package/src/index.ts +119 -79
  200. package/src/lib/config/cors.ts +96 -0
  201. package/src/lib/config/env.ts +12 -1
  202. package/src/lib/fileUpload.ts +55 -5
  203. package/src/lib/prisma.ts +25 -6
  204. package/src/middleware/auth.ts +19 -4
  205. package/src/middleware/security.ts +1 -1
  206. package/src/models/auth.ts +59 -29
  207. package/src/models/class.ts +15 -4
  208. package/src/models/notification.ts +13 -7
  209. package/src/models/worksheet.ts +202 -0
  210. package/src/pipelines/aiLabChat.ts +183 -104
  211. package/src/pipelines/aiNewtonChat.ts +24 -14
  212. package/src/pipelines/gradeWorksheet.ts +10 -2
  213. package/src/pipelines/labChatPrompt.ts +196 -68
  214. package/src/routers/assignment.ts +1 -0
  215. package/src/routers/class.ts +2 -2
  216. package/src/routers/labChat.ts +3 -1
  217. package/src/routers/message.ts +38 -1
  218. package/src/routers/newtonChat.ts +4 -2
  219. package/src/routers/notifications.ts +2 -2
  220. package/src/routers/studentProgress.ts +25 -1
  221. package/src/routers/worksheet.ts +31 -19
  222. package/src/services/assignment.ts +30 -2
  223. package/src/services/auth.ts +26 -12
  224. package/src/services/class.ts +4 -1
  225. package/src/services/file.ts +6 -0
  226. package/src/services/labChat.ts +42 -18
  227. package/src/services/message.ts +163 -18
  228. package/src/services/newtonChat.ts +49 -18
  229. package/src/services/notification.ts +27 -8
  230. package/src/services/studentProgress.ts +425 -121
  231. package/src/services/worksheet.ts +191 -51
  232. package/src/trpc.ts +12 -18
  233. package/src/utils/inference.ts +205 -112
  234. package/tests/globalSetup.ts +13 -4
  235. package/tests/lib/cors.test.ts +103 -0
  236. package/tests/pipelines/aiLabChat.test.ts +64 -84
  237. package/tests/routers/auth.test.ts +120 -17
  238. package/tests/routers/class.test.ts +7 -1
  239. package/tests/routers/message.test.ts +236 -9
  240. package/tests/routers/newtonChat.test.ts +81 -0
  241. package/tests/routers/notification.test.ts +12 -0
  242. package/tests/routers/studentProgress.test.ts +2 -31
  243. package/tests/routers/worksheet.test.ts +194 -0
  244. package/tests/setup.ts +22 -5
  245. package/tests/utils/aiLabChatPrompt.test.ts +114 -6
  246. package/tests/utils/inference.test.ts +218 -0
  247. package/tests/utils/studentProgress.test.ts +361 -0
  248. package/vitest.unit.config.ts +1 -0
@@ -7,8 +7,9 @@ import { GenerationStatus, type WorksheetQuestionType } from "@prisma/client";
7
7
  import { prisma } from "../lib/prisma.js";
8
8
  import { cancelGradePipeline, regradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
9
9
  import {
10
- findWorksheetById,
11
10
  findWorksheetByIdMinimal,
11
+ findWorksheetByIdForClassMember,
12
+ findWorksheetByIdForTeacher,
12
13
  findWorksheetsByClassId,
13
14
  createWorksheet,
14
15
  updateWorksheet,
@@ -17,30 +18,63 @@ import {
17
18
  createWorksheetQuestion,
18
19
  updateWorksheetQuestion,
19
20
  deleteWorksheetQuestion,
20
- findSubmissionById,
21
+ findSubmissionByIdForWorksheetAccess,
21
22
  findOrCreateWorksheetResponse,
22
- findWorksheetResponseWithResponses,
23
- findWorksheetQuestionById,
23
+ findWorksheetResponseByIdForTeacher,
24
+ findWorksheetResponseWithResponsesForStudent,
25
+ findWorksheetQuestionByIdForTeacher,
26
+ findWorksheetQuestionByIdInWorksheet,
24
27
  updateStudentQuestionProgress,
25
28
  createStudentQuestionProgress,
26
29
  findStudentQuestionProgress,
27
30
  updateStudentQuestionProgressForGrading,
28
31
  createStudentQuestionProgressForGrading,
29
32
  createComment,
33
+ findStudentQuestionProgressByIdForTeacher,
34
+ findStudentQuestionProgressByIdForTeacherAndResponse,
30
35
  } from "../models/worksheet.js";
31
36
 
32
37
  type MCQOptions = { id: string; text: string; isCorrect: boolean }[];
33
38
 
39
+ const sanitizeQuestionOptionsForStudent = (options: unknown) => {
40
+ if (!Array.isArray(options)) return options;
41
+
42
+ return options.map((option) => {
43
+ if (!option || typeof option !== "object") return option;
44
+
45
+ const { isCorrect: _isCorrect, ...rest } = option as Record<string, unknown>;
46
+ return rest;
47
+ });
48
+ };
49
+
50
+ const sanitizeWorksheetForStudent = <T extends {
51
+ questions: Array<Record<string, unknown>>;
52
+ }>(worksheet: T) => ({
53
+ ...worksheet,
54
+ questions: worksheet.questions.map((question) => ({
55
+ ...question,
56
+ answer: "",
57
+ markScheme: null,
58
+ options: sanitizeQuestionOptionsForStudent(question.options),
59
+ })),
60
+ });
61
+
34
62
  /** Get worksheet with questions and mark schemes. */
35
- export async function getWorksheet(worksheetId: string) {
36
- const worksheet = await findWorksheetById(worksheetId);
37
- if (!worksheet) {
63
+ export async function getWorksheet(worksheetId: string, userId: string) {
64
+ const teacherWorksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
65
+ if (teacherWorksheet) {
66
+ return teacherWorksheet;
67
+ }
68
+
69
+ const studentWorksheet = await findWorksheetByIdForClassMember(worksheetId, userId);
70
+ if (!studentWorksheet) {
38
71
  throw new TRPCError({
39
- code: "NOT_FOUND",
40
- message: "Worksheet not found",
72
+ code: "FORBIDDEN",
73
+ message: "You do not have access to this worksheet",
41
74
  });
42
75
  }
43
- return worksheet;
76
+
77
+ return sanitizeWorksheetForStudent(studentWorksheet);
44
78
  }
45
79
 
46
80
  /** Check if worksheet exists. */
@@ -59,12 +93,27 @@ export async function listWorksheets(classId: string) {
59
93
 
60
94
  export async function updateWorksheetRecord(
61
95
  worksheetId: string,
96
+ userId: string,
62
97
  data: { name?: string }
63
98
  ) {
99
+ const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
100
+ if (!worksheet) {
101
+ throw new TRPCError({
102
+ code: "FORBIDDEN",
103
+ message: "Only class teachers can update worksheets",
104
+ });
105
+ }
64
106
  return updateWorksheet(worksheetId, data);
65
107
  }
66
108
 
67
- export async function deleteWorksheetRecord(worksheetId: string) {
109
+ export async function deleteWorksheetRecord(worksheetId: string, userId: string) {
110
+ const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
111
+ if (!worksheet) {
112
+ throw new TRPCError({
113
+ code: "FORBIDDEN",
114
+ message: "Only class teachers can delete worksheets",
115
+ });
116
+ }
68
117
  return deleteWorksheet(worksheetId);
69
118
  }
70
119
 
@@ -74,6 +123,7 @@ export async function createWorksheetRecord(classId: string, name: string) {
74
123
 
75
124
  export async function addQuestionToWorksheet(
76
125
  worksheetId: string,
126
+ userId: string,
77
127
  data: {
78
128
  question: string;
79
129
  answer: string;
@@ -83,11 +133,11 @@ export async function addQuestionToWorksheet(
83
133
  type: WorksheetQuestionType;
84
134
  }
85
135
  ) {
86
- const worksheet = await findWorksheetByIdMinimal(worksheetId);
136
+ const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
87
137
  if (!worksheet) {
88
138
  throw new TRPCError({
89
- code: "NOT_FOUND",
90
- message: "Worksheet not found",
139
+ code: "FORBIDDEN",
140
+ message: "Only class teachers can modify worksheet questions",
91
141
  });
92
142
  }
93
143
  return createWorksheetQuestion({
@@ -98,15 +148,16 @@ export async function addQuestionToWorksheet(
98
148
 
99
149
  export async function reorderWorksheetQuestions(
100
150
  worksheetId: string,
151
+ userId: string,
101
152
  movedId: string,
102
153
  position: "before" | "after",
103
154
  targetId: string
104
155
  ) {
105
- const worksheet = await findWorksheetByIdMinimal(worksheetId);
156
+ const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
106
157
  if (!worksheet) {
107
158
  throw new TRPCError({
108
- code: "NOT_FOUND",
109
- message: "Worksheet not found",
159
+ code: "FORBIDDEN",
160
+ message: "Only class teachers can reorder worksheet questions",
110
161
  });
111
162
  }
112
163
 
@@ -158,6 +209,7 @@ export async function reorderWorksheetQuestions(
158
209
  export async function updateWorksheetQuestionRecord(
159
210
  worksheetId: string,
160
211
  questionId: string,
212
+ userId: string,
161
213
  data: {
162
214
  question?: string;
163
215
  answer?: string;
@@ -167,11 +219,11 @@ export async function updateWorksheetQuestionRecord(
167
219
  type?: WorksheetQuestionType;
168
220
  }
169
221
  ) {
170
- const worksheet = await findWorksheetByIdMinimal(worksheetId);
171
- if (!worksheet) {
222
+ const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);
223
+ if (!question || question.worksheetId !== worksheetId) {
172
224
  throw new TRPCError({
173
- code: "NOT_FOUND",
174
- message: "Worksheet not found",
225
+ code: "FORBIDDEN",
226
+ message: "Only class teachers can update worksheet questions",
175
227
  });
176
228
  }
177
229
  return updateWorksheetQuestion(questionId, data);
@@ -179,13 +231,14 @@ export async function updateWorksheetQuestionRecord(
179
231
 
180
232
  export async function deleteWorksheetQuestionRecord(
181
233
  worksheetId: string,
182
- questionId: string
234
+ questionId: string,
235
+ userId: string
183
236
  ) {
184
- const worksheet = await findWorksheetByIdMinimal(worksheetId);
185
- if (!worksheet) {
237
+ const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);
238
+ if (!question || question.worksheetId !== worksheetId) {
186
239
  throw new TRPCError({
187
- code: "NOT_FOUND",
188
- message: "Worksheet not found",
240
+ code: "FORBIDDEN",
241
+ message: "Only class teachers can delete worksheet questions",
189
242
  });
190
243
  }
191
244
  return deleteWorksheetQuestion(questionId);
@@ -193,13 +246,27 @@ export async function deleteWorksheetQuestionRecord(
193
246
 
194
247
  export async function getWorksheetSubmission(
195
248
  worksheetId: string,
196
- submissionId: string
249
+ submissionId: string,
250
+ userId: string
197
251
  ) {
198
- const submission = await findSubmissionById(submissionId);
252
+ const worksheet = await findWorksheetByIdForClassMember(worksheetId, userId);
253
+ if (!worksheet) {
254
+ throw new TRPCError({
255
+ code: "FORBIDDEN",
256
+ message: "You do not have access to this worksheet",
257
+ });
258
+ }
259
+
260
+ const submission = await findSubmissionByIdForWorksheetAccess(
261
+ submissionId,
262
+ worksheetId,
263
+ worksheet.classId,
264
+ userId
265
+ );
199
266
  if (!submission) {
200
267
  throw new TRPCError({
201
- code: "NOT_FOUND",
202
- message: "Submission not found",
268
+ code: "FORBIDDEN",
269
+ message: "You do not have access to this worksheet submission",
203
270
  });
204
271
  }
205
272
  return findOrCreateWorksheetResponse(
@@ -215,25 +282,36 @@ export async function answerWorksheetQuestion(
215
282
  response: string,
216
283
  userId: string
217
284
  ) {
218
- const worksheetResponse = await findWorksheetResponseWithResponses(
285
+ const worksheetResponse = await findWorksheetResponseWithResponsesForStudent(
219
286
  worksheetResponseId,
287
+ userId,
220
288
  questionId
221
289
  );
222
290
  if (!worksheetResponse) {
291
+ throw new TRPCError({
292
+ code: "FORBIDDEN",
293
+ message: "You can only answer your own worksheet responses",
294
+ });
295
+ }
296
+
297
+ const question = await findWorksheetQuestionByIdInWorksheet(
298
+ questionId,
299
+ worksheetResponse.worksheetId
300
+ );
301
+ if (!question) {
223
302
  throw new TRPCError({
224
303
  code: "NOT_FOUND",
225
- message: "Worksheet response not found",
304
+ message: "Worksheet question not found",
226
305
  });
227
306
  }
228
307
 
229
- const question = await findWorksheetQuestionById(questionId);
230
308
  const isMarkableByAlgo =
231
- question?.type === "MULTIPLE_CHOICE" || question?.type === "TRUE_FALSE";
232
- const marksAwardedIfCorrect = question?.points || 0;
309
+ question.type === "MULTIPLE_CHOICE" || question.type === "TRUE_FALSE";
310
+ const marksAwardedIfCorrect = question.points || 0;
233
311
  const correctAnswer = isMarkableByAlgo
234
- ? question?.type === "MULTIPLE_CHOICE"
235
- ? (question?.options as MCQOptions)?.find((o) => o.isCorrect)?.id
236
- : question?.answer?.toString()
312
+ ? question.type === "MULTIPLE_CHOICE"
313
+ ? (question.options as MCQOptions)?.find((o) => o.isCorrect)?.id
314
+ : question.answer?.toString()
237
315
  : null;
238
316
 
239
317
  const existingResponse = worksheetResponse.responses[0];
@@ -273,21 +351,46 @@ export async function answerWorksheetQuestion(
273
351
 
274
352
  export async function cancelGrading(
275
353
  worksheetResponseId: string,
276
- progressId: string
354
+ progressId: string,
355
+ userId: string
277
356
  ) {
357
+ const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(
358
+ progressId,
359
+ worksheetResponseId,
360
+ userId
361
+ );
362
+ if (!progress) {
363
+ throw new TRPCError({
364
+ code: "FORBIDDEN",
365
+ message: "Only class teachers can cancel worksheet grading",
366
+ });
367
+ }
278
368
  return cancelGradePipeline(worksheetResponseId, progressId);
279
369
  }
280
370
 
281
371
  export async function regradeQuestion(
282
372
  worksheetResponseId: string,
283
- progressId: string
373
+ progressId: string,
374
+ userId: string
284
375
  ) {
376
+ const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(
377
+ progressId,
378
+ worksheetResponseId,
379
+ userId
380
+ );
381
+ if (!progress) {
382
+ throw new TRPCError({
383
+ code: "FORBIDDEN",
384
+ message: "Only class teachers can regrade worksheet answers",
385
+ });
386
+ }
285
387
  return regradeWorksheetPipeline(worksheetResponseId, progressId);
286
388
  }
287
389
 
288
390
  export async function gradeAnswer(
289
391
  questionId: string,
290
392
  studentWorksheetResponseId: string,
393
+ userId: string,
291
394
  data: {
292
395
  responseId?: string;
293
396
  response?: string;
@@ -297,7 +400,44 @@ export async function gradeAnswer(
297
400
  points?: number;
298
401
  }
299
402
  ) {
403
+ const worksheetResponse = await findWorksheetResponseByIdForTeacher(
404
+ studentWorksheetResponseId,
405
+ userId
406
+ );
407
+ if (!worksheetResponse) {
408
+ throw new TRPCError({
409
+ code: "FORBIDDEN",
410
+ message: "Only class teachers can grade worksheet answers",
411
+ });
412
+ }
413
+
414
+ const question = await findWorksheetQuestionByIdInWorksheet(
415
+ questionId,
416
+ worksheetResponse.worksheetId
417
+ );
418
+ if (!question) {
419
+ throw new TRPCError({
420
+ code: "NOT_FOUND",
421
+ message: "Worksheet question not found",
422
+ });
423
+ }
424
+
300
425
  if (data.responseId) {
426
+ const existingProgress = await findStudentQuestionProgressByIdForTeacher(
427
+ data.responseId,
428
+ userId
429
+ );
430
+ if (
431
+ !existingProgress ||
432
+ existingProgress.studentWorksheetResponseId !== studentWorksheetResponseId ||
433
+ existingProgress.questionId !== questionId
434
+ ) {
435
+ throw new TRPCError({
436
+ code: "FORBIDDEN",
437
+ message: "Only class teachers can grade worksheet answers",
438
+ });
439
+ }
440
+
301
441
  return updateStudentQuestionProgressForGrading(data.responseId, {
302
442
  isCorrect: data.isCorrect,
303
443
  feedback: data.feedback,
@@ -306,17 +446,6 @@ export async function gradeAnswer(
306
446
  });
307
447
  }
308
448
 
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
449
  const existing = await findStudentQuestionProgress(
321
450
  worksheetResponse.studentId,
322
451
  questionId,
@@ -350,6 +479,17 @@ export async function addCommentToResponse(
350
479
  comment: string,
351
480
  authorId: string
352
481
  ) {
482
+ const progress = await findStudentQuestionProgressByIdForTeacher(
483
+ responseId,
484
+ authorId
485
+ );
486
+ if (!progress) {
487
+ throw new TRPCError({
488
+ code: "FORBIDDEN",
489
+ message: "Only class teachers can comment on worksheet responses",
490
+ });
491
+ }
492
+
353
493
  return createComment({
354
494
  studentQuestionProgressId: responseId,
355
495
  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,