@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
@@ -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
  ],
@@ -16,7 +16,8 @@ import { DocumentBlock } from "../lib/jsonStyles.js";
16
16
  import { type LabChatResponse, labChatResponseSchema } from "./aiLabChatContract.js";
17
17
  import { buildLabChatResponseMessages } from "./labChatPrompt.js";
18
18
 
19
- const LAB_CHAT_RESPONSE_TIMEOUT_MS = 90_000;
19
+ /** Cap model wait time; leave headroom after inference for PDF/GCS/DB within Vercel maxDuration (500s). */
20
+ const LAB_CHAT_RESPONSE_TIMEOUT_MS = 400_000;
20
21
 
21
22
  const withTimeout = async <T>(
22
23
  task: Promise<T>,
@@ -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
+ });