@studious-lms/server 1.4.6 → 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 (148) hide show
  1. package/dist/models/access.d.ts +112 -0
  2. package/dist/models/access.d.ts.map +1 -0
  3. package/dist/models/access.js +49 -0
  4. package/dist/models/access.js.map +1 -0
  5. package/dist/models/announcement.d.ts +17 -2
  6. package/dist/models/announcement.d.ts.map +1 -1
  7. package/dist/models/announcement.js +21 -4
  8. package/dist/models/announcement.js.map +1 -1
  9. package/dist/models/assignment.d.ts +37 -37
  10. package/dist/models/assignment.d.ts.map +1 -1
  11. package/dist/models/assignment.js +15 -15
  12. package/dist/models/assignment.js.map +1 -1
  13. package/dist/models/attendance.d.ts +1 -1
  14. package/dist/models/attendance.d.ts.map +1 -1
  15. package/dist/models/attendance.js +6 -5
  16. package/dist/models/attendance.js.map +1 -1
  17. package/dist/models/auth.d.ts +5 -5
  18. package/dist/models/auth.d.ts.map +1 -1
  19. package/dist/models/auth.js +7 -11
  20. package/dist/models/auth.js.map +1 -1
  21. package/dist/models/class.d.ts +26 -26
  22. package/dist/models/class.d.ts.map +1 -1
  23. package/dist/models/class.js +7 -11
  24. package/dist/models/class.js.map +1 -1
  25. package/dist/models/comment.d.ts +134 -70
  26. package/dist/models/comment.d.ts.map +1 -1
  27. package/dist/models/comment.js +30 -42
  28. package/dist/models/comment.js.map +1 -1
  29. package/dist/models/conversation.d.ts +7 -7
  30. package/dist/models/event.d.ts +52 -3
  31. package/dist/models/event.d.ts.map +1 -1
  32. package/dist/models/event.js +56 -6
  33. package/dist/models/event.js.map +1 -1
  34. package/dist/models/file.d.ts +178 -103
  35. package/dist/models/file.d.ts.map +1 -1
  36. package/dist/models/file.js +54 -14
  37. package/dist/models/file.js.map +1 -1
  38. package/dist/models/folder.d.ts +22 -22
  39. package/dist/models/folder.d.ts.map +1 -1
  40. package/dist/models/folder.js +5 -4
  41. package/dist/models/folder.js.map +1 -1
  42. package/dist/models/labChat.d.ts +20 -20
  43. package/dist/models/labChat.d.ts.map +1 -1
  44. package/dist/models/labChat.js +8 -10
  45. package/dist/models/labChat.js.map +1 -1
  46. package/dist/models/marketing.d.ts +1 -1
  47. package/dist/models/message.d.ts +6 -6
  48. package/dist/models/newtonChat.d.ts +5 -5
  49. package/dist/models/school.d.ts +361 -0
  50. package/dist/models/school.d.ts.map +1 -0
  51. package/dist/models/school.js +314 -0
  52. package/dist/models/school.js.map +1 -0
  53. package/dist/models/section.d.ts +1 -1
  54. package/dist/models/section.d.ts.map +1 -1
  55. package/dist/models/section.js +5 -4
  56. package/dist/models/section.js.map +1 -1
  57. package/dist/models/user.d.ts +4 -4
  58. package/dist/models/worksheet.d.ts +65 -65
  59. package/dist/models/worksheet.d.ts.map +1 -1
  60. package/dist/models/worksheet.js +6 -14
  61. package/dist/models/worksheet.js.map +1 -1
  62. package/dist/pipelines/gradeWorksheet.d.ts +2 -2
  63. package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
  64. package/dist/routers/_app.d.ts +2855 -2051
  65. package/dist/routers/_app.d.ts.map +1 -1
  66. package/dist/routers/_app.js +4 -2
  67. package/dist/routers/_app.js.map +1 -1
  68. package/dist/routers/announcement.d.ts +5 -5
  69. package/dist/routers/announcement.d.ts.map +1 -1
  70. package/dist/routers/announcement.js +25 -5
  71. package/dist/routers/announcement.js.map +1 -1
  72. package/dist/routers/assignment.d.ts +44 -44
  73. package/dist/routers/assignment.d.ts.map +1 -1
  74. package/dist/routers/assignment.js +24 -11
  75. package/dist/routers/assignment.js.map +1 -1
  76. package/dist/routers/class.d.ts +21 -20
  77. package/dist/routers/class.d.ts.map +1 -1
  78. package/dist/routers/class.js +3 -2
  79. package/dist/routers/class.js.map +1 -1
  80. package/dist/routers/comment.d.ts +5 -5
  81. package/dist/routers/conversation.d.ts +6 -6
  82. package/dist/routers/event.d.ts +21 -2
  83. package/dist/routers/event.d.ts.map +1 -1
  84. package/dist/routers/file.d.ts +2 -2
  85. package/dist/routers/folder.d.ts +24 -24
  86. package/dist/routers/labChat.d.ts +7 -7
  87. package/dist/routers/school.d.ts +384 -0
  88. package/dist/routers/school.d.ts.map +1 -0
  89. package/dist/routers/school.js +79 -0
  90. package/dist/routers/school.js.map +1 -0
  91. package/dist/routers/search.d.ts +1 -1
  92. package/dist/routers/worksheet.d.ts +21 -21
  93. package/dist/services/announcement.d.ts.map +1 -1
  94. package/dist/services/announcement.js +10 -6
  95. package/dist/services/announcement.js.map +1 -1
  96. package/dist/services/assignment.d.ts +36 -36
  97. package/dist/services/class.d.ts +20 -19
  98. package/dist/services/class.d.ts.map +1 -1
  99. package/dist/services/class.js +22 -3
  100. package/dist/services/class.js.map +1 -1
  101. package/dist/services/comment.d.ts +5 -5
  102. package/dist/services/comment.d.ts.map +1 -1
  103. package/dist/services/comment.js +8 -3
  104. package/dist/services/comment.js.map +1 -1
  105. package/dist/services/conversation.d.ts +6 -6
  106. package/dist/services/event.d.ts +21 -2
  107. package/dist/services/event.d.ts.map +1 -1
  108. package/dist/services/event.js +11 -7
  109. package/dist/services/event.js.map +1 -1
  110. package/dist/services/file.d.ts +2 -2
  111. package/dist/services/file.d.ts.map +1 -1
  112. package/dist/services/file.js +17 -8
  113. package/dist/services/file.js.map +1 -1
  114. package/dist/services/folder.d.ts +24 -24
  115. package/dist/services/labChat.d.ts +7 -7
  116. package/dist/services/school.d.ts +299 -0
  117. package/dist/services/school.d.ts.map +1 -0
  118. package/dist/services/school.js +225 -0
  119. package/dist/services/school.js.map +1 -0
  120. package/dist/services/worksheet.d.ts +21 -21
  121. package/package.json +1 -1
  122. package/prisma/migrations/20260504071500_school_memberships/migration.sql +81 -0
  123. package/prisma/schema.prisma +66 -2
  124. package/src/models/access.ts +48 -0
  125. package/src/models/announcement.ts +19 -2
  126. package/src/models/assignment.ts +17 -13
  127. package/src/models/attendance.ts +4 -3
  128. package/src/models/auth.ts +8 -9
  129. package/src/models/class.ts +8 -9
  130. package/src/models/comment.ts +29 -40
  131. package/src/models/event.ts +54 -4
  132. package/src/models/file.ts +53 -12
  133. package/src/models/folder.ts +3 -2
  134. package/src/models/labChat.ts +9 -8
  135. package/src/models/school.ts +361 -0
  136. package/src/models/section.ts +3 -2
  137. package/src/models/worksheet.ts +7 -12
  138. package/src/routers/_app.ts +2 -0
  139. package/src/routers/announcement.ts +30 -4
  140. package/src/routers/assignment.ts +27 -9
  141. package/src/routers/class.ts +1 -0
  142. package/src/routers/school.ts +122 -0
  143. package/src/services/announcement.ts +12 -4
  144. package/src/services/class.ts +25 -2
  145. package/src/services/comment.ts +10 -1
  146. package/src/services/event.ts +23 -6
  147. package/src/services/file.ts +20 -6
  148. package/src/services/school.ts +308 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * School model - school membership, invites, and admin lookups.
3
+ */
4
+ import { SchoolMembershipRole } from "@prisma/client";
5
+ import { prisma } from "../lib/prisma.js";
6
+
7
+ const memberSelect = {
8
+ id: true,
9
+ role: true,
10
+ status: true,
11
+ createdAt: true,
12
+ updatedAt: true,
13
+ user: {
14
+ select: {
15
+ id: true,
16
+ username: true,
17
+ email: true,
18
+ profile: {
19
+ select: {
20
+ displayName: true,
21
+ profilePicture: true,
22
+ profilePictureThumbnail: true,
23
+ },
24
+ },
25
+ },
26
+ },
27
+ } as const;
28
+
29
+ export function findSchoolMembership(schoolId: string, userId: string) {
30
+ return prisma.schoolMembership.findUnique({
31
+ where: { schoolId_userId: { schoolId, userId } },
32
+ });
33
+ }
34
+
35
+ export function findActiveSchoolMembership(schoolId: string, userId: string) {
36
+ return prisma.schoolMembership.findFirst({
37
+ where: { schoolId, userId, status: "ACTIVE" },
38
+ });
39
+ }
40
+
41
+ export function findActiveSchoolAdminMembership(schoolId: string, userId: string) {
42
+ return prisma.schoolMembership.findFirst({
43
+ where: { schoolId, userId, role: "ADMIN", status: "ACTIVE" },
44
+ });
45
+ }
46
+
47
+ export function listUserSchoolMemberships(userId: string) {
48
+ return prisma.schoolMembership.findMany({
49
+ where: { userId, status: "ACTIVE" },
50
+ include: {
51
+ school: {
52
+ select: {
53
+ id: true,
54
+ name: true,
55
+ subdomain: true,
56
+ logoId: true,
57
+ createdAt: true,
58
+ updatedAt: true,
59
+ },
60
+ },
61
+ },
62
+ orderBy: { createdAt: "asc" },
63
+ });
64
+ }
65
+
66
+ export function createSchoolWithAdmin(data: {
67
+ name: string;
68
+ subdomain?: string;
69
+ logoId?: string;
70
+ userId: string;
71
+ }) {
72
+ return prisma.school.create({
73
+ data: {
74
+ name: data.name,
75
+ subdomain: data.subdomain,
76
+ logoId: data.logoId,
77
+ memberships: {
78
+ create: {
79
+ userId: data.userId,
80
+ role: "ADMIN",
81
+ status: "ACTIVE",
82
+ },
83
+ },
84
+ },
85
+ include: {
86
+ memberships: {
87
+ where: { userId: data.userId },
88
+ },
89
+ },
90
+ });
91
+ }
92
+
93
+ export function getSchoolById(schoolId: string) {
94
+ return prisma.school.findUnique({
95
+ where: { id: schoolId },
96
+ select: {
97
+ id: true,
98
+ name: true,
99
+ subdomain: true,
100
+ logoId: true,
101
+ createdAt: true,
102
+ updatedAt: true,
103
+ },
104
+ });
105
+ }
106
+
107
+ export function getSchoolOverview(schoolId: string) {
108
+ return prisma.school.findUnique({
109
+ where: { id: schoolId },
110
+ select: {
111
+ id: true,
112
+ name: true,
113
+ subdomain: true,
114
+ _count: {
115
+ select: {
116
+ classes: true,
117
+ memberships: { where: { status: "ACTIVE" } },
118
+ invites: { where: { status: "PENDING" } },
119
+ },
120
+ },
121
+ memberships: {
122
+ where: { status: "ACTIVE" },
123
+ select: { role: true },
124
+ },
125
+ classes: {
126
+ orderBy: { name: "asc" },
127
+ take: 5,
128
+ select: {
129
+ id: true,
130
+ name: true,
131
+ subject: true,
132
+ section: true,
133
+ color: true,
134
+ _count: { select: { teachers: true, students: true } },
135
+ },
136
+ },
137
+ invites: {
138
+ where: { status: "PENDING" },
139
+ orderBy: { createdAt: "desc" },
140
+ take: 5,
141
+ select: {
142
+ id: true,
143
+ email: true,
144
+ role: true,
145
+ expiresAt: true,
146
+ createdAt: true,
147
+ },
148
+ },
149
+ },
150
+ });
151
+ }
152
+
153
+ export function listSchoolClasses(schoolId: string) {
154
+ return prisma.class.findMany({
155
+ where: { schoolId },
156
+ orderBy: [{ name: "asc" }, { section: "asc" }],
157
+ include: {
158
+ teachers: {
159
+ select: {
160
+ id: true,
161
+ username: true,
162
+ profile: {
163
+ select: {
164
+ displayName: true,
165
+ profilePicture: true,
166
+ profilePictureThumbnail: true,
167
+ },
168
+ },
169
+ },
170
+ },
171
+ students: { select: { id: true } },
172
+ _count: { select: { teachers: true, students: true, assignments: true } },
173
+ },
174
+ });
175
+ }
176
+
177
+ export function listSchoolMembers(schoolId: string) {
178
+ return prisma.schoolMembership.findMany({
179
+ where: { schoolId },
180
+ select: memberSelect,
181
+ orderBy: [{ role: "asc" }, { createdAt: "asc" }],
182
+ });
183
+ }
184
+
185
+ export function countActiveAdmins(schoolId: string) {
186
+ return prisma.schoolMembership.count({
187
+ where: { schoolId, role: "ADMIN", status: "ACTIVE" },
188
+ });
189
+ }
190
+
191
+ export function upsertSchoolMembership(data: {
192
+ schoolId: string;
193
+ userId: string;
194
+ role: SchoolMembershipRole;
195
+ }) {
196
+ return prisma.schoolMembership.upsert({
197
+ where: {
198
+ schoolId_userId: {
199
+ schoolId: data.schoolId,
200
+ userId: data.userId,
201
+ },
202
+ },
203
+ update: {
204
+ role: data.role,
205
+ status: "ACTIVE",
206
+ },
207
+ create: {
208
+ schoolId: data.schoolId,
209
+ userId: data.userId,
210
+ role: data.role,
211
+ status: "ACTIVE",
212
+ },
213
+ });
214
+ }
215
+
216
+ export function updateSchoolMembershipRole(
217
+ schoolId: string,
218
+ userId: string,
219
+ role: SchoolMembershipRole
220
+ ) {
221
+ return prisma.schoolMembership.update({
222
+ where: { schoolId_userId: { schoolId, userId } },
223
+ data: { role },
224
+ });
225
+ }
226
+
227
+ export function removeSchoolMembershipPreservingContent(schoolId: string, userId: string) {
228
+ return prisma.$transaction(async (tx) => {
229
+ const classes = await tx.class.findMany({
230
+ where: { schoolId },
231
+ select: { id: true },
232
+ });
233
+ const classIds = classes.map((cls) => ({ id: cls.id }));
234
+
235
+ for (const classWhere of classIds) {
236
+ await tx.class.update({
237
+ where: classWhere,
238
+ data: {
239
+ teachers: { disconnect: { id: userId } },
240
+ students: { disconnect: { id: userId } },
241
+ },
242
+ });
243
+ }
244
+
245
+ return tx.schoolMembership.delete({
246
+ where: { schoolId_userId: { schoolId, userId } },
247
+ });
248
+ });
249
+ }
250
+
251
+ export function findUserByEmailForSchool(email: string) {
252
+ return prisma.user.findFirst({
253
+ where: { email },
254
+ select: { id: true, email: true, username: true },
255
+ });
256
+ }
257
+
258
+ export function findUserByIdForSchool(userId: string) {
259
+ return prisma.user.findUnique({
260
+ where: { id: userId },
261
+ select: { id: true, email: true, username: true },
262
+ });
263
+ }
264
+
265
+ export function createSchoolInvite(data: {
266
+ schoolId: string;
267
+ invitedById: string;
268
+ email: string;
269
+ role: SchoolMembershipRole;
270
+ token: string;
271
+ expiresAt: Date;
272
+ }) {
273
+ return prisma.schoolInvite.create({
274
+ data,
275
+ include: {
276
+ school: { select: { id: true, name: true } },
277
+ },
278
+ });
279
+ }
280
+
281
+ export function listSchoolInvites(schoolId: string) {
282
+ return prisma.schoolInvite.findMany({
283
+ where: { schoolId },
284
+ orderBy: { createdAt: "desc" },
285
+ select: {
286
+ id: true,
287
+ email: true,
288
+ role: true,
289
+ status: true,
290
+ expiresAt: true,
291
+ acceptedAt: true,
292
+ revokedAt: true,
293
+ createdAt: true,
294
+ invitedBy: {
295
+ select: {
296
+ id: true,
297
+ username: true,
298
+ profile: { select: { displayName: true } },
299
+ },
300
+ },
301
+ },
302
+ });
303
+ }
304
+
305
+ export function revokeSchoolInvite(inviteId: string, schoolId: string) {
306
+ return prisma.schoolInvite.update({
307
+ where: { id: inviteId, schoolId },
308
+ data: {
309
+ status: "REVOKED",
310
+ revokedAt: new Date(),
311
+ },
312
+ });
313
+ }
314
+
315
+ export function findPendingInviteByToken(token: string) {
316
+ return prisma.schoolInvite.findUnique({
317
+ where: { token },
318
+ include: { school: { select: { id: true, name: true } } },
319
+ });
320
+ }
321
+
322
+ export function acceptSchoolInvite(inviteId: string, userId: string) {
323
+ return prisma.$transaction(async (tx) => {
324
+ const invite = await tx.schoolInvite.findUnique({
325
+ where: { id: inviteId },
326
+ });
327
+
328
+ if (!invite) {
329
+ throw new Error("Invite not found");
330
+ }
331
+
332
+ const membership = await tx.schoolMembership.upsert({
333
+ where: {
334
+ schoolId_userId: {
335
+ schoolId: invite.schoolId,
336
+ userId,
337
+ },
338
+ },
339
+ update: {
340
+ role: invite.role,
341
+ status: "ACTIVE",
342
+ },
343
+ create: {
344
+ schoolId: invite.schoolId,
345
+ userId,
346
+ role: invite.role,
347
+ status: "ACTIVE",
348
+ },
349
+ });
350
+
351
+ await tx.schoolInvite.update({
352
+ where: { id: inviteId },
353
+ data: {
354
+ status: "ACCEPTED",
355
+ acceptedAt: new Date(),
356
+ },
357
+ });
358
+
359
+ return membership;
360
+ });
361
+ }
@@ -2,6 +2,7 @@
2
2
  * Section model – class sections, ordering, assignment grouping.
3
3
  */
4
4
  import { prisma } from "../lib/prisma.js";
5
+ import { classTeacherOrSchoolAdminWhere } from "./access.js";
5
6
 
6
7
  /** @returns Section by ID. */
7
8
  export function findSectionById(id: string) {
@@ -10,12 +11,12 @@ export function findSectionById(id: string) {
10
11
  });
11
12
  }
12
13
 
13
- /** @returns Class if user is a teacher. */
14
+ /** @returns Class if user is a teacher or school admin. */
14
15
  export function findClassWithTeacher(classId: string, teacherId: string) {
15
16
  return prisma.class.findFirst({
16
17
  where: {
17
18
  id: classId,
18
- teachers: { some: { id: teacherId } },
19
+ ...classTeacherOrSchoolAdminWhere(teacherId),
19
20
  },
20
21
  });
21
22
  }
@@ -3,20 +3,17 @@
3
3
  */
4
4
  import { prisma } from "../lib/prisma.js";
5
5
  import type { WorksheetQuestionType, GenerationStatus } from "@prisma/client";
6
+ import {
7
+ classMemberOrSchoolAdminWhere,
8
+ classTeacherOrSchoolAdminWhere,
9
+ } from "./access.js";
6
10
 
7
11
  const worksheetClassMemberWhere = (userId: string) => ({
8
- class: {
9
- OR: [
10
- { teachers: { some: { id: userId } } },
11
- { students: { some: { id: userId } } },
12
- ],
13
- },
12
+ class: classMemberOrSchoolAdminWhere(userId),
14
13
  });
15
14
 
16
15
  const worksheetTeacherWhere = (userId: string) => ({
17
- class: {
18
- teachers: { some: { id: userId } },
19
- },
16
+ class: classTeacherOrSchoolAdminWhere(userId),
20
17
  });
21
18
 
22
19
  /** @returns Worksheet with questions and class. */
@@ -207,9 +204,7 @@ export function findSubmissionByIdForWorksheetAccess(
207
204
  },
208
205
  {
209
206
  assignment: {
210
- class: {
211
- teachers: { some: { id: userId } },
212
- },
207
+ class: classTeacherOrSchoolAdminWhere(userId),
213
208
  },
214
209
  },
215
210
  ],
@@ -22,6 +22,7 @@ import { worksheetRouter } from "./worksheet.js";
22
22
  import { commentRouter } from "./comment.js";
23
23
  import { studentProgressRouter } from "./studentProgress.js";
24
24
  import { searchRouter } from "./search.js";
25
+ import { schoolRouter } from "./school.js";
25
26
 
26
27
  export const appRouter = createTRPCRouter({
27
28
  class: classRouter,
@@ -45,6 +46,7 @@ export const appRouter = createTRPCRouter({
45
46
  comment: commentRouter,
46
47
  studentProgress: studentProgressRouter,
47
48
  search: searchRouter,
49
+ school: schoolRouter,
48
50
  });
49
51
 
50
52
  // Export type router type definition
@@ -339,6 +339,24 @@ export const announcementRouter = createTRPCRouter({
339
339
  class: {
340
340
  include: {
341
341
  teachers: true,
342
+ school: {
343
+ include: {
344
+ users: {
345
+ where: {
346
+ role: "ADMIN",
347
+ },
348
+ },
349
+ memberships: {
350
+ where: {
351
+ role: "ADMIN",
352
+ status: "ACTIVE",
353
+ },
354
+ select: {
355
+ userId: true,
356
+ },
357
+ },
358
+ },
359
+ },
342
360
  },
343
361
  },
344
362
  },
@@ -353,17 +371,25 @@ export const announcementRouter = createTRPCRouter({
353
371
  });
354
372
  }
355
373
 
356
- // Only the author or a class teacher can delete comments
374
+ // Only the author, a class teacher, or the class school admin can delete comments
357
375
  const userId = ctx.user.id;
358
376
  const isAuthor = comment.authorId === userId;
359
377
  const isClassTeacher = comment.announcement!.class.teachers.some(
360
378
  (teacher) => teacher.id === userId
361
379
  );
380
+ const isSchoolAdmin = Boolean(
381
+ comment.announcement!.class.school?.memberships.some(
382
+ (admin) => admin.userId === userId
383
+ ) ||
384
+ comment.announcement!.class.school?.users.some(
385
+ (admin) => admin.id === userId
386
+ )
387
+ );
362
388
 
363
- if (!isAuthor && !isClassTeacher) {
389
+ if (!isAuthor && !isClassTeacher && !isSchoolAdmin) {
364
390
  throw new TRPCError({
365
391
  code: "FORBIDDEN",
366
- message: "Only the comment author or class teachers can delete comments",
392
+ message: "Only the comment author, class teachers, or school admins can delete comments",
367
393
  });
368
394
  }
369
395
 
@@ -696,4 +722,4 @@ export const announcementRouter = createTRPCRouter({
696
722
  message: "Unexpected error",
697
723
  });
698
724
  }),
699
- });
725
+ });
@@ -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,
@@ -394,16 +398,11 @@ export const assignmentRouter = createTRPCRouter({
394
398
  assignmentId: input.assignmentId,
395
399
  assignment: {
396
400
  classId: input.classId,
397
- class: {
398
- OR: [
399
- { teachers: { some: { id: ctx.user!.id } } },
400
- { students: { some: { id: ctx.user!.id } } },
401
- ],
402
- },
401
+ class: classMemberOrSchoolAdminWhere(ctx.user!.id),
403
402
  },
404
403
  OR: [
405
404
  { studentId: ctx.user!.id },
406
- { assignment: { class: { teachers: { some: { id: ctx.user!.id } } } } },
405
+ { assignment: { class: classTeacherOrSchoolAdminWhere(ctx.user!.id) } },
407
406
  ],
408
407
  },
409
408
  select: { id: true },
@@ -437,7 +436,7 @@ export const assignmentRouter = createTRPCRouter({
437
436
  assignment: { classId: input.classId },
438
437
  OR: [
439
438
  { studentId: ctx.user!.id },
440
- { assignment: { class: { teachers: { some: { id: ctx.user!.id } } } } },
439
+ { assignment: { class: classTeacherOrSchoolAdminWhere(ctx.user!.id) } },
441
440
  ],
442
441
  },
443
442
  select: { id: true },
@@ -630,6 +629,21 @@ export const assignmentRouter = createTRPCRouter({
630
629
  include: {
631
630
  students: true,
632
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
+ },
633
647
  },
634
648
  },
635
649
  },
@@ -647,8 +661,12 @@ export const assignmentRouter = createTRPCRouter({
647
661
  // Check if user is the student who owns the submission or a teacher of the class
648
662
  const isStudent = submission.studentId === ctx.user.id;
649
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
+ );
650
668
 
651
- if (!isStudent && !isTeacher) {
669
+ if (!isStudent && !isTeacher && !isSchoolAdmin) {
652
670
  throw new TRPCError({
653
671
  code: "FORBIDDEN",
654
672
  message: "You don't have permission to upload files to this submission",
@@ -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 }) =>
@@ -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
+ });