@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.
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +8 -3
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/models/access.d.ts +112 -0
- package/dist/models/access.d.ts.map +1 -0
- package/dist/models/access.js +49 -0
- package/dist/models/access.js.map +1 -0
- package/dist/models/announcement.d.ts +17 -2
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/announcement.js +21 -4
- package/dist/models/announcement.js.map +1 -1
- package/dist/models/assignment.d.ts +37 -37
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/assignment.js +15 -15
- package/dist/models/assignment.js.map +1 -1
- package/dist/models/attendance.d.ts +1 -1
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +6 -5
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/auth.d.ts +5 -5
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +7 -11
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +26 -26
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +7 -11
- package/dist/models/class.js.map +1 -1
- package/dist/models/comment.d.ts +233 -6
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +133 -3
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +7 -7
- package/dist/models/event.d.ts +52 -3
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/event.js +56 -6
- package/dist/models/event.js.map +1 -1
- package/dist/models/file.d.ts +178 -103
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/file.js +54 -14
- package/dist/models/file.js.map +1 -1
- package/dist/models/folder.d.ts +22 -22
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/folder.js +5 -4
- package/dist/models/folder.js.map +1 -1
- package/dist/models/labChat.d.ts +20 -20
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/labChat.js +8 -10
- package/dist/models/labChat.js.map +1 -1
- package/dist/models/marketing.d.ts +1 -1
- package/dist/models/message.d.ts +6 -6
- package/dist/models/newtonChat.d.ts +5 -5
- package/dist/models/school.d.ts +361 -0
- package/dist/models/school.d.ts.map +1 -0
- package/dist/models/school.js +314 -0
- package/dist/models/school.js.map +1 -0
- package/dist/models/section.d.ts +1 -1
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/section.js +5 -4
- package/dist/models/section.js.map +1 -1
- package/dist/models/user.d.ts +4 -4
- package/dist/models/worksheet.d.ts +67 -65
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +6 -14
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +4 -3
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +2 -2
- package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
- package/dist/routers/_app.d.ts +2462 -1104
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/announcement.d.ts +13 -5
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +25 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +266 -36
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +221 -3
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/class.d.ts +21 -20
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +3 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/comment.d.ts +40 -3
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/comment.js +14 -5
- package/dist/routers/comment.js.map +1 -1
- package/dist/routers/conversation.d.ts +6 -6
- package/dist/routers/event.d.ts +21 -2
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +2 -2
- package/dist/routers/folder.d.ts +24 -24
- package/dist/routers/labChat.d.ts +7 -7
- package/dist/routers/school.d.ts +384 -0
- package/dist/routers/school.d.ts.map +1 -0
- package/dist/routers/school.js +79 -0
- package/dist/routers/school.js.map +1 -0
- package/dist/routers/search.d.ts +1 -1
- package/dist/routers/worksheet.d.ts +23 -21
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/services/announcement.d.ts.map +1 -1
- package/dist/services/announcement.js +10 -6
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts +36 -36
- package/dist/services/class.d.ts +20 -19
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +22 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +31 -5
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/comment.js +102 -19
- package/dist/services/comment.js.map +1 -1
- package/dist/services/conversation.d.ts +6 -6
- package/dist/services/event.d.ts +21 -2
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/event.js +11 -7
- package/dist/services/event.js.map +1 -1
- package/dist/services/file.d.ts +2 -2
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +17 -8
- package/dist/services/file.js.map +1 -1
- package/dist/services/folder.d.ts +24 -24
- package/dist/services/labChat.d.ts +7 -7
- package/dist/services/school.d.ts +299 -0
- package/dist/services/school.d.ts.map +1 -0
- package/dist/services/school.js +225 -0
- package/dist/services/school.js.map +1 -0
- package/dist/services/worksheet.d.ts +23 -21
- package/dist/services/worksheet.d.ts.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +60 -0
- package/prisma/migrations/20260504071500_school_memberships/migration.sql +81 -0
- package/prisma/schema.prisma +75 -2
- package/src/lib/config/cors.ts +8 -1
- package/src/models/access.ts +48 -0
- package/src/models/announcement.ts +19 -2
- package/src/models/assignment.ts +17 -13
- package/src/models/attendance.ts +4 -3
- package/src/models/auth.ts +8 -9
- package/src/models/class.ts +8 -9
- package/src/models/comment.ts +137 -1
- package/src/models/event.ts +54 -4
- package/src/models/file.ts +53 -12
- package/src/models/folder.ts +3 -2
- package/src/models/labChat.ts +9 -8
- package/src/models/school.ts +361 -0
- package/src/models/section.ts +3 -2
- package/src/models/worksheet.ts +7 -12
- package/src/pipelines/aiLabChat.ts +2 -1
- package/src/routers/_app.ts +2 -0
- package/src/routers/announcement.ts +30 -4
- package/src/routers/assignment.ts +243 -2
- package/src/routers/class.ts +1 -0
- package/src/routers/comment.ts +19 -2
- package/src/routers/school.ts +122 -0
- package/src/services/announcement.ts +12 -4
- package/src/services/class.ts +25 -2
- package/src/services/comment.ts +122 -18
- package/src/services/event.ts +23 -6
- package/src/services/file.ts +20 -6
- package/src/services/school.ts +308 -0
- package/tests/lib/cors.test.ts +13 -1
- package/tests/routers/assignment.test.ts +68 -2
- package/tests/routers/comment.test.ts +42 -1
- 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
|
-
|
package/src/routers/class.ts
CHANGED
package/src/routers/comment.ts
CHANGED
|
@@ -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
|
|
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
|
|
292
|
+
message: "Only the announcement creator, class teachers, or school admins can delete announcements",
|
|
285
293
|
});
|
|
286
294
|
}
|
|
287
295
|
|
package/src/services/class.ts
CHANGED
|
@@ -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
|
+
}
|