@studious-lms/server 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/index.js +4 -4
  2. package/dist/middleware/auth.js +1 -1
  3. package/dist/middleware/logging.js +1 -1
  4. package/dist/routers/_app.js +1 -1
  5. package/dist/routers/agenda.js +1 -1
  6. package/dist/routers/announcement.js +1 -1
  7. package/dist/routers/assignment.js +3 -3
  8. package/dist/routers/attendance.js +1 -1
  9. package/dist/routers/auth.js +2 -2
  10. package/dist/routers/class.js +2 -2
  11. package/dist/routers/event.js +1 -1
  12. package/dist/routers/file.js +2 -2
  13. package/dist/routers/section.js +1 -1
  14. package/dist/routers/user.js +2 -2
  15. package/dist/trpc.js +2 -2
  16. package/package.json +1 -6
  17. package/prisma/schema.prisma +228 -0
  18. package/src/exportType.ts +9 -0
  19. package/src/index.ts +94 -0
  20. package/src/lib/fileUpload.ts +163 -0
  21. package/src/lib/googleCloudStorage.ts +94 -0
  22. package/src/lib/prisma.ts +16 -0
  23. package/src/lib/thumbnailGenerator.ts +185 -0
  24. package/src/logger.ts +163 -0
  25. package/src/middleware/auth.ts +191 -0
  26. package/src/middleware/logging.ts +54 -0
  27. package/src/routers/_app.ts +34 -0
  28. package/src/routers/agenda.ts +79 -0
  29. package/src/routers/announcement.ts +134 -0
  30. package/src/routers/assignment.ts +1614 -0
  31. package/src/routers/attendance.ts +284 -0
  32. package/src/routers/auth.ts +286 -0
  33. package/src/routers/class.ts +753 -0
  34. package/src/routers/event.ts +509 -0
  35. package/src/routers/file.ts +96 -0
  36. package/src/routers/section.ts +138 -0
  37. package/src/routers/user.ts +82 -0
  38. package/src/socket/handlers.ts +143 -0
  39. package/src/trpc.ts +90 -0
  40. package/src/types/trpc.ts +15 -0
  41. package/src/utils/email.ts +11 -0
  42. package/src/utils/generateInviteCode.ts +8 -0
  43. package/src/utils/logger.ts +156 -0
  44. package/tsconfig.json +17 -0
  45. package/generated/prisma/client.d.ts +0 -1
  46. package/generated/prisma/client.js +0 -4
  47. package/generated/prisma/default.d.ts +0 -1
  48. package/generated/prisma/default.js +0 -4
  49. package/generated/prisma/edge.d.ts +0 -1
  50. package/generated/prisma/edge.js +0 -389
  51. package/generated/prisma/index-browser.js +0 -375
  52. package/generated/prisma/index.d.ts +0 -34865
  53. package/generated/prisma/index.js +0 -410
  54. package/generated/prisma/libquery_engine-darwin-arm64.dylib.node +0 -0
  55. package/generated/prisma/package.json +0 -140
  56. package/generated/prisma/runtime/edge-esm.js +0 -34
  57. package/generated/prisma/runtime/edge.js +0 -34
  58. package/generated/prisma/runtime/index-browser.d.ts +0 -370
  59. package/generated/prisma/runtime/index-browser.js +0 -16
  60. package/generated/prisma/runtime/library.d.ts +0 -3647
  61. package/generated/prisma/runtime/library.js +0 -146
  62. package/generated/prisma/runtime/react-native.js +0 -83
  63. package/generated/prisma/runtime/wasm.js +0 -35
  64. package/generated/prisma/schema.prisma +0 -304
  65. package/generated/prisma/wasm.d.ts +0 -1
  66. package/generated/prisma/wasm.js +0 -375
@@ -0,0 +1,753 @@
1
+ import { z } from "zod";
2
+ import { createTRPCRouter, protectedProcedure, protectedTeacherProcedure, protectedClassMemberProcedure } from "../trpc";
3
+ import { prisma } from "../lib/prisma";
4
+ import { TRPCError } from "@trpc/server";
5
+ import { generateInviteCode } from "../utils/generateInviteCode";
6
+
7
+ export const classRouter = createTRPCRouter({
8
+ getAll: protectedProcedure
9
+
10
+ .query(async ({ ctx }) => {
11
+ const [teacherClasses, studentClasses] = await Promise.all([
12
+ prisma.class.findMany({
13
+ where: {
14
+ teachers: {
15
+ some: {
16
+ id: ctx.user?.id,
17
+ },
18
+ },
19
+ },
20
+ include: {
21
+ assignments: {
22
+ where: {
23
+ dueDate: {
24
+ equals: new Date(new Date().setHours(0, 0, 0, 0)),
25
+ },
26
+ },
27
+ select: {
28
+ id: true,
29
+ title: true,
30
+ },
31
+ },
32
+ },
33
+ }),
34
+ prisma.class.findMany({
35
+ where: {
36
+ students: {
37
+ some: {
38
+ id: ctx.user?.id,
39
+ },
40
+ },
41
+ },
42
+ include: {
43
+ assignments: {
44
+ where: {
45
+ dueDate: {
46
+ equals: new Date(new Date().setHours(0, 0, 0, 0)),
47
+ },
48
+ },
49
+ select: {
50
+ id: true,
51
+ title: true,
52
+ },
53
+ },
54
+ },
55
+ }),
56
+ ]);
57
+
58
+ const adminClasses = await prisma.class.findMany({
59
+ where: {
60
+ teachers: {
61
+ some: {
62
+ id: ctx.user?.id,
63
+ },
64
+ },
65
+ },
66
+ include: {
67
+ assignments: {
68
+ where: {
69
+ dueDate: {
70
+ equals: new Date(new Date().setHours(0, 0, 0, 0)),
71
+ },
72
+ },
73
+ select: {
74
+ id: true,
75
+ title: true,
76
+ },
77
+ },
78
+ },
79
+ });
80
+
81
+ return {
82
+ teacherInClass: teacherClasses.map(cls => ({
83
+ id: cls.id,
84
+ name: cls.name,
85
+ section: cls.section,
86
+ subject: cls.subject,
87
+ dueToday: cls.assignments,
88
+ color: cls.color,
89
+ })),
90
+ studentInClass: studentClasses.map(cls => ({
91
+ id: cls.id,
92
+ name: cls.name,
93
+ section: cls.section,
94
+ subject: cls.subject,
95
+ dueToday: cls.assignments,
96
+ color: cls.color,
97
+ })),
98
+ adminInClass: adminClasses.map(cls => ({
99
+ id: cls.id,
100
+ name: cls.name,
101
+ section: cls.section,
102
+ subject: cls.subject,
103
+ dueToday: cls.assignments,
104
+ color: cls.color,
105
+ })),
106
+ };
107
+ }),
108
+ get: protectedProcedure
109
+ .input(z.object({
110
+ classId: z.string(),
111
+ }))
112
+ .query(async ({ ctx, input }) => {
113
+ const { classId } = input;
114
+
115
+ const classData = await prisma.class.findUnique({
116
+ where: {
117
+ id: classId,
118
+ },
119
+ include: {
120
+ teachers: {
121
+ select: {
122
+ id: true,
123
+ username: true,
124
+ },
125
+ },
126
+ students: {
127
+ select: {
128
+ id: true,
129
+ username: true,
130
+ },
131
+ },
132
+ announcements: {
133
+ orderBy: {
134
+ createdAt: 'desc',
135
+ },
136
+ select: {
137
+ id: true,
138
+ remarks: true,
139
+ createdAt: true,
140
+ teacher: {
141
+ select: {
142
+ id: true,
143
+ username: true,
144
+ },
145
+ },
146
+ },
147
+ },
148
+ assignments: {
149
+ select: {
150
+ type: true,
151
+ id: true,
152
+ title: true,
153
+ dueDate: true,
154
+ createdAt: true,
155
+ weight: true,
156
+ graded: true,
157
+ maxGrade: true,
158
+ instructions: true,
159
+ section: {
160
+ select: {
161
+ id: true,
162
+ name: true,
163
+ },
164
+ },
165
+ markScheme: {
166
+ select: {
167
+ id: true,
168
+ structured: true,
169
+ },
170
+ },
171
+ gradingBoundary: {
172
+ select: {
173
+ id: true,
174
+ structured: true,
175
+ },
176
+ },
177
+ submissions: {
178
+ where: {
179
+ studentId: ctx.user?.id,
180
+ },
181
+ select: {
182
+ studentId: true,
183
+ id: true,
184
+ submitted: true,
185
+ returned: true,
186
+ submittedAt: true,
187
+ },
188
+ },
189
+ },
190
+ },
191
+ },
192
+ });
193
+
194
+ const sections = await prisma.section.findMany({
195
+ where: {
196
+ classId: classId,
197
+ },
198
+ });
199
+
200
+ if (!classData) {
201
+ throw new Error('Class not found');
202
+ }
203
+
204
+ return {
205
+ class: {
206
+ ...classData,
207
+ assignments: classData.assignments.map(assignment => ({
208
+ ...assignment,
209
+ late: assignment.dueDate < new Date(),
210
+ submitted: assignment.submissions.some(submission => submission.studentId === ctx.user?.id),
211
+ returned: assignment.submissions.some(submission => submission.studentId === ctx.user?.id && submission.returned),
212
+ })),
213
+ sections,
214
+ },
215
+ };
216
+ }),
217
+ update: protectedTeacherProcedure
218
+ .input(z.object({
219
+ classId: z.string(),
220
+ name: z.string().optional(),
221
+ section: z.string().optional(),
222
+ subject: z.string().optional(),
223
+ }))
224
+ .mutation(async ({ ctx, input }) => {
225
+ const { classId, ...updateData } = input;
226
+
227
+ const updatedClass = await prisma.class.update({
228
+ where: {
229
+ id: classId,
230
+ },
231
+ data: updateData,
232
+ select: {
233
+ id: true,
234
+ name: true,
235
+ section: true,
236
+ subject: true,
237
+ }
238
+ });
239
+
240
+ return {
241
+ updatedClass,
242
+ }
243
+ }),
244
+ create: protectedProcedure
245
+ .input(z.object({
246
+ students: z.array(z.string()).optional(),
247
+ teachers: z.array(z.string()).optional(),
248
+ name: z.string(),
249
+ section: z.string(),
250
+ subject: z.string(),
251
+ }))
252
+ .mutation(async ({ ctx, input }) => {
253
+ const { students, teachers, name, section, subject } = input;
254
+
255
+ if (teachers && teachers.length > 0 && students && students.length > 0) {
256
+ const newClass = await prisma.class.create({
257
+ data: {
258
+ name,
259
+ section,
260
+ subject,
261
+ teachers: {
262
+ connect: teachers.map(teacher => ({ id: teacher })),
263
+ },
264
+ students: {
265
+ connect: students.map(student => ({ id: student })),
266
+ },
267
+ },
268
+ include: {
269
+ teachers: true,
270
+ students: true,
271
+ },
272
+ });
273
+ return newClass;
274
+ }
275
+
276
+ const newClass = await prisma.class.create({
277
+ data: {
278
+ name,
279
+ section,
280
+ subject,
281
+ teachers: {
282
+ connect: {
283
+ id: ctx.user?.id,
284
+ },
285
+ },
286
+ },
287
+ });
288
+
289
+ return newClass;
290
+ }),
291
+ delete: protectedTeacherProcedure
292
+ .input(z.object({
293
+ classId: z.string(),
294
+ id: z.string(),
295
+ }))
296
+ .mutation(async ({ ctx, input }) => {
297
+ // Verify user is the teacher of this class
298
+ const classToDelete = await prisma.class.findFirst({
299
+ where: {
300
+ id: input.id,
301
+ },
302
+ });
303
+
304
+ if (!classToDelete) {
305
+ throw new Error("Class not found or you don't have permission to delete it");
306
+ }
307
+
308
+ await prisma.class.delete({
309
+ where: {
310
+ id: input.id,
311
+ },
312
+ });
313
+
314
+ return {
315
+ deletedClass: {
316
+ id: input.id,
317
+ }
318
+ }
319
+ }),
320
+ addStudent: protectedTeacherProcedure
321
+ .input(z.object({
322
+ classId: z.string(),
323
+ studentId: z.string(),
324
+ }))
325
+ .mutation(async ({ ctx, input }) => {
326
+ const { classId, studentId } = input;
327
+
328
+ const student = await prisma.user.findUnique({
329
+ where: {
330
+ id: studentId,
331
+ },
332
+ });
333
+
334
+ if (!student) {
335
+ throw new Error("Student not found");
336
+ }
337
+
338
+ const updatedClass = await prisma.class.update({
339
+ where: {
340
+ id: classId,
341
+ },
342
+ data: {
343
+ students: {
344
+ connect: {
345
+ id: studentId,
346
+ },
347
+ },
348
+ },
349
+ select: {
350
+ id: true,
351
+ name: true,
352
+ section: true,
353
+ subject: true,
354
+ }
355
+ });
356
+
357
+ return {
358
+ updatedClass,
359
+ newStudent: student,
360
+ }
361
+ }),
362
+ changeRole: protectedTeacherProcedure
363
+ .input(z.object({
364
+ classId: z.string(),
365
+ userId: z.string(),
366
+ type: z.enum(['teacher', 'student']),
367
+ }))
368
+ .mutation(async ({ ctx, input }) => {
369
+ const { classId, userId, type } = input;
370
+
371
+ const user = await prisma.user.findUnique({
372
+ where: { id: userId },
373
+ select: {
374
+ id: true,
375
+ username: true,
376
+ },
377
+ });
378
+
379
+ if (!user) {
380
+ throw new Error("User not found");
381
+ }
382
+
383
+ const updatedClass = await prisma.class.update({
384
+ where: { id: classId },
385
+ data: {
386
+ [type === 'teacher' ? 'teachers' : 'students']: {
387
+ connect: { id: userId },
388
+ },
389
+ [type === 'teacher' ? 'students' : 'teachers']: {
390
+ disconnect: { id: userId },
391
+ },
392
+ },
393
+ });
394
+
395
+ return {
396
+ updatedClass,
397
+ user: {
398
+ ...user,
399
+ type,
400
+ },
401
+ };
402
+ }),
403
+ removeMember: protectedTeacherProcedure
404
+ .input(z.object({
405
+ classId: z.string(),
406
+ userId: z.string(),
407
+ }))
408
+ .mutation(async ({ ctx, input }) => {
409
+ const { classId, userId } = input;
410
+
411
+ const updatedClass = await prisma.class.update({
412
+ where: { id: classId },
413
+ data: {
414
+ teachers: {
415
+ disconnect: { id: userId },
416
+ },
417
+ students: {
418
+ disconnect: { id: userId },
419
+ },
420
+ },
421
+ });
422
+
423
+ return {
424
+ updatedClass,
425
+ removedUserId: userId,
426
+ };
427
+ }),
428
+ join: protectedProcedure
429
+ .input(z.object({
430
+ classCode: z.string(),
431
+ }))
432
+ .mutation(async ({ ctx, input }) => {
433
+ const { classCode } = input;
434
+
435
+ const session = await prisma.session.findFirst({
436
+ where: {
437
+ id: classCode,
438
+ },
439
+ });
440
+
441
+ if (!session || !session.classId) {
442
+ throw new Error("Class not found");
443
+ }
444
+
445
+ if (session.expiresAt && session.expiresAt < new Date()) {
446
+ throw new Error("Session expired");
447
+ }
448
+
449
+ const updatedClass = await prisma.class.update({
450
+ where: { id: session.classId },
451
+ data: {
452
+ students: {
453
+ connect: { id: ctx.user?.id },
454
+ },
455
+ },
456
+ select: {
457
+ id: true,
458
+ name: true,
459
+ section: true,
460
+ subject: true,
461
+ },
462
+ });
463
+
464
+ return {
465
+ joinedClass: updatedClass,
466
+ }
467
+ }),
468
+ getInviteCode: protectedTeacherProcedure
469
+ .input(z.object({
470
+ classId: z.string(),
471
+ }))
472
+ .query(async ({ ctx, input }) => {
473
+ const { classId } = input;
474
+
475
+ const session = await prisma.session.findFirst({
476
+ where: {
477
+ classId,
478
+ },
479
+ });
480
+
481
+ if ((session?.expiresAt && session.expiresAt < new Date()) || !session) {
482
+ const newSession = await prisma.session.create({
483
+ data: {
484
+ id: generateInviteCode(),
485
+ classId,
486
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours from now
487
+ }
488
+ });
489
+ return {
490
+ code: newSession.id,
491
+ }
492
+ }
493
+
494
+ return {
495
+ code: session?.id,
496
+ };
497
+ }),
498
+ createInviteCode: protectedTeacherProcedure
499
+ .input(z.object({
500
+ classId: z.string(),
501
+ }))
502
+ .mutation(async ({ ctx, input }) => {
503
+ const { classId } = input;
504
+
505
+ // Create a new session for the invite code
506
+ const session = await prisma.session.create({
507
+ data: {
508
+ id: generateInviteCode(),
509
+ classId,
510
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours from now
511
+ }
512
+ });
513
+
514
+ return {
515
+ code: session.id,
516
+ };
517
+ }),
518
+ getGrades: protectedClassMemberProcedure
519
+ .input(z.object({
520
+ classId: z.string(),
521
+ userId: z.string(),
522
+ }))
523
+ .query(async ({ ctx, input }) => {
524
+ const { classId, userId } = input;
525
+
526
+ const isTeacher = await prisma.class.findFirst({
527
+ where: {
528
+ id: classId,
529
+ teachers: {
530
+ some: { id: ctx.user?.id }
531
+ }
532
+ }
533
+ });
534
+ // If student, only allow viewing their own grades
535
+ if (ctx.user?.id !== userId && !isTeacher) {
536
+ throw new TRPCError({
537
+ code: 'UNAUTHORIZED',
538
+ message: 'You can only view your own grades',
539
+ });
540
+ }
541
+
542
+ const grades = await prisma.submission.findMany({
543
+ where: {
544
+ studentId: userId,
545
+ assignment: {
546
+ classId: classId,
547
+ graded: true
548
+ }
549
+ },
550
+ include: {
551
+ assignment: {
552
+ select: {
553
+ id: true,
554
+ title: true,
555
+ maxGrade: true,
556
+ weight: true,
557
+ }
558
+ },
559
+ }
560
+ });
561
+
562
+ return {
563
+ grades,
564
+ };
565
+ }),
566
+ updateGrade: protectedTeacherProcedure
567
+ .input(z.object({
568
+ classId: z.string(),
569
+ assignmentId: z.string(),
570
+ submissionId: z.string(),
571
+ gradeReceived: z.number().nullable(),
572
+ }))
573
+ .mutation(async ({ ctx, input }) => {
574
+ const { classId, assignmentId, submissionId, gradeReceived } = input;
575
+
576
+ // Update the grade
577
+ const updatedSubmission = await prisma.submission.update({
578
+ where: {
579
+ id: submissionId,
580
+ assignmentId: assignmentId,
581
+ },
582
+ data: {
583
+ gradeReceived,
584
+ },
585
+ include: {
586
+ assignment: {
587
+ select: {
588
+ id: true,
589
+ title: true,
590
+ maxGrade: true,
591
+ weight: true,
592
+ }
593
+ }
594
+ }
595
+ });
596
+
597
+ return updatedSubmission;
598
+ }),
599
+ getEvents: protectedTeacherProcedure
600
+ .input(z.object({
601
+ classId: z.string(),
602
+ }))
603
+ .query(async ({ ctx, input }) => {
604
+ const { classId } = input;
605
+
606
+ const events = await prisma.event.findMany({
607
+ where: {
608
+ class: {
609
+ id: classId,
610
+ }
611
+ },
612
+ select: {
613
+ name: true,
614
+ startTime: true,
615
+ endTime: true,
616
+ }
617
+ });
618
+
619
+ return events;
620
+ }),
621
+ listMarkSchemes: protectedTeacherProcedure
622
+ .input(z.object({
623
+ classId: z.string(),
624
+ }))
625
+ .query(async ({ ctx, input }) => {
626
+ const { classId } = input;
627
+
628
+ const markSchemes = await prisma.markScheme.findMany({
629
+ where: {
630
+ classId: classId,
631
+ },
632
+ });
633
+
634
+ return markSchemes;
635
+ }),
636
+ createMarkScheme: protectedTeacherProcedure
637
+ .input(z.object({
638
+ classId: z.string(),
639
+ structure: z.string(),
640
+ }))
641
+ .mutation(async ({ ctx, input }) => {
642
+ const { classId, structure } = input;
643
+
644
+ const validatedStructure = structure.replace(/\\n/g, '\n');
645
+
646
+ const markScheme = await prisma.markScheme.create({
647
+ data: {
648
+ classId: classId,
649
+ structured: validatedStructure,
650
+ },
651
+ });
652
+
653
+ return markScheme;
654
+ }),
655
+ updateMarkScheme: protectedTeacherProcedure
656
+ .input(z.object({
657
+ classId: z.string(),
658
+ markSchemeId: z.string(),
659
+ structure: z.string(),
660
+ }))
661
+ .mutation(async ({ ctx, input }) => {
662
+ const { classId, markSchemeId, structure } = input;
663
+
664
+ const validatedStructure = structure.replace(/\\n/g, '\n');
665
+
666
+ const markScheme = await prisma.markScheme.update({
667
+ where: { id: markSchemeId },
668
+ data: { structured: validatedStructure },
669
+ });
670
+
671
+ return markScheme;
672
+ }),
673
+ deleteMarkScheme: protectedTeacherProcedure
674
+ .input(z.object({
675
+ classId: z.string(),
676
+ markSchemeId: z.string(),
677
+ }))
678
+ .mutation(async ({ ctx, input }) => {
679
+ const { classId, markSchemeId } = input;
680
+
681
+ const markScheme = await prisma.markScheme.delete({
682
+ where: { id: markSchemeId },
683
+ });
684
+
685
+ return markScheme;
686
+ }),
687
+ listGradingBoundaries: protectedTeacherProcedure
688
+ .input(z.object({
689
+ classId: z.string(),
690
+ }))
691
+ .query(async ({ ctx, input }) => {
692
+ const { classId } = input;
693
+
694
+ const gradingBoundaries = await prisma.gradingBoundary.findMany({
695
+ where: {
696
+ classId: classId,
697
+ },
698
+ });
699
+
700
+ return gradingBoundaries;
701
+ }),
702
+ createGradingBoundary: protectedTeacherProcedure
703
+ .input(z.object({
704
+ classId: z.string(),
705
+ structure: z.string(),
706
+ }))
707
+ .mutation(async ({ ctx, input }) => {
708
+ const { classId, structure } = input;
709
+
710
+ const validatedStructure = structure.replace(/\\n/g, '\n');
711
+
712
+ const gradingBoundary = await prisma.gradingBoundary.create({
713
+ data: {
714
+ classId: classId,
715
+ structured: validatedStructure,
716
+ },
717
+ });
718
+
719
+ return gradingBoundary;
720
+ }),
721
+ updateGradingBoundary: protectedTeacherProcedure
722
+ .input(z.object({
723
+ classId: z.string(),
724
+ gradingBoundaryId: z.string(),
725
+ structure: z.string(),
726
+ }))
727
+ .mutation(async ({ ctx, input }) => {
728
+ const { classId, gradingBoundaryId, structure } = input;
729
+
730
+ const validatedStructure = structure.replace(/\\n/g, '\n');
731
+
732
+ const gradingBoundary = await prisma.gradingBoundary.update({
733
+ where: { id: gradingBoundaryId },
734
+ data: { structured: validatedStructure },
735
+ });
736
+
737
+ return gradingBoundary;
738
+ }),
739
+ deleteGradingBoundary: protectedTeacherProcedure
740
+ .input(z.object({
741
+ classId: z.string(),
742
+ gradingBoundaryId: z.string(),
743
+ }))
744
+ .mutation(async ({ ctx, input }) => {
745
+ const { classId, gradingBoundaryId } = input;
746
+
747
+ const gradingBoundary = await prisma.gradingBoundary.delete({
748
+ where: { id: gradingBoundaryId },
749
+ });
750
+
751
+ return gradingBoundary;
752
+ }),
753
+ });