@studious-lms/server 1.4.5 → 1.4.7

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 (167) hide show
  1. package/dist/lib/config/cors.d.ts.map +1 -1
  2. package/dist/lib/config/cors.js +8 -3
  3. package/dist/lib/config/cors.js.map +1 -1
  4. package/dist/models/access.d.ts +112 -0
  5. package/dist/models/access.d.ts.map +1 -0
  6. package/dist/models/access.js +49 -0
  7. package/dist/models/access.js.map +1 -0
  8. package/dist/models/announcement.d.ts +17 -2
  9. package/dist/models/announcement.d.ts.map +1 -1
  10. package/dist/models/announcement.js +21 -4
  11. package/dist/models/announcement.js.map +1 -1
  12. package/dist/models/assignment.d.ts +37 -37
  13. package/dist/models/assignment.d.ts.map +1 -1
  14. package/dist/models/assignment.js +15 -15
  15. package/dist/models/assignment.js.map +1 -1
  16. package/dist/models/attendance.d.ts +1 -1
  17. package/dist/models/attendance.d.ts.map +1 -1
  18. package/dist/models/attendance.js +6 -5
  19. package/dist/models/attendance.js.map +1 -1
  20. package/dist/models/auth.d.ts +5 -5
  21. package/dist/models/auth.d.ts.map +1 -1
  22. package/dist/models/auth.js +7 -11
  23. package/dist/models/auth.js.map +1 -1
  24. package/dist/models/class.d.ts +26 -26
  25. package/dist/models/class.d.ts.map +1 -1
  26. package/dist/models/class.js +7 -11
  27. package/dist/models/class.js.map +1 -1
  28. package/dist/models/comment.d.ts +233 -6
  29. package/dist/models/comment.d.ts.map +1 -1
  30. package/dist/models/comment.js +133 -3
  31. package/dist/models/comment.js.map +1 -1
  32. package/dist/models/conversation.d.ts +7 -7
  33. package/dist/models/event.d.ts +52 -3
  34. package/dist/models/event.d.ts.map +1 -1
  35. package/dist/models/event.js +56 -6
  36. package/dist/models/event.js.map +1 -1
  37. package/dist/models/file.d.ts +178 -103
  38. package/dist/models/file.d.ts.map +1 -1
  39. package/dist/models/file.js +54 -14
  40. package/dist/models/file.js.map +1 -1
  41. package/dist/models/folder.d.ts +22 -22
  42. package/dist/models/folder.d.ts.map +1 -1
  43. package/dist/models/folder.js +5 -4
  44. package/dist/models/folder.js.map +1 -1
  45. package/dist/models/labChat.d.ts +20 -20
  46. package/dist/models/labChat.d.ts.map +1 -1
  47. package/dist/models/labChat.js +8 -10
  48. package/dist/models/labChat.js.map +1 -1
  49. package/dist/models/marketing.d.ts +1 -1
  50. package/dist/models/message.d.ts +6 -6
  51. package/dist/models/newtonChat.d.ts +5 -5
  52. package/dist/models/school.d.ts +361 -0
  53. package/dist/models/school.d.ts.map +1 -0
  54. package/dist/models/school.js +314 -0
  55. package/dist/models/school.js.map +1 -0
  56. package/dist/models/section.d.ts +1 -1
  57. package/dist/models/section.d.ts.map +1 -1
  58. package/dist/models/section.js +5 -4
  59. package/dist/models/section.js.map +1 -1
  60. package/dist/models/user.d.ts +4 -4
  61. package/dist/models/worksheet.d.ts +67 -65
  62. package/dist/models/worksheet.d.ts.map +1 -1
  63. package/dist/models/worksheet.js +6 -14
  64. package/dist/models/worksheet.js.map +1 -1
  65. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  66. package/dist/pipelines/aiLabChat.js +4 -3
  67. package/dist/pipelines/aiLabChat.js.map +1 -1
  68. package/dist/pipelines/gradeWorksheet.d.ts +2 -2
  69. package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
  70. package/dist/routers/_app.d.ts +2462 -1104
  71. package/dist/routers/_app.d.ts.map +1 -1
  72. package/dist/routers/_app.js +4 -2
  73. package/dist/routers/_app.js.map +1 -1
  74. package/dist/routers/announcement.d.ts +13 -5
  75. package/dist/routers/announcement.d.ts.map +1 -1
  76. package/dist/routers/announcement.js +25 -5
  77. package/dist/routers/announcement.js.map +1 -1
  78. package/dist/routers/assignment.d.ts +266 -36
  79. package/dist/routers/assignment.d.ts.map +1 -1
  80. package/dist/routers/assignment.js +221 -3
  81. package/dist/routers/assignment.js.map +1 -1
  82. package/dist/routers/class.d.ts +21 -20
  83. package/dist/routers/class.d.ts.map +1 -1
  84. package/dist/routers/class.js +3 -2
  85. package/dist/routers/class.js.map +1 -1
  86. package/dist/routers/comment.d.ts +40 -3
  87. package/dist/routers/comment.d.ts.map +1 -1
  88. package/dist/routers/comment.js +14 -5
  89. package/dist/routers/comment.js.map +1 -1
  90. package/dist/routers/conversation.d.ts +6 -6
  91. package/dist/routers/event.d.ts +21 -2
  92. package/dist/routers/event.d.ts.map +1 -1
  93. package/dist/routers/file.d.ts +2 -2
  94. package/dist/routers/folder.d.ts +24 -24
  95. package/dist/routers/labChat.d.ts +7 -7
  96. package/dist/routers/school.d.ts +384 -0
  97. package/dist/routers/school.d.ts.map +1 -0
  98. package/dist/routers/school.js +79 -0
  99. package/dist/routers/school.js.map +1 -0
  100. package/dist/routers/search.d.ts +1 -1
  101. package/dist/routers/worksheet.d.ts +23 -21
  102. package/dist/routers/worksheet.d.ts.map +1 -1
  103. package/dist/services/announcement.d.ts.map +1 -1
  104. package/dist/services/announcement.js +10 -6
  105. package/dist/services/announcement.js.map +1 -1
  106. package/dist/services/assignment.d.ts +36 -36
  107. package/dist/services/class.d.ts +20 -19
  108. package/dist/services/class.d.ts.map +1 -1
  109. package/dist/services/class.js +22 -3
  110. package/dist/services/class.js.map +1 -1
  111. package/dist/services/comment.d.ts +31 -5
  112. package/dist/services/comment.d.ts.map +1 -1
  113. package/dist/services/comment.js +102 -19
  114. package/dist/services/comment.js.map +1 -1
  115. package/dist/services/conversation.d.ts +6 -6
  116. package/dist/services/event.d.ts +21 -2
  117. package/dist/services/event.d.ts.map +1 -1
  118. package/dist/services/event.js +11 -7
  119. package/dist/services/event.js.map +1 -1
  120. package/dist/services/file.d.ts +2 -2
  121. package/dist/services/file.d.ts.map +1 -1
  122. package/dist/services/file.js +17 -8
  123. package/dist/services/file.js.map +1 -1
  124. package/dist/services/folder.d.ts +24 -24
  125. package/dist/services/labChat.d.ts +7 -7
  126. package/dist/services/school.d.ts +299 -0
  127. package/dist/services/school.d.ts.map +1 -0
  128. package/dist/services/school.js +225 -0
  129. package/dist/services/school.js.map +1 -0
  130. package/dist/services/worksheet.d.ts +23 -21
  131. package/dist/services/worksheet.d.ts.map +1 -1
  132. package/package.json +1 -1
  133. package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +60 -0
  134. package/prisma/migrations/20260504071500_school_memberships/migration.sql +81 -0
  135. package/prisma/schema.prisma +75 -2
  136. package/src/lib/config/cors.ts +8 -1
  137. package/src/models/access.ts +48 -0
  138. package/src/models/announcement.ts +19 -2
  139. package/src/models/assignment.ts +17 -13
  140. package/src/models/attendance.ts +4 -3
  141. package/src/models/auth.ts +8 -9
  142. package/src/models/class.ts +8 -9
  143. package/src/models/comment.ts +137 -1
  144. package/src/models/event.ts +54 -4
  145. package/src/models/file.ts +53 -12
  146. package/src/models/folder.ts +3 -2
  147. package/src/models/labChat.ts +9 -8
  148. package/src/models/school.ts +361 -0
  149. package/src/models/section.ts +3 -2
  150. package/src/models/worksheet.ts +7 -12
  151. package/src/pipelines/aiLabChat.ts +2 -1
  152. package/src/routers/_app.ts +2 -0
  153. package/src/routers/announcement.ts +30 -4
  154. package/src/routers/assignment.ts +243 -2
  155. package/src/routers/class.ts +1 -0
  156. package/src/routers/comment.ts +19 -2
  157. package/src/routers/school.ts +122 -0
  158. package/src/services/announcement.ts +12 -4
  159. package/src/services/class.ts +25 -2
  160. package/src/services/comment.ts +122 -18
  161. package/src/services/event.ts +23 -6
  162. package/src/services/file.ts +20 -6
  163. package/src/services/school.ts +308 -0
  164. package/tests/lib/cors.test.ts +13 -1
  165. package/tests/routers/assignment.test.ts +68 -2
  166. package/tests/routers/comment.test.ts +42 -1
  167. package/vercel.json +8 -0
@@ -6,6 +6,10 @@ import { createDirectUploadFiles, type DirectUploadFile, confirmDirectUpload, up
6
6
  import { deleteFile } from "../lib/googleCloudStorage.js";
7
7
  import { logger } from "../utils/logger.js";
8
8
  import { gradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
9
+ import {
10
+ classMemberOrSchoolAdminWhere,
11
+ classTeacherOrSchoolAdminWhere,
12
+ } from "../models/access.js";
9
13
  import {
10
14
  assignmentExists,
11
15
  getDueToday,
@@ -172,6 +176,66 @@ const updateUploadProgressSchema = z.object({
172
176
  progress: z.number().min(0).max(100),
173
177
  });
174
178
 
179
+ const assignmentCommentSchema = z.object({
180
+ assignmentId: z.string(),
181
+ classId: z.string(),
182
+ content: z.string().min(1, "Comment cannot be empty"),
183
+ parentCommentId: z.string().optional(),
184
+ });
185
+
186
+ const assignmentCommentsSchema = z.object({
187
+ assignmentId: z.string(),
188
+ classId: z.string(),
189
+ });
190
+
191
+ const submissionCommentSchema = z.object({
192
+ assignmentId: z.string(),
193
+ classId: z.string(),
194
+ submissionId: z.string(),
195
+ content: z.string().min(1, "Comment cannot be empty"),
196
+ parentCommentId: z.string().optional(),
197
+ });
198
+
199
+ const submissionCommentsSchema = z.object({
200
+ assignmentId: z.string(),
201
+ classId: z.string(),
202
+ submissionId: z.string(),
203
+ });
204
+
205
+ const commentInclude = {
206
+ author: {
207
+ select: {
208
+ id: true,
209
+ username: true,
210
+ profile: {
211
+ select: {
212
+ displayName: true,
213
+ profilePicture: true,
214
+ profilePictureThumbnail: true,
215
+ },
216
+ },
217
+ },
218
+ },
219
+ replies: {
220
+ include: {
221
+ author: {
222
+ select: {
223
+ id: true,
224
+ username: true,
225
+ profile: {
226
+ select: {
227
+ displayName: true,
228
+ profilePicture: true,
229
+ profilePictureThumbnail: true,
230
+ },
231
+ },
232
+ },
233
+ },
234
+ },
235
+ orderBy: { createdAt: "asc" as const },
236
+ },
237
+ } as const;
238
+
175
239
  export const assignmentRouter = createTRPCRouter({
176
240
  reorder: protectedTeacherProcedure
177
241
  .input(z.object({
@@ -254,6 +318,165 @@ export const assignmentRouter = createTRPCRouter({
254
318
  .input(z.object({ assignmentId: z.string(), classId: z.string() }))
255
319
  .query(({ ctx, input }) => getSubmissions(input.assignmentId, ctx.user!.id)),
256
320
 
321
+ getComments: protectedClassMemberProcedure
322
+ .input(assignmentCommentsSchema)
323
+ .query(async ({ input }) => {
324
+ const assignment = await prisma.assignment.findFirst({
325
+ where: { id: input.assignmentId, classId: input.classId },
326
+ select: { id: true },
327
+ });
328
+ if (!assignment) {
329
+ throw new TRPCError({
330
+ code: "NOT_FOUND",
331
+ message: "Assignment not found",
332
+ });
333
+ }
334
+
335
+ const comments = await prisma.comment.findMany({
336
+ where: {
337
+ assignmentId: input.assignmentId,
338
+ submissionId: null,
339
+ parentCommentId: null,
340
+ },
341
+ include: commentInclude,
342
+ orderBy: { createdAt: "asc" },
343
+ });
344
+
345
+ return { comments };
346
+ }),
347
+
348
+ addComment: protectedClassMemberProcedure
349
+ .input(assignmentCommentSchema)
350
+ .mutation(async ({ ctx, input }) => {
351
+ const assignment = await prisma.assignment.findFirst({
352
+ where: { id: input.assignmentId, classId: input.classId },
353
+ select: { id: true },
354
+ });
355
+ if (!assignment) {
356
+ throw new TRPCError({
357
+ code: "NOT_FOUND",
358
+ message: "Assignment not found",
359
+ });
360
+ }
361
+
362
+ if (input.parentCommentId) {
363
+ const parentComment = await prisma.comment.findFirst({
364
+ where: {
365
+ id: input.parentCommentId,
366
+ assignmentId: input.assignmentId,
367
+ submissionId: null,
368
+ },
369
+ select: { id: true },
370
+ });
371
+ if (!parentComment) {
372
+ throw new TRPCError({
373
+ code: "NOT_FOUND",
374
+ message: "Parent comment not found",
375
+ });
376
+ }
377
+ }
378
+
379
+ const comment = await prisma.comment.create({
380
+ data: {
381
+ content: input.content,
382
+ authorId: ctx.user!.id,
383
+ assignmentId: input.assignmentId,
384
+ ...(input.parentCommentId && { parentCommentId: input.parentCommentId }),
385
+ },
386
+ include: commentInclude,
387
+ });
388
+
389
+ return { comment };
390
+ }),
391
+
392
+ getSubmissionComments: protectedClassMemberProcedure
393
+ .input(submissionCommentsSchema)
394
+ .query(async ({ ctx, input }) => {
395
+ const submission = await prisma.submission.findFirst({
396
+ where: {
397
+ id: input.submissionId,
398
+ assignmentId: input.assignmentId,
399
+ assignment: {
400
+ classId: input.classId,
401
+ class: classMemberOrSchoolAdminWhere(ctx.user!.id),
402
+ },
403
+ OR: [
404
+ { studentId: ctx.user!.id },
405
+ { assignment: { class: classTeacherOrSchoolAdminWhere(ctx.user!.id) } },
406
+ ],
407
+ },
408
+ select: { id: true },
409
+ });
410
+ if (!submission) {
411
+ throw new TRPCError({
412
+ code: "NOT_FOUND",
413
+ message: "Submission not found",
414
+ });
415
+ }
416
+
417
+ const comments = await prisma.comment.findMany({
418
+ where: {
419
+ submissionId: input.submissionId,
420
+ parentCommentId: null,
421
+ },
422
+ include: commentInclude,
423
+ orderBy: { createdAt: "asc" },
424
+ });
425
+
426
+ return { comments };
427
+ }),
428
+
429
+ addSubmissionComment: protectedClassMemberProcedure
430
+ .input(submissionCommentSchema)
431
+ .mutation(async ({ ctx, input }) => {
432
+ const submission = await prisma.submission.findFirst({
433
+ where: {
434
+ id: input.submissionId,
435
+ assignmentId: input.assignmentId,
436
+ assignment: { classId: input.classId },
437
+ OR: [
438
+ { studentId: ctx.user!.id },
439
+ { assignment: { class: classTeacherOrSchoolAdminWhere(ctx.user!.id) } },
440
+ ],
441
+ },
442
+ select: { id: true },
443
+ });
444
+ if (!submission) {
445
+ throw new TRPCError({
446
+ code: "NOT_FOUND",
447
+ message: "Submission not found",
448
+ });
449
+ }
450
+
451
+ if (input.parentCommentId) {
452
+ const parentComment = await prisma.comment.findFirst({
453
+ where: {
454
+ id: input.parentCommentId,
455
+ submissionId: input.submissionId,
456
+ },
457
+ select: { id: true },
458
+ });
459
+ if (!parentComment) {
460
+ throw new TRPCError({
461
+ code: "NOT_FOUND",
462
+ message: "Parent comment not found",
463
+ });
464
+ }
465
+ }
466
+
467
+ const comment = await prisma.comment.create({
468
+ data: {
469
+ content: input.content,
470
+ authorId: ctx.user!.id,
471
+ submissionId: input.submissionId,
472
+ ...(input.parentCommentId && { parentCommentId: input.parentCommentId }),
473
+ },
474
+ include: commentInclude,
475
+ });
476
+
477
+ return { comment };
478
+ }),
479
+
257
480
  updateSubmissionAsTeacher: protectedTeacherProcedure
258
481
  .input(updateSubmissionSchema)
259
482
  .mutation(({ ctx, input }) =>
@@ -406,6 +629,21 @@ export const assignmentRouter = createTRPCRouter({
406
629
  include: {
407
630
  students: true,
408
631
  teachers: true,
632
+ school: {
633
+ include: {
634
+ users: {
635
+ where: { role: "ADMIN" },
636
+ select: { id: true },
637
+ },
638
+ memberships: {
639
+ where: {
640
+ role: "ADMIN",
641
+ status: "ACTIVE",
642
+ },
643
+ select: { userId: true },
644
+ },
645
+ },
646
+ },
409
647
  },
410
648
  },
411
649
  },
@@ -423,8 +661,12 @@ export const assignmentRouter = createTRPCRouter({
423
661
  // Check if user is the student who owns the submission or a teacher of the class
424
662
  const isStudent = submission.studentId === ctx.user.id;
425
663
  const isTeacher = submission.assignment.class.teachers.some(teacher => teacher.id === ctx.user?.id);
664
+ const isSchoolAdmin = Boolean(
665
+ submission.assignment.class.school?.memberships.some(admin => admin.userId === ctx.user?.id) ||
666
+ submission.assignment.class.school?.users.some(admin => admin.id === ctx.user?.id)
667
+ );
426
668
 
427
- if (!isStudent && !isTeacher) {
669
+ if (!isStudent && !isTeacher && !isSchoolAdmin) {
428
670
  throw new TRPCError({
429
671
  code: "FORBIDDEN",
430
672
  message: "You don't have permission to upload files to this submission",
@@ -660,4 +902,3 @@ export const assignmentRouter = createTRPCRouter({
660
902
  };
661
903
  }),
662
904
  });
663
-
@@ -74,6 +74,7 @@ export const classRouter = createTRPCRouter({
74
74
  section: z.string(),
75
75
  subject: z.string(),
76
76
  color: z.string().optional(),
77
+ schoolId: z.string().optional(),
77
78
  })
78
79
  )
79
80
  .mutation(({ ctx, input }) =>
@@ -5,6 +5,8 @@ import {
5
5
  getComment,
6
6
  getReplies,
7
7
  replyToComment,
8
+ updateComment,
9
+ deleteComment,
8
10
  addReaction,
9
11
  removeReaction,
10
12
  getReactions,
@@ -15,11 +17,11 @@ export { commentSelect } from "../models/comment.js";
15
17
  export const commentRouter = createTRPCRouter({
16
18
  get: protectedProcedure
17
19
  .input(z.object({ id: z.string() }))
18
- .query(({ input }) => getComment(input.id)),
20
+ .query(({ ctx, input }) => getComment(ctx.user!.id, input.id)),
19
21
 
20
22
  getReplies: protectedProcedure
21
23
  .input(z.object({ commentId: z.string() }))
22
- .query(({ input }) => getReplies(input.commentId)),
24
+ .query(({ ctx, input }) => getReplies(ctx.user!.id, input.commentId)),
23
25
 
24
26
  replyToComment: protectedProcedure
25
27
  .input(
@@ -32,6 +34,21 @@ export const commentRouter = createTRPCRouter({
32
34
  replyToComment(ctx.user!.id, input.parentCommentId, input.content)
33
35
  ),
34
36
 
37
+ update: protectedProcedure
38
+ .input(
39
+ z.object({
40
+ id: z.string(),
41
+ content: z.string().min(1, "Comment cannot be empty"),
42
+ })
43
+ )
44
+ .mutation(({ ctx, input }) =>
45
+ updateComment(ctx.user!.id, input.id, input.content)
46
+ ),
47
+
48
+ delete: protectedProcedure
49
+ .input(z.object({ id: z.string() }))
50
+ .mutation(({ ctx, input }) => deleteComment(ctx.user!.id, input.id)),
51
+
35
52
  addReaction: protectedProcedure
36
53
  .input(
37
54
  z.object({
@@ -0,0 +1,122 @@
1
+ import { z } from "zod";
2
+ import {
3
+ createTRPCRouter,
4
+ protectedProcedure,
5
+ publicProcedure,
6
+ } from "../trpc.js";
7
+ import {
8
+ acceptInvite,
9
+ addMember,
10
+ createInvite,
11
+ createSchoolClass,
12
+ createSchoolRecord,
13
+ getAdminOverview,
14
+ getCurrentSchool,
15
+ getInvite,
16
+ listClasses,
17
+ listInvites,
18
+ listMembers,
19
+ removeMember,
20
+ revokeInvite,
21
+ updateMemberRole,
22
+ } from "../services/school.js";
23
+
24
+ const schoolRoleSchema = z.enum(["ADMIN", "TEACHER", "STUDENT"]);
25
+
26
+ const schoolIdSchema = z.object({
27
+ schoolId: z.string(),
28
+ });
29
+
30
+ export const schoolRouter = createTRPCRouter({
31
+ getCurrent: protectedProcedure
32
+ .input(z.object({ schoolId: z.string().optional() }).optional())
33
+ .query(({ ctx, input }) =>
34
+ getCurrentSchool(ctx.user!.id, input?.schoolId)
35
+ ),
36
+
37
+ create: protectedProcedure
38
+ .input(
39
+ z.object({
40
+ name: z.string().min(1),
41
+ subdomain: z.string().min(1).optional(),
42
+ logoId: z.string().optional(),
43
+ })
44
+ )
45
+ .mutation(({ ctx, input }) => createSchoolRecord(ctx.user!.id, input)),
46
+
47
+ getAdminOverview: protectedProcedure
48
+ .input(schoolIdSchema)
49
+ .query(({ ctx, input }) => getAdminOverview(ctx.user!.id, input.schoolId)),
50
+
51
+ listClasses: protectedProcedure
52
+ .input(schoolIdSchema)
53
+ .query(({ ctx, input }) => listClasses(ctx.user!.id, input.schoolId)),
54
+
55
+ createClass: protectedProcedure
56
+ .input(
57
+ z.object({
58
+ schoolId: z.string(),
59
+ name: z.string(),
60
+ section: z.string(),
61
+ subject: z.string(),
62
+ color: z.string().optional(),
63
+ teachers: z.array(z.string()).optional(),
64
+ students: z.array(z.string()).optional(),
65
+ })
66
+ )
67
+ .mutation(({ ctx, input }) => createSchoolClass(ctx.user!.id, input)),
68
+
69
+ listMembers: protectedProcedure
70
+ .input(schoolIdSchema)
71
+ .query(({ ctx, input }) => listMembers(ctx.user!.id, input.schoolId)),
72
+
73
+ addMember: protectedProcedure
74
+ .input(
75
+ z.object({
76
+ schoolId: z.string(),
77
+ memberUserId: z.string(),
78
+ role: schoolRoleSchema,
79
+ })
80
+ )
81
+ .mutation(({ ctx, input }) => addMember(ctx.user!.id, input)),
82
+
83
+ updateMemberRole: protectedProcedure
84
+ .input(
85
+ z.object({
86
+ schoolId: z.string(),
87
+ memberUserId: z.string(),
88
+ role: schoolRoleSchema,
89
+ })
90
+ )
91
+ .mutation(({ ctx, input }) => updateMemberRole(ctx.user!.id, input)),
92
+
93
+ removeMember: protectedProcedure
94
+ .input(z.object({ schoolId: z.string(), memberUserId: z.string() }))
95
+ .mutation(({ ctx, input }) => removeMember(ctx.user!.id, input)),
96
+
97
+ listInvites: protectedProcedure
98
+ .input(schoolIdSchema)
99
+ .query(({ ctx, input }) => listInvites(ctx.user!.id, input.schoolId)),
100
+
101
+ createInvite: protectedProcedure
102
+ .input(
103
+ z.object({
104
+ schoolId: z.string(),
105
+ email: z.string().email(),
106
+ role: schoolRoleSchema,
107
+ })
108
+ )
109
+ .mutation(({ ctx, input }) => createInvite(ctx.user!.id, input)),
110
+
111
+ revokeInvite: protectedProcedure
112
+ .input(z.object({ schoolId: z.string(), inviteId: z.string() }))
113
+ .mutation(({ ctx, input }) => revokeInvite(ctx.user!.id, input)),
114
+
115
+ getInvite: publicProcedure
116
+ .input(z.object({ token: z.string() }))
117
+ .query(({ input }) => getInvite(input.token)),
118
+
119
+ acceptInvite: protectedProcedure
120
+ .input(z.object({ token: z.string() }))
121
+ .mutation(({ ctx, input }) => acceptInvite(ctx.user!.id, input.token)),
122
+ });
@@ -157,11 +157,15 @@ export async function updateAnnouncementRecord(
157
157
 
158
158
  const isCreator = announcement.teacherId === userId;
159
159
  const isClassTeacher = announcement.class.teachers.some((t) => t.id === userId);
160
+ const isSchoolAdmin = Boolean(
161
+ announcement.class.school?.memberships.some((admin) => admin.userId === userId) ||
162
+ announcement.class.school?.users.some((admin) => admin.id === userId)
163
+ );
160
164
 
161
- if (!isCreator && !isClassTeacher) {
165
+ if (!isCreator && !isClassTeacher && !isSchoolAdmin) {
162
166
  throw new TRPCError({
163
167
  code: "FORBIDDEN",
164
- message: "Only the announcement creator or class teachers can update announcements",
168
+ message: "Only the announcement creator, class teachers, or school admins can update announcements",
165
169
  });
166
170
  }
167
171
 
@@ -277,11 +281,15 @@ export async function deleteAnnouncementRecord(userId: string, id: string, class
277
281
 
278
282
  const isCreator = announcement.teacherId === userId;
279
283
  const isClassTeacher = announcement.class.teachers.some((t) => t.id === userId);
284
+ const isSchoolAdmin = Boolean(
285
+ announcement.class.school?.memberships.some((admin) => admin.userId === userId) ||
286
+ announcement.class.school?.users.some((admin) => admin.id === userId)
287
+ );
280
288
 
281
- if (!isCreator && !isClassTeacher) {
289
+ if (!isCreator && !isClassTeacher && !isSchoolAdmin) {
282
290
  throw new TRPCError({
283
291
  code: "FORBIDDEN",
284
- message: "Only the announcement creator or class teachers can delete announcements",
292
+ message: "Only the announcement creator, class teachers, or school admins can delete announcements",
285
293
  });
286
294
  }
287
295
 
@@ -25,6 +25,7 @@ import {
25
25
  findFullExportableClass,
26
26
  createClassByImport,
27
27
  } from "../models/class.js";
28
+ import { listUserSchoolMemberships } from "../models/school.js";
28
29
  import { Class, Folder } from "@prisma/client";
29
30
 
30
31
  /** Get all classes where user is teacher or student, with assignments and members. */
@@ -121,9 +122,29 @@ export async function createClass(
121
122
  section: string;
122
123
  subject: string;
123
124
  color?: string;
125
+ schoolId?: string;
124
126
  }
125
127
  ) {
126
- const { students, teachers, name, section, subject, color } = input;
128
+ const { students, teachers, name, section, subject, color, schoolId } = input;
129
+
130
+ if (schoolId) {
131
+ const memberships = await listUserSchoolMemberships(userId);
132
+ const membership = memberships.find((item) => item.schoolId === schoolId);
133
+
134
+ if (!membership) {
135
+ throw new TRPCError({
136
+ code: "FORBIDDEN",
137
+ message: "You are not a member of this school",
138
+ });
139
+ }
140
+
141
+ if (membership.role === "STUDENT") {
142
+ throw new TRPCError({
143
+ code: "FORBIDDEN",
144
+ message: "Students cannot create school classes",
145
+ });
146
+ }
147
+ }
127
148
 
128
149
  if (teachers && teachers.length > 0 && students && students.length > 0) {
129
150
  return prisma.class.create({
@@ -132,6 +153,7 @@ export async function createClass(
132
153
  section,
133
154
  subject,
134
155
  color,
156
+ schoolId,
135
157
  teachers: { connect: teachers.map((id) => ({ id })) },
136
158
  students: { connect: students.map((id) => ({ id })) },
137
159
  },
@@ -145,6 +167,7 @@ export async function createClass(
145
167
  section,
146
168
  subject,
147
169
  color,
170
+ schoolId,
148
171
  teachers: { connect: { id: userId } },
149
172
  },
150
173
  });
@@ -629,4 +652,4 @@ export async function importClass(classId: string, userId: string, year: number,
629
652
  cause: err,
630
653
  });
631
654
  }
632
- }
655
+ }