@studious-lms/server 1.2.45 → 1.2.47

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 (241) hide show
  1. package/.env.example +45 -0
  2. package/.env.test.example +37 -0
  3. package/README.md +34 -7
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/clover.xml +12110 -0
  7. package/coverage/coverage-final.json +44 -0
  8. package/coverage/favicon.png +0 -0
  9. package/coverage/index.html +221 -0
  10. package/coverage/prettify.css +1 -0
  11. package/coverage/prettify.js +2 -0
  12. package/coverage/server/index.html +116 -0
  13. package/coverage/server/src/exportType.ts.html +109 -0
  14. package/coverage/server/src/index.html +161 -0
  15. package/coverage/server/src/index.ts.html +1702 -0
  16. package/coverage/server/src/instrument.ts.html +130 -0
  17. package/coverage/server/src/lib/config/env.ts.html +448 -0
  18. package/coverage/server/src/lib/config/index.html +116 -0
  19. package/coverage/server/src/lib/fileUpload.ts.html +1138 -0
  20. package/coverage/server/src/lib/googleCloudStorage.ts.html +334 -0
  21. package/coverage/server/src/lib/index.html +206 -0
  22. package/coverage/server/src/lib/jsonConversion.ts.html +2323 -0
  23. package/coverage/server/src/lib/jsonStyles.ts.html +193 -0
  24. package/coverage/server/src/lib/notificationHandler.ts.html +193 -0
  25. package/coverage/server/src/lib/pusher.ts.html +121 -0
  26. package/coverage/server/src/lib/thumbnailGenerator.ts.html +592 -0
  27. package/coverage/server/src/middleware/auth.ts.html +646 -0
  28. package/coverage/server/src/middleware/index.html +146 -0
  29. package/coverage/server/src/middleware/logging.ts.html +244 -0
  30. package/coverage/server/src/middleware/security.ts.html +271 -0
  31. package/coverage/server/src/routers/_app.ts.html +232 -0
  32. package/coverage/server/src/routers/agenda.ts.html +319 -0
  33. package/coverage/server/src/routers/announcement.ts.html +3481 -0
  34. package/coverage/server/src/routers/assignment.ts.html +7633 -0
  35. package/coverage/server/src/routers/attendance.ts.html +1030 -0
  36. package/coverage/server/src/routers/auth.ts.html +1081 -0
  37. package/coverage/server/src/routers/class.ts.html +3535 -0
  38. package/coverage/server/src/routers/comment.ts.html +991 -0
  39. package/coverage/server/src/routers/conversation.ts.html +982 -0
  40. package/coverage/server/src/routers/event.ts.html +1609 -0
  41. package/coverage/server/src/routers/file.ts.html +1144 -0
  42. package/coverage/server/src/routers/folder.ts.html +2797 -0
  43. package/coverage/server/src/routers/index.html +386 -0
  44. package/coverage/server/src/routers/labChat.ts.html +3073 -0
  45. package/coverage/server/src/routers/marketing.ts.html +340 -0
  46. package/coverage/server/src/routers/message.ts.html +1912 -0
  47. package/coverage/server/src/routers/notifications.ts.html +364 -0
  48. package/coverage/server/src/routers/section.ts.html +1120 -0
  49. package/coverage/server/src/routers/user.ts.html +862 -0
  50. package/coverage/server/src/routers/worksheet.ts.html +1729 -0
  51. package/coverage/server/src/trpc.ts.html +397 -0
  52. package/coverage/server/src/types/index.html +116 -0
  53. package/coverage/server/src/types/trpc.ts.html +127 -0
  54. package/coverage/server/src/utils/aiUser.ts.html +280 -0
  55. package/coverage/server/src/utils/email.ts.html +121 -0
  56. package/coverage/server/src/utils/generateInviteCode.ts.html +106 -0
  57. package/coverage/server/src/utils/index.html +206 -0
  58. package/coverage/server/src/utils/inference.ts.html +709 -0
  59. package/coverage/server/src/utils/logger.ts.html +664 -0
  60. package/coverage/server/src/utils/prismaErrorHandler.ts.html +907 -0
  61. package/coverage/server/src/utils/prismaWrapper.ts.html +355 -0
  62. package/coverage/server/vitest.config.ts.html +196 -0
  63. package/coverage/sort-arrow-sprite.png +0 -0
  64. package/coverage/sorter.js +210 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +83 -52
  67. package/dist/index.js.map +1 -1
  68. package/dist/instrument.js +15 -8
  69. package/dist/instrument.js.map +1 -1
  70. package/dist/lib/config/env.d.ts +169 -0
  71. package/dist/lib/config/env.d.ts.map +1 -0
  72. package/dist/lib/config/env.js +115 -0
  73. package/dist/lib/config/env.js.map +1 -0
  74. package/dist/lib/fileUpload.d.ts.map +1 -1
  75. package/dist/lib/fileUpload.js +5 -4
  76. package/dist/lib/fileUpload.js.map +1 -1
  77. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  78. package/dist/lib/googleCloudStorage.js +7 -8
  79. package/dist/lib/googleCloudStorage.js.map +1 -1
  80. package/dist/lib/jsonConversion.d.ts.map +1 -1
  81. package/dist/lib/jsonConversion.js +14 -16
  82. package/dist/lib/jsonConversion.js.map +1 -1
  83. package/dist/lib/notificationHandler.d.ts +2 -2
  84. package/dist/lib/prisma.d.ts +2 -2
  85. package/dist/lib/prisma.d.ts.map +1 -1
  86. package/dist/lib/prisma.js +22 -3
  87. package/dist/lib/prisma.js.map +1 -1
  88. package/dist/lib/pusher.d.ts.map +1 -1
  89. package/dist/lib/pusher.js +8 -7
  90. package/dist/lib/pusher.js.map +1 -1
  91. package/dist/middleware/auth.d.ts.map +1 -1
  92. package/dist/middleware/auth.js +7 -5
  93. package/dist/middleware/auth.js.map +1 -1
  94. package/dist/middleware/security.d.ts +5 -0
  95. package/dist/middleware/security.d.ts.map +1 -0
  96. package/dist/middleware/security.js +77 -0
  97. package/dist/middleware/security.js.map +1 -0
  98. package/dist/routers/_app.d.ts +368 -108
  99. package/dist/routers/_app.d.ts.map +1 -1
  100. package/dist/routers/_app.js +4 -2
  101. package/dist/routers/_app.js.map +1 -1
  102. package/dist/routers/agenda.d.ts.map +1 -1
  103. package/dist/routers/agenda.js +12 -9
  104. package/dist/routers/agenda.js.map +1 -1
  105. package/dist/routers/announcement.d.ts +8 -0
  106. package/dist/routers/announcement.d.ts.map +1 -1
  107. package/dist/routers/announcement.js +6 -4
  108. package/dist/routers/announcement.js.map +1 -1
  109. package/dist/routers/assignment.d.ts +17 -4
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/assignment.js +51 -19
  112. package/dist/routers/assignment.js.map +1 -1
  113. package/dist/routers/attendance.d.ts +1 -0
  114. package/dist/routers/attendance.d.ts.map +1 -1
  115. package/dist/routers/attendance.js +4 -4
  116. package/dist/routers/attendance.js.map +1 -1
  117. package/dist/routers/auth.d.ts +20 -0
  118. package/dist/routers/auth.d.ts.map +1 -1
  119. package/dist/routers/auth.js +132 -15
  120. package/dist/routers/auth.js.map +1 -1
  121. package/dist/routers/class.d.ts +10 -0
  122. package/dist/routers/class.d.ts.map +1 -1
  123. package/dist/routers/class.js +49 -5
  124. package/dist/routers/class.js.map +1 -1
  125. package/dist/routers/comment.d.ts +2 -0
  126. package/dist/routers/comment.d.ts.map +1 -1
  127. package/dist/routers/conversation.d.ts +2 -0
  128. package/dist/routers/conversation.d.ts.map +1 -1
  129. package/dist/routers/conversation.js +46 -31
  130. package/dist/routers/conversation.js.map +1 -1
  131. package/dist/routers/file.d.ts.map +1 -1
  132. package/dist/routers/file.js +30 -7
  133. package/dist/routers/file.js.map +1 -1
  134. package/dist/routers/labChat.d.ts +2 -0
  135. package/dist/routers/labChat.d.ts.map +1 -1
  136. package/dist/routers/labChat.js +5 -322
  137. package/dist/routers/labChat.js.map +1 -1
  138. package/dist/routers/marketing.d.ts +1 -1
  139. package/dist/routers/message.d.ts +1 -0
  140. package/dist/routers/message.d.ts.map +1 -1
  141. package/dist/routers/message.js +3 -2
  142. package/dist/routers/message.js.map +1 -1
  143. package/dist/routers/newtonChat.d.ts +55 -0
  144. package/dist/routers/newtonChat.d.ts.map +1 -0
  145. package/dist/routers/newtonChat.js +262 -0
  146. package/dist/routers/newtonChat.js.map +1 -0
  147. package/dist/routers/notifications.d.ts +4 -4
  148. package/dist/routers/section.d.ts +19 -4
  149. package/dist/routers/section.d.ts.map +1 -1
  150. package/dist/routers/section.js +26 -8
  151. package/dist/routers/section.js.map +1 -1
  152. package/dist/routers/user.d.ts.map +1 -1
  153. package/dist/routers/user.js +5 -4
  154. package/dist/routers/user.js.map +1 -1
  155. package/dist/routers/worksheet.d.ts +44 -41
  156. package/dist/routers/worksheet.d.ts.map +1 -1
  157. package/dist/routers/worksheet.js +25 -34
  158. package/dist/routers/worksheet.js.map +1 -1
  159. package/dist/seedDatabase.d.ts +1 -1
  160. package/dist/seedDatabase.js +275 -284
  161. package/dist/seedDatabase.js.map +1 -1
  162. package/dist/server/pipelines/aiLabChat.d.ts +21 -0
  163. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  164. package/dist/server/pipelines/aiLabChat.js +456 -0
  165. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  166. package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
  167. package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
  168. package/dist/server/pipelines/aiNewtonChat.js +280 -0
  169. package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
  170. package/dist/server/pipelines/gradeWorksheet.d.ts +15 -0
  171. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  172. package/dist/server/pipelines/gradeWorksheet.js +139 -0
  173. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  174. package/dist/trpc.d.ts.map +1 -1
  175. package/dist/trpc.js +2 -2
  176. package/dist/trpc.js.map +1 -1
  177. package/dist/utils/email.d.ts +9 -1
  178. package/dist/utils/email.d.ts.map +1 -1
  179. package/dist/utils/email.js +20 -5
  180. package/dist/utils/email.js.map +1 -1
  181. package/dist/utils/inference.d.ts +5 -0
  182. package/dist/utils/inference.d.ts.map +1 -1
  183. package/dist/utils/inference.js +71 -7
  184. package/dist/utils/inference.js.map +1 -1
  185. package/dist/utils/logger.d.ts.map +1 -1
  186. package/dist/utils/logger.js +3 -3
  187. package/dist/utils/logger.js.map +1 -1
  188. package/docker-compose.yml +14 -0
  189. package/package.json +13 -4
  190. package/prisma/schema.prisma +34 -5
  191. package/scripts/test-pre-push.ts +14 -0
  192. package/src/index.ts +98 -54
  193. package/src/instrument.ts +13 -6
  194. package/src/lib/config/env.ts +126 -0
  195. package/src/lib/fileUpload.ts +3 -2
  196. package/src/lib/googleCloudStorage.ts +6 -6
  197. package/src/lib/jsonConversion.ts +12 -14
  198. package/src/lib/prisma.ts +23 -2
  199. package/src/lib/pusher.ts +6 -5
  200. package/src/middleware/auth.ts +5 -3
  201. package/src/middleware/security.ts +80 -0
  202. package/src/routers/_app.ts +2 -0
  203. package/src/routers/agenda.ts +10 -7
  204. package/src/routers/announcement.ts +4 -2
  205. package/src/routers/assignment.ts +74 -41
  206. package/src/routers/attendance.ts +2 -2
  207. package/src/routers/auth.ts +143 -14
  208. package/src/routers/class.ts +52 -3
  209. package/src/routers/conversation.ts +49 -29
  210. package/src/routers/file.ts +29 -5
  211. package/src/routers/labChat.ts +3 -367
  212. package/src/routers/message.ts +1 -1
  213. package/src/routers/newtonChat.ts +299 -0
  214. package/src/routers/section.ts +26 -6
  215. package/src/routers/user.ts +3 -2
  216. package/src/routers/worksheet.ts +26 -38
  217. package/src/seedDatabase.ts +290 -283
  218. package/src/server/pipelines/aiLabChat.ts +507 -0
  219. package/src/server/pipelines/aiNewtonChat.ts +338 -0
  220. package/src/server/pipelines/gradeWorksheet.ts +151 -0
  221. package/src/trpc.ts +2 -0
  222. package/src/utils/email.ts +30 -3
  223. package/src/utils/inference.ts +85 -5
  224. package/src/utils/logger.ts +2 -1
  225. package/tests/announcement.test.ts +164 -0
  226. package/tests/assignment.test.ts +296 -0
  227. package/tests/attendance.test.ts +168 -0
  228. package/tests/auth.test.ts +33 -10
  229. package/tests/class.test.ts +34 -9
  230. package/tests/event.test.ts +228 -0
  231. package/tests/section.test.ts +216 -0
  232. package/tests/setup.ts +70 -16
  233. package/tests/user.test.ts +158 -0
  234. package/vitest.config.ts +26 -0
  235. package/API_SPECIFICATION.md +0 -1597
  236. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  237. package/CHAT_API_SPEC.md +0 -579
  238. package/LAB_CHAT_API_SPEC.md +0 -518
  239. package/dist/routers/school.d.ts +0 -208
  240. package/dist/routers/school.d.ts.map +0 -1
  241. package/dist/routers/school.js +0 -483
@@ -16,6 +16,7 @@ import { notificationRouter } from "./notifications.js";
16
16
  import { conversationRouter } from "./conversation.js";
17
17
  import { messageRouter } from "./message.js";
18
18
  import { labChatRouter } from "./labChat.js";
19
+ import { newtonChatRouter } from "./newtonChat.js";
19
20
  import { marketingRouter } from "./marketing.js";
20
21
  import { worksheetRouter } from "./worksheet.js";
21
22
  import { commentRouter } from "./comment.js";
@@ -36,6 +37,7 @@ export const appRouter = createTRPCRouter({
36
37
  conversation: conversationRouter,
37
38
  message: messageRouter,
38
39
  labChat: labChatRouter,
40
+ newtonChat: newtonChatRouter,
39
41
  marketing: marketingRouter,
40
42
  worksheet: worksheetRouter,
41
43
  comment: commentRouter,
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
3
  import { prisma } from "../lib/prisma.js";
4
4
  import { TRPCError } from "@trpc/server";
5
- import { addDays, startOfDay, endOfDay } from "date-fns";
5
+ import { addDays, addMonths, subMonths, startOfDay, endOfDay } from "date-fns";
6
6
 
7
7
  export const agendaRouter = createTRPCRouter({
8
8
  get: protectedProcedure
@@ -17,8 +17,11 @@ export const agendaRouter = createTRPCRouter({
17
17
  });
18
18
  }
19
19
 
20
- const weekStart = startOfDay(new Date(input.weekStart));
21
- const weekEnd = endOfDay(addDays(weekStart, 6));
20
+ // Expand query range to 6 months (3 months before and after the reference date)
21
+ // to allow calendar navigation and ensure newly created events are visible
22
+ const referenceDate = new Date(input.weekStart);
23
+ const rangeStart = startOfDay(subMonths(referenceDate, 3));
24
+ const rangeEnd = endOfDay(addMonths(referenceDate, 3));
22
25
 
23
26
  const [personalEvents, classEvents] = await Promise.all([
24
27
  // Get personal events
@@ -26,8 +29,8 @@ export const agendaRouter = createTRPCRouter({
26
29
  where: {
27
30
  userId: ctx.user.id,
28
31
  startTime: {
29
- gte: weekStart,
30
- lte: weekEnd,
32
+ gte: rangeStart,
33
+ lte: rangeEnd,
31
34
  },
32
35
  class: {
33
36
  is: null,
@@ -59,8 +62,8 @@ export const agendaRouter = createTRPCRouter({
59
62
  ],
60
63
  },
61
64
  startTime: {
62
- gte: weekStart,
63
- lte: weekEnd,
65
+ gte: rangeStart,
66
+ lte: rangeEnd,
64
67
  },
65
68
  },
66
69
  include: {
@@ -206,9 +206,10 @@ export const announcementRouter = createTRPCRouter({
206
206
  };
207
207
  }),
208
208
 
209
- update: protectedProcedure
209
+ update: protectedTeacherProcedure
210
210
  .input(z.object({
211
211
  id: z.string(),
212
+ classId: z.string(),
212
213
  data: z.object({
213
214
  remarks: z.string().min(1, "Remarks cannot be empty").optional(),
214
215
  files: z.array(directFileSchema).optional(),
@@ -339,9 +340,10 @@ export const announcementRouter = createTRPCRouter({
339
340
  };
340
341
  }),
341
342
 
342
- delete: protectedProcedure
343
+ delete: protectedTeacherProcedure
343
344
  .input(z.object({
344
345
  id: z.string(),
346
+ classId: z.string(),
345
347
  }))
346
348
  .mutation(async ({ ctx, input }) => {
347
349
  if (!ctx.user) {
@@ -6,6 +6,7 @@ import { createDirectUploadFiles, type DirectUploadFile, confirmDirectUpload, up
6
6
  import { deleteFile } from "../lib/googleCloudStorage.js";
7
7
  import { sendNotifications } from "../lib/notificationHandler.js";
8
8
  import { logger } from "../utils/logger.js";
9
+ import { gradeWorksheetPipeline } from "../server/pipelines/gradeWorksheet.js";
9
10
 
10
11
  // DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
11
12
  // Use directFileSchema instead
@@ -20,6 +21,7 @@ const directFileSchema = z.object({
20
21
 
21
22
  const createAssignmentSchema = z.object({
22
23
  classId: z.string(),
24
+ id: z.string().optional(),
23
25
  title: z.string(),
24
26
  instructions: z.string(),
25
27
  dueDate: z.string(),
@@ -173,11 +175,11 @@ async function getUnifiedList(tx: any, classId: string) {
173
175
  // Updated to batch updates to prevent timeouts with large lists
174
176
  async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Array<{ id: string; type: 'section' | 'assignment' }>) {
175
177
  const BATCH_SIZE = 10; // Process 10 items at a time to avoid overwhelming the transaction
176
-
178
+
177
179
  // Group items by type for more efficient updates
178
180
  const sections: Array<{ id: string; order: number }> = [];
179
181
  const assignments: Array<{ id: string; order: number }> = [];
180
-
182
+
181
183
  orderedItems.forEach((item, index) => {
182
184
  const orderData = { id: item.id, order: index + 1 };
183
185
  if (item.type === 'section') {
@@ -186,7 +188,7 @@ async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Arra
186
188
  assignments.push(orderData);
187
189
  }
188
190
  });
189
-
191
+
190
192
  // Process updates in batches
191
193
  const processBatch = async (items: Array<{ id: string; order: number }>, type: 'section' | 'assignment') => {
192
194
  for (let i = 0; i < items.length; i += BATCH_SIZE) {
@@ -202,7 +204,7 @@ async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Arra
202
204
  );
203
205
  }
204
206
  };
205
-
207
+
206
208
  // Process sections and assignments sequentially to avoid transaction overload
207
209
  await processBatch(sections, 'section');
208
210
  await processBatch(assignments, 'assignment');
@@ -318,7 +320,21 @@ export const assignmentRouter = createTRPCRouter({
318
320
  return updated;
319
321
  }),
320
322
 
321
- move: protectedTeacherProcedure
323
+ exists: protectedClassMemberProcedure
324
+ .input(z.object({
325
+ id: z.string(),
326
+ }))
327
+ .query(async ({ ctx, input }) => {
328
+ if (!ctx.user) {
329
+ throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User must be authenticated' });
330
+ }
331
+ const assignment = await prisma.assignment.findUnique({
332
+ where: { id: input.id },
333
+ });
334
+
335
+ return assignment ? true : false;
336
+ }),
337
+ move: protectedTeacherProcedure
322
338
  .input(z.object({
323
339
  id: z.string(),
324
340
  classId: z.string(),
@@ -354,10 +370,10 @@ export const assignmentRouter = createTRPCRouter({
354
370
  return updated;
355
371
  }),
356
372
 
357
- create: protectedProcedure
373
+ create: protectedTeacherProcedure
358
374
  .input(createAssignmentSchema)
359
375
  .mutation(async ({ ctx, input }) => {
360
- const { classId, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
376
+ const { classId, id, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
361
377
 
362
378
  if (!ctx.user) {
363
379
  throw new TRPCError({
@@ -401,16 +417,14 @@ export const assignmentRouter = createTRPCRouter({
401
417
  }, 0);
402
418
  }
403
419
 
404
- console.log(studentIds);
405
-
406
420
  // Prepare submission data outside transaction
407
- const submissionData = studentIds && studentIds.length > 0
421
+ const submissionData = studentIds && studentIds.length > 0
408
422
  ? studentIds.map((studentId) => ({
409
- student: { connect: { id: studentId } }
410
- }))
423
+ student: { connect: { id: studentId } }
424
+ }))
411
425
  : classData.students.map((student) => ({
412
- student: { connect: { id: student.id } }
413
- }));
426
+ student: { connect: { id: student.id } }
427
+ }));
414
428
 
415
429
  const teacherId = ctx.user.id;
416
430
 
@@ -419,6 +433,7 @@ export const assignmentRouter = createTRPCRouter({
419
433
  // Create assignment with order 0 (will be at top)
420
434
  const created = await tx.assignment.create({
421
435
  data: {
436
+ ...(id && { id }),
422
437
  title,
423
438
  instructions,
424
439
  dueDate: new Date(dueDate),
@@ -516,7 +531,7 @@ export const assignmentRouter = createTRPCRouter({
516
531
  order: { increment: 1 }
517
532
  }
518
533
  });
519
-
534
+
520
535
  await tx.section.updateMany({
521
536
  where: {
522
537
  classId: classId,
@@ -581,12 +596,12 @@ export const assignmentRouter = createTRPCRouter({
581
596
 
582
597
  // Execute file operations in parallel
583
598
  await Promise.all(fileOperations);
584
-
599
+
585
600
  // Send notifications asynchronously (non-blocking)
586
601
  sendNotifications(classData.students.map(student => student.id), {
587
602
  title: `🔔 New assignment for ${classData.name}`,
588
603
  content:
589
- `The assignment "${title}" has been created in ${classData.name}.\n
604
+ `The assignment "${title}" has been created in ${classData.name}.\n
590
605
  Due date: ${new Date(dueDate).toLocaleDateString()}.
591
606
  [Link to assignment](/class/${classId}/assignments/${assignment.id})`
592
607
  }).catch(error => {
@@ -595,7 +610,7 @@ export const assignmentRouter = createTRPCRouter({
595
610
 
596
611
  return assignment;
597
612
  }),
598
- update: protectedProcedure
613
+ update: protectedTeacherProcedure
599
614
  .input(updateAssignmentSchema)
600
615
  .mutation(async ({ ctx, input }) => {
601
616
  const { id, title, instructions, dueDate, files, existingFileIds, worksheetIds, aiPolicyLevel, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, studentIds } = input;
@@ -640,10 +655,10 @@ export const assignmentRouter = createTRPCRouter({
640
655
  },
641
656
  }),
642
657
  prisma.class.findFirst({
643
- where: {
644
- assignments: {
645
- some: { id }
646
- }
658
+ where: {
659
+ assignments: {
660
+ some: { id }
661
+ }
647
662
  },
648
663
  include: {
649
664
  students: {
@@ -661,13 +676,13 @@ export const assignmentRouter = createTRPCRouter({
661
676
  }
662
677
 
663
678
  // Prepare submission data outside transaction if needed
664
- const submissionData = studentIds && studentIds.length > 0
679
+ const submissionData = studentIds && studentIds.length > 0
665
680
  ? studentIds.map((studentId) => ({
666
- student: { connect: { id: studentId } }
667
- }))
681
+ student: { connect: { id: studentId } }
682
+ }))
668
683
  : classData?.students.map((student) => ({
669
- student: { connect: { id: student.id } }
670
- }));
684
+ student: { connect: { id: student.id } }
685
+ }));
671
686
 
672
687
  // Handle file deletion operations outside transaction
673
688
  const fileDeletionPromises: Promise<void>[] = [];
@@ -929,7 +944,7 @@ export const assignmentRouter = createTRPCRouter({
929
944
  };
930
945
  }),
931
946
 
932
- get: protectedProcedure
947
+ get: protectedClassMemberProcedure
933
948
  .input(getAssignmentSchema)
934
949
  .query(async ({ ctx, input }) => {
935
950
  const { id, classId } = input;
@@ -1140,19 +1155,12 @@ export const assignmentRouter = createTRPCRouter({
1140
1155
  };
1141
1156
  }),
1142
1157
 
1143
- getSubmissionById: protectedTeacherProcedure
1158
+ getSubmissionById: protectedClassMemberProcedure
1144
1159
  .input(z.object({
1145
- submissionId: z.string(),
1146
1160
  classId: z.string(),
1161
+ submissionId: z.string(),
1147
1162
  }))
1148
1163
  .query(async ({ ctx, input }) => {
1149
- if (!ctx.user) {
1150
- throw new TRPCError({
1151
- code: "UNAUTHORIZED",
1152
- message: "User must be authenticated",
1153
- });
1154
- }
1155
-
1156
1164
  const { submissionId, classId } = input;
1157
1165
 
1158
1166
  const submission = await prisma.submission.findFirst({
@@ -1161,11 +1169,22 @@ export const assignmentRouter = createTRPCRouter({
1161
1169
  assignment: {
1162
1170
  classId,
1163
1171
  class: {
1164
- teachers: {
1165
- some: {
1166
- id: ctx.user.id
1172
+ OR: [
1173
+ {
1174
+ teachers: {
1175
+ some: {
1176
+ id: ctx.user?.id
1177
+ }
1178
+ }
1179
+ },
1180
+ {
1181
+ students: {
1182
+ some: {
1183
+ id: ctx.user?.id
1184
+ }
1185
+ }
1167
1186
  }
1168
- }
1187
+ ],
1169
1188
  }
1170
1189
  },
1171
1190
  },
@@ -1287,6 +1306,20 @@ export const assignmentRouter = createTRPCRouter({
1287
1306
 
1288
1307
  if (submit !== undefined) {
1289
1308
  // Toggle submission status
1309
+ if (submission.assignment.acceptWorksheet && submission.assignment.gradeWithAI) {
1310
+
1311
+ // Grade the submission with AI
1312
+ const worksheetResponses = await prisma.studentWorksheetResponse.findMany({
1313
+ where: {
1314
+ submissionId: submission.id,
1315
+ },
1316
+ });
1317
+
1318
+ for (const worksheetResponse of worksheetResponses) {
1319
+ // Run it in the background, non-blocking
1320
+ gradeWorksheetPipeline(worksheetResponse.id);
1321
+ }
1322
+ }
1290
1323
  return await prisma.submission.update({
1291
1324
  where: { id: submission.id },
1292
1325
  data: {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { createTRPCRouter, protectedProcedure } from "../trpc.js";
2
+ import { createTRPCRouter, protectedClassMemberProcedure, protectedProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
5
 
@@ -11,7 +11,7 @@ const attendanceSchema = z.object({
11
11
  });
12
12
 
13
13
  export const attendanceRouter = createTRPCRouter({
14
- get: protectedProcedure
14
+ get: protectedClassMemberProcedure
15
15
  .input(z.object({
16
16
  classId: z.string(),
17
17
  eventId: z.string().optional(),
@@ -4,8 +4,10 @@ import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
5
  import { v4 as uuidv4 } from 'uuid';
6
6
  import { compare, hash } from "bcryptjs";
7
- import { transport } from "../utils/email.js";
7
+ import { sendMail } from "../utils/email.js";
8
8
  import { prismaWrapper } from "../utils/prismaWrapper.js";
9
+ import { env } from "../lib/config/env.js";
10
+ import { logger } from "../utils/logger.js";
9
11
 
10
12
  const loginSchema = z.object({
11
13
  username: z.string(),
@@ -106,14 +108,18 @@ export const authRouter = createTRPCRouter({
106
108
  'creating verification token'
107
109
  );
108
110
 
109
- // await transport.sendMail({
110
- // from: 'noreply@studious.sh',
111
- // to: user.email,
112
- // subject: 'Verify your email',
113
- // text: `Click the link to verify your email: ${process.env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
114
- // });
111
+ try {
112
+ await sendMail({
113
+ from: 'noreply@studious.sh',
114
+ to: user.email,
115
+ subject: 'Verify your email',
116
+ text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
117
+ });
118
+ } catch (err) {
119
+ logger.error('Failed to send verification email', { email: user.email, err });
120
+ }
115
121
 
116
- console.log(`${process.env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`)
122
+ // logger.info(`Password verification email sent to ${user.email} at ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`);
117
123
 
118
124
  return {
119
125
  user: {
@@ -280,12 +286,18 @@ export const authRouter = createTRPCRouter({
280
286
  },
281
287
  });
282
288
 
283
- // await transport.sendMail({
284
- // from: 'noreply@studious.sh',
285
- // to: user.email,
286
- // subject: 'Verify your email',
287
- // text: `Click the link to verify your email: ${process.env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
288
- // });
289
+ try {
290
+ await sendMail({
291
+ from: 'noreply@studious.sh',
292
+ to: user.email,
293
+ subject: 'Verify your email',
294
+ text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
295
+ });
296
+ } catch (err) {
297
+ logger.error('Failed to send verification email', { email: user.email, err });
298
+ }
299
+
300
+ // logger.info(`Password verification email sent to ${user.email} at ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`);
289
301
 
290
302
  return { success: true };
291
303
  }),
@@ -326,6 +338,123 @@ export const authRouter = createTRPCRouter({
326
338
  where: { id: token },
327
339
  });
328
340
 
341
+ return { success: true };
342
+ }),
343
+
344
+ requestPasswordReset: publicProcedure
345
+ .input(z.object({
346
+ email: z.string().email(),
347
+ }))
348
+ .mutation(async ({ input }) => {
349
+ const { email } = input;
350
+
351
+ const user = await prisma.user.findFirst({
352
+ where: { email },
353
+ select: {
354
+ id: true,
355
+ email: true,
356
+ username: true,
357
+ },
358
+ });
359
+
360
+ // Don't reveal if user exists or not for security
361
+ if (!user) {
362
+ return { success: true };
363
+ }
364
+
365
+ // Delete any existing password reset tokens for this user
366
+ // Only delete tokens that expire within 2 hours (likely password reset tokens)
367
+ const twoHoursFromNow = new Date(Date.now() + 1000 * 60 * 60 * 2);
368
+ await prisma.session.deleteMany({
369
+ where: {
370
+ userId: user.id,
371
+ classId: null,
372
+ expiresAt: {
373
+ lte: twoHoursFromNow, // Only delete short-lived tokens (password reset tokens)
374
+ },
375
+ },
376
+ });
377
+
378
+ // Create a new password reset token (expires in 1 hour)
379
+ const resetToken = await prisma.session.create({
380
+ data: {
381
+ id: uuidv4(),
382
+ userId: user.id,
383
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60), // 1 hour
384
+ },
385
+ });
386
+
387
+ // Send password reset email
388
+ try {
389
+ await sendMail({
390
+ from: 'noreply@studious.sh',
391
+ to: user.email,
392
+ subject: 'Reset your password',
393
+ text: `Click the link to reset your password: ${env.NEXT_PUBLIC_APP_URL}/reset-password/${resetToken.id}`,
394
+ });
395
+ } catch (err) {
396
+ logger.error('Failed to send password reset email', { email: user.email, err });
397
+ }
398
+
399
+ // logger.info(`Password reset email sent to ${user.email} at ${env.NEXT_PUBLIC_APP_URL}/reset-password/${resetToken.id}`);
400
+
401
+ return { success: true };
402
+ }),
403
+
404
+ resetPassword: publicProcedure
405
+ .input(z.object({
406
+ token: z.string(),
407
+ password: z.string().min(6, "Password must be at least 6 characters"),
408
+ confirmPassword: z.string(),
409
+ }).refine((data) => data.password === data.confirmPassword, {
410
+ message: "Passwords don't match",
411
+ path: ["confirmPassword"],
412
+ }))
413
+ .mutation(async ({ input }) => {
414
+ const { token, password } = input;
415
+
416
+ const session = await prisma.session.findUnique({
417
+ where: { id: token },
418
+ include: {
419
+ user: {
420
+ select: {
421
+ id: true,
422
+ },
423
+ },
424
+ },
425
+ });
426
+
427
+ if (!session || !session.userId) {
428
+ throw new TRPCError({
429
+ code: "NOT_FOUND",
430
+ message: "Invalid or expired reset token",
431
+ });
432
+ }
433
+
434
+ if (session.expiresAt && session.expiresAt < new Date()) {
435
+ // Clean up expired token
436
+ await prisma.session.delete({
437
+ where: { id: token },
438
+ });
439
+ throw new TRPCError({
440
+ code: "UNAUTHORIZED",
441
+ message: "Reset token has expired",
442
+ });
443
+ }
444
+
445
+ // Update the user's password
446
+ await prisma.user.update({
447
+ where: { id: session.userId },
448
+ data: {
449
+ password: await hash(password, 10),
450
+ },
451
+ });
452
+
453
+ // Clean up the reset token
454
+ await prisma.session.delete({
455
+ where: { id: token },
456
+ });
457
+
329
458
  return { success: true };
330
459
  }),
331
460
  });
@@ -465,6 +465,51 @@ export const classRouter = createTRPCRouter({
465
465
  removedUserId: userId,
466
466
  };
467
467
  }),
468
+ leaveClass: protectedProcedure
469
+ .input(z.object({
470
+ classId: z.string(),
471
+ }))
472
+ .mutation(async ({ ctx, input }) => {
473
+ const { classId } = input;
474
+ const userId = ctx.user?.id;
475
+
476
+ if (!userId) {
477
+ throw new TRPCError({
478
+ code: 'UNAUTHORIZED',
479
+ message: 'User not authenticated',
480
+ });
481
+ }
482
+
483
+ const classData = await prisma.class.findFirst({
484
+ where: {
485
+ id: classId,
486
+ students: {
487
+ some: { id: userId },
488
+ },
489
+ },
490
+ });
491
+
492
+ if (!classData) {
493
+ throw new TRPCError({
494
+ code: 'NOT_FOUND',
495
+ message: 'Class not found or you are not a student in this class',
496
+ });
497
+ }
498
+
499
+ await prisma.class.update({
500
+ where: { id: classId },
501
+ data: {
502
+ students: {
503
+ disconnect: { id: userId },
504
+ },
505
+ },
506
+ });
507
+
508
+ return {
509
+ success: true,
510
+ leftClassId: classId,
511
+ };
512
+ }),
468
513
  join: protectedProcedure
469
514
  .input(z.object({
470
515
  classCode: z.string(),
@@ -472,9 +517,13 @@ export const classRouter = createTRPCRouter({
472
517
  .mutation(async ({ ctx, input }) => {
473
518
  const { classCode } = input;
474
519
 
520
+ // Case-insensitive search for invite code
475
521
  const session = await prisma.session.findFirst({
476
522
  where: {
477
- id: classCode,
523
+ id: {
524
+ equals: classCode,
525
+ mode: 'insensitive',
526
+ },
478
527
  },
479
528
  });
480
529
 
@@ -676,7 +725,7 @@ export const classRouter = createTRPCRouter({
676
725
 
677
726
  return events;
678
727
  }),
679
- listMarkSchemes: protectedTeacherProcedure
728
+ listMarkSchemes: protectedClassMemberProcedure
680
729
  .input(z.object({
681
730
  classId: z.string(),
682
731
  }))
@@ -755,7 +804,7 @@ export const classRouter = createTRPCRouter({
755
804
 
756
805
  return markScheme;
757
806
  }),
758
- listGradingBoundaries: protectedTeacherProcedure
807
+ listGradingBoundaries: protectedClassMemberProcedure
759
808
  .input(z.object({
760
809
  classId: z.string(),
761
810
  }))