@studious-lms/server 1.2.45 → 1.2.46

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 (231) 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 +6 -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 +294 -98
  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 +7 -4
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/assignment.js +35 -18
  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 +1 -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 +1 -0
  135. package/dist/routers/labChat.d.ts.map +1 -1
  136. package/dist/routers/labChat.js +2 -3
  137. package/dist/routers/labChat.js.map +1 -1
  138. package/dist/routers/marketing.d.ts +1 -1
  139. package/dist/routers/newtonChat.d.ts +55 -0
  140. package/dist/routers/newtonChat.d.ts.map +1 -0
  141. package/dist/routers/newtonChat.js +438 -0
  142. package/dist/routers/newtonChat.js.map +1 -0
  143. package/dist/routers/notifications.d.ts +4 -4
  144. package/dist/routers/section.d.ts +9 -4
  145. package/dist/routers/section.d.ts.map +1 -1
  146. package/dist/routers/section.js +8 -8
  147. package/dist/routers/section.js.map +1 -1
  148. package/dist/routers/user.d.ts.map +1 -1
  149. package/dist/routers/user.js +5 -4
  150. package/dist/routers/user.js.map +1 -1
  151. package/dist/routers/worksheet.d.ts +30 -36
  152. package/dist/routers/worksheet.d.ts.map +1 -1
  153. package/dist/routers/worksheet.js +11 -33
  154. package/dist/routers/worksheet.js.map +1 -1
  155. package/dist/seedDatabase.d.ts +1 -1
  156. package/dist/seedDatabase.js +275 -284
  157. package/dist/seedDatabase.js.map +1 -1
  158. package/dist/server/pipelines/aiLabChat.d.ts +10 -0
  159. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  160. package/dist/server/pipelines/aiLabChat.js +83 -0
  161. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  162. package/dist/server/pipelines/gradeWorksheet.d.ts +2 -0
  163. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  164. package/dist/server/pipelines/gradeWorksheet.js +138 -0
  165. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  166. package/dist/trpc.d.ts.map +1 -1
  167. package/dist/trpc.js +2 -2
  168. package/dist/trpc.js.map +1 -1
  169. package/dist/utils/email.d.ts +9 -1
  170. package/dist/utils/email.d.ts.map +1 -1
  171. package/dist/utils/email.js +20 -5
  172. package/dist/utils/email.js.map +1 -1
  173. package/dist/utils/inference.d.ts +3 -0
  174. package/dist/utils/inference.d.ts.map +1 -1
  175. package/dist/utils/inference.js +41 -7
  176. package/dist/utils/inference.js.map +1 -1
  177. package/dist/utils/logger.d.ts.map +1 -1
  178. package/dist/utils/logger.js +3 -3
  179. package/dist/utils/logger.js.map +1 -1
  180. package/docker-compose.yml +14 -0
  181. package/package.json +13 -4
  182. package/prisma/schema.prisma +32 -5
  183. package/scripts/test-pre-push.ts +14 -0
  184. package/src/index.ts +98 -54
  185. package/src/instrument.ts +13 -6
  186. package/src/lib/config/env.ts +126 -0
  187. package/src/lib/fileUpload.ts +3 -2
  188. package/src/lib/googleCloudStorage.ts +6 -6
  189. package/src/lib/jsonConversion.ts +12 -14
  190. package/src/lib/prisma.ts +23 -2
  191. package/src/lib/pusher.ts +6 -5
  192. package/src/middleware/auth.ts +4 -3
  193. package/src/middleware/security.ts +80 -0
  194. package/src/routers/_app.ts +2 -0
  195. package/src/routers/agenda.ts +10 -7
  196. package/src/routers/announcement.ts +4 -2
  197. package/src/routers/assignment.ts +58 -40
  198. package/src/routers/attendance.ts +2 -2
  199. package/src/routers/auth.ts +143 -14
  200. package/src/routers/class.ts +52 -3
  201. package/src/routers/conversation.ts +49 -29
  202. package/src/routers/file.ts +29 -5
  203. package/src/routers/labChat.ts +0 -1
  204. package/src/routers/newtonChat.ts +520 -0
  205. package/src/routers/section.ts +6 -6
  206. package/src/routers/user.ts +3 -2
  207. package/src/routers/worksheet.ts +9 -37
  208. package/src/seedDatabase.ts +290 -283
  209. package/src/server/pipelines/aiLabChat.ts +92 -0
  210. package/src/server/pipelines/gradeWorksheet.ts +152 -0
  211. package/src/trpc.ts +2 -0
  212. package/src/utils/email.ts +30 -3
  213. package/src/utils/inference.ts +50 -5
  214. package/src/utils/logger.ts +2 -1
  215. package/tests/announcement.test.ts +164 -0
  216. package/tests/assignment.test.ts +296 -0
  217. package/tests/attendance.test.ts +168 -0
  218. package/tests/auth.test.ts +33 -10
  219. package/tests/class.test.ts +34 -9
  220. package/tests/event.test.ts +228 -0
  221. package/tests/section.test.ts +216 -0
  222. package/tests/setup.ts +70 -16
  223. package/tests/user.test.ts +158 -0
  224. package/vitest.config.ts +26 -0
  225. package/API_SPECIFICATION.md +0 -1597
  226. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  227. package/CHAT_API_SPEC.md +0 -579
  228. package/LAB_CHAT_API_SPEC.md +0 -518
  229. package/dist/routers/school.d.ts +0 -208
  230. package/dist/routers/school.d.ts.map +0 -1
  231. package/dist/routers/school.js +0 -483
@@ -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
  }))
@@ -148,51 +148,69 @@ export const conversationRouter = createTRPCRouter({
148
148
 
149
149
  // For DMs, check if conversation already exists
150
150
  if (type === 'DM') {
151
- const existingDM = await prisma.conversation.findFirst({
151
+ // Get the target user's ID from their username
152
+ const targetUser = await prisma.user.findFirst({
153
+ where: { username: memberIds[0] },
154
+ select: { id: true, username: true },
155
+ });
156
+
157
+ if (!targetUser) {
158
+ throw new TRPCError({
159
+ code: 'BAD_REQUEST',
160
+ message: `User "${memberIds[0]}" not found`,
161
+ });
162
+ }
163
+
164
+ // Find all DM conversations where current user is a member
165
+ const existingDMs = await prisma.conversation.findMany({
152
166
  where: {
153
167
  type: 'DM',
154
168
  members: {
155
- every: {
156
- userId: {
157
- in: [userId, memberIds[0]],
158
- },
159
- },
160
- },
161
- AND: {
162
- members: {
163
- some: {
164
- userId,
165
- },
169
+ some: {
170
+ userId,
166
171
  },
167
172
  },
168
173
  },
169
174
  include: {
170
175
  members: {
171
- include: {
172
- user: {
173
- select: {
174
- id: true,
175
- username: true,
176
- profile: {
177
- select: {
178
- displayName: true,
179
- profilePicture: true,
180
- },
181
- },
182
- },
183
- },
176
+ select: {
177
+ userId: true,
184
178
  },
185
179
  },
186
180
  },
187
181
  });
188
182
 
183
+ // Check if any of these conversations has exactly 2 members (current user + target user)
184
+ const existingDM = existingDMs.find(conv => {
185
+ const memberUserIds = conv.members.map(m => m.userId);
186
+ return memberUserIds.length === 2 &&
187
+ memberUserIds.includes(userId) &&
188
+ memberUserIds.includes(targetUser.id);
189
+ });
190
+
189
191
  if (existingDM) {
190
- return existingDM;
192
+ // Conversation already exists, throw error with friendly message
193
+ throw new TRPCError({
194
+ code: 'BAD_REQUEST',
195
+ message: `A conversation with ${targetUser.username} already exists`,
196
+ });
191
197
  }
192
198
  }
193
199
 
194
200
  // Verify all members exist
195
- const members = await prisma.user.findMany({
201
+ const membersWithIds = await prisma.user.findMany({
202
+ where: {
203
+ id: {
204
+ in: memberIds,
205
+ },
206
+ },
207
+ select: {
208
+ id: true,
209
+ username: true,
210
+ },
211
+ });
212
+
213
+ const membersWithUsernames = await prisma.user.findMany({
196
214
  where: {
197
215
  username: {
198
216
  in: memberIds,
@@ -204,6 +222,8 @@ export const conversationRouter = createTRPCRouter({
204
222
  },
205
223
  });
206
224
 
225
+ const members = [...membersWithIds, ...membersWithUsernames];
226
+
207
227
  if (members.length !== memberIds.length) {
208
228
  throw new TRPCError({
209
229
  code: 'BAD_REQUEST',
@@ -222,8 +242,8 @@ export const conversationRouter = createTRPCRouter({
222
242
  userId,
223
243
  role: type === 'GROUP' ? 'ADMIN' : 'MEMBER',
224
244
  },
225
- ...memberIds.map((memberId) => ({
226
- userId: members.find((member) => member.username === memberId)!.id,
245
+ ...members.map((member) => ({
246
+ userId: member.id,
227
247
  role: 'MEMBER' as const,
228
248
  })),
229
249
  ],
@@ -73,6 +73,16 @@ export const fileRouter = createTRPCRouter({
73
73
  }
74
74
  }
75
75
  }
76
+ },
77
+ announcement: {
78
+ include: {
79
+ class: {
80
+ include: {
81
+ students: true,
82
+ teachers: true
83
+ }
84
+ }
85
+ }
76
86
  }
77
87
  }
78
88
  });
@@ -92,18 +102,30 @@ export const fileRouter = createTRPCRouter({
92
102
  // Check if user is a teacher of the class
93
103
  if (file.assignment?.class) {
94
104
  classId = file.assignment.class.id;
95
- hasAccess = file.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
105
+ const isTeacher = file.assignment.class.teachers.some(teacher => teacher.id === userId);
106
+ const isStudent = file.assignment.class.students.some(student => student.id === userId);
107
+ logger.info(`Assignment file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}`);
108
+ hasAccess = isTeacher || isStudent;
109
+ }
110
+
111
+ // Check if user has access to announcement files (teachers or students in the class)
112
+ if ((file as any).announcement?.class) {
113
+ classId = (file as any).announcement.class.id;
114
+ const isTeacher = (file as any).announcement.class.teachers.some((teacher: any) => teacher.id === userId);
115
+ const isStudent = (file as any).announcement.class.students.some((student: any) => student.id === userId);
116
+ logger.info(`Announcement file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}`);
117
+ hasAccess = hasAccess || isTeacher || isStudent;
96
118
  }
97
119
 
98
120
  if (file.submission?.assignment?.classId) {
99
121
  classId = file.submission.assignment.classId;
100
- hasAccess = file.submission?.studentId === userId || false;
122
+ hasAccess = hasAccess || file.submission?.studentId === userId || false;
101
123
  if (!hasAccess) hasAccess = file.submission.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
102
124
  }
103
125
 
104
126
  if (file.annotations?.assignment?.classId) {
105
127
  classId = file.annotations?.assignment.classId;
106
- hasAccess = file.annotations?.studentId === userId || false;
128
+ hasAccess = hasAccess || file.annotations?.studentId === userId || false;
107
129
  if (!hasAccess) hasAccess = file.annotations.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
108
130
  }
109
131
 
@@ -114,8 +136,10 @@ export const fileRouter = createTRPCRouter({
114
136
 
115
137
  // Check if file is in a folder and user has access to the class
116
138
  if (file.folder?.class) {
117
- hasAccess = hasAccess || file.folder.class.teachers.some(teacher => teacher.id === userId);
118
- hasAccess = hasAccess || file.folder.class.students.some(student => student.id === userId);
139
+ const isTeacher = file.folder.class.teachers.some(teacher => teacher.id === userId);
140
+ const isStudent = file.folder.class.students.some(student => student.id === userId);
141
+ hasAccess = hasAccess || isTeacher;
142
+ hasAccess = hasAccess || isStudent;
119
143
  }
120
144
 
121
145
  if (!hasAccess) {
@@ -902,7 +902,6 @@ WHEN CREATING COURSE MATERIALS (docs field):
902
902
  if (jsonData.docs && Array.isArray(jsonData.docs)) {
903
903
 
904
904
 
905
- console.log('Generating PDFs', { labChatId, docs: JSON.stringify(jsonData.docs, null, 2) });
906
905
  for (let i = 0; i < jsonData.docs.length; i++) {
907
906
  const doc = jsonData.docs[i];
908
907
  if (!doc.title || !doc.blocks || !Array.isArray(doc.blocks)) {