@studious-lms/server 1.4.6 → 1.4.9
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/.github/workflows/npm.yml +28 -0
- 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 +69 -37
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/assignment.js +124 -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 +134 -70
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +30 -42
- 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 +163 -141
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +42 -17
- package/dist/models/worksheet.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 +1271 -327
- 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 +5 -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 +79 -44
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +32 -12
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/class.d.ts +40 -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 +5 -5
- 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 +36 -20
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +10 -3
- package/dist/routers/worksheet.js.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 +66 -36
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +37 -3
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/class.d.ts +39 -19
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +52 -15
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +5 -5
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/comment.js +8 -3
- 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 +31 -20
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +37 -3
- package/dist/services/worksheet.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260504071500_school_memberships/migration.sql +81 -0
- package/prisma/schema.prisma +66 -2
- package/src/models/access.ts +48 -0
- package/src/models/announcement.ts +19 -2
- package/src/models/assignment.ts +149 -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 +29 -40
- 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 +61 -12
- package/src/routers/_app.ts +2 -0
- package/src/routers/announcement.ts +30 -4
- package/src/routers/assignment.ts +46 -9
- package/src/routers/class.ts +1 -0
- package/src/routers/school.ts +122 -0
- package/src/routers/worksheet.ts +18 -0
- package/src/services/announcement.ts +12 -4
- package/src/services/assignment.ts +47 -1
- package/src/services/class.ts +61 -14
- package/src/services/comment.ts +10 -1
- package/src/services/event.ts +23 -6
- package/src/services/file.ts +20 -6
- package/src/services/school.ts +308 -0
- package/src/services/worksheet.ts +46 -0
- package/dist/lib/notificationHandler.d.ts +0 -25
- package/dist/lib/notificationHandler.d.ts.map +0 -1
- package/dist/lib/notificationHandler.js +0 -32
- package/dist/lib/notificationHandler.js.map +0 -1
- package/dist/server/pipelines/aiLabChat.d.ts +0 -21
- package/dist/server/pipelines/aiLabChat.d.ts.map +0 -1
- package/dist/server/pipelines/aiLabChat.js +0 -456
- package/dist/server/pipelines/aiLabChat.js.map +0 -1
- package/dist/server/pipelines/aiNewtonChat.d.ts +0 -30
- package/dist/server/pipelines/aiNewtonChat.d.ts.map +0 -1
- package/dist/server/pipelines/aiNewtonChat.js +0 -285
- package/dist/server/pipelines/aiNewtonChat.js.map +0 -1
- package/dist/server/pipelines/gradeWorksheet.d.ts +0 -30
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +0 -1
- package/dist/server/pipelines/gradeWorksheet.js +0 -248
- package/dist/server/pipelines/gradeWorksheet.js.map +0 -1
|
@@ -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
|
+
});
|
package/src/routers/worksheet.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
updateWorksheetRecord,
|
|
20
20
|
deleteWorksheetRecord,
|
|
21
21
|
createWorksheetRecord,
|
|
22
|
+
duplicateWorksheetRecord,
|
|
22
23
|
createWorksheetFromPdf,
|
|
23
24
|
createWorksheetFromUploadedPdf,
|
|
24
25
|
addQuestionToWorksheet,
|
|
@@ -108,6 +109,23 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
108
109
|
createWorksheetRecord(input.classId, input.name)
|
|
109
110
|
),
|
|
110
111
|
|
|
112
|
+
duplicateWorksheet: protectedTeacherProcedure
|
|
113
|
+
.input(
|
|
114
|
+
z.object({
|
|
115
|
+
classId: z.string(),
|
|
116
|
+
name: z.string(),
|
|
117
|
+
worksheetId: z.string(),
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
.mutation(({ ctx, input }) =>
|
|
121
|
+
duplicateWorksheetRecord(
|
|
122
|
+
input.worksheetId,
|
|
123
|
+
input.classId,
|
|
124
|
+
input.name,
|
|
125
|
+
ctx.user!.id
|
|
126
|
+
)
|
|
127
|
+
),
|
|
128
|
+
|
|
111
129
|
createFromPdf: protectedTeacherProcedure
|
|
112
130
|
.input(z.object({ classId: z.string(), url: z.string().url() }))
|
|
113
131
|
.mutation(({ input }) => createWorksheetFromPdf(input.classId, input.url)),
|
|
@@ -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
|
|
|
@@ -6,6 +6,7 @@ import { prisma } from "../lib/prisma.js";
|
|
|
6
6
|
import {
|
|
7
7
|
findAssignmentById,
|
|
8
8
|
findAssignmentWithDetails,
|
|
9
|
+
findAssignmentWithClassForTeacher,
|
|
9
10
|
findAssignmentForDelete,
|
|
10
11
|
findAssignmentsDueToday,
|
|
11
12
|
findSubmissionByAssignmentAndStudent,
|
|
@@ -15,10 +16,10 @@ import {
|
|
|
15
16
|
findSubmissionsByAssignmentForTeacher,
|
|
16
17
|
createSubmission,
|
|
17
18
|
findSectionsByClassId,
|
|
18
|
-
findAssignmentWithClassForTeacher,
|
|
19
19
|
findAvailableEventsForAssignment,
|
|
20
20
|
attachAssignmentToEvent,
|
|
21
21
|
detachAssignmentFromEvent,
|
|
22
|
+
duplicateAssignment,
|
|
22
23
|
} from "../models/assignment.js";
|
|
23
24
|
import { createDirectUploadFiles, type UploadedFile } from "../lib/fileUpload.js";
|
|
24
25
|
import { deleteFile } from "../lib/googleCloudStorage.js";
|
|
@@ -320,6 +321,51 @@ export async function createAssignmentRecord(
|
|
|
320
321
|
return assignment;
|
|
321
322
|
}
|
|
322
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Duplicate an assignment into another class. Caller must teach both the source assignment’s
|
|
326
|
+
* class and the target class.
|
|
327
|
+
*/
|
|
328
|
+
export async function duplicateAssignmentRecord(
|
|
329
|
+
assignmentId: string,
|
|
330
|
+
classId: string,
|
|
331
|
+
title: string,
|
|
332
|
+
userId: string
|
|
333
|
+
) {
|
|
334
|
+
const source = await findAssignmentWithClassForTeacher(assignmentId, userId);
|
|
335
|
+
if (!source) {
|
|
336
|
+
throw new TRPCError({
|
|
337
|
+
code: "NOT_FOUND",
|
|
338
|
+
message:
|
|
339
|
+
"Assignment not found or you are not a teacher of the class it belongs to",
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const targetOk = await prisma.class.findFirst({
|
|
344
|
+
where: {
|
|
345
|
+
id: classId,
|
|
346
|
+
teachers: { some: { id: userId } },
|
|
347
|
+
},
|
|
348
|
+
select: { id: true },
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (!targetOk) {
|
|
352
|
+
throw new TRPCError({
|
|
353
|
+
code: "FORBIDDEN",
|
|
354
|
+
message: "You are not a teacher of the target class",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const created = await duplicateAssignment(assignmentId, classId, title, userId);
|
|
359
|
+
if (!created) {
|
|
360
|
+
throw new TRPCError({
|
|
361
|
+
code: "NOT_FOUND",
|
|
362
|
+
message: "Assignment not found or could not be duplicated",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return created;
|
|
367
|
+
}
|
|
368
|
+
|
|
323
369
|
export async function updateAssignmentRecord(
|
|
324
370
|
userId: string,
|
|
325
371
|
input: {
|
package/src/services/class.ts
CHANGED
|
@@ -27,6 +27,33 @@ import {
|
|
|
27
27
|
} from "../models/class.js";
|
|
28
28
|
import { Class, Folder } from "@prisma/client";
|
|
29
29
|
|
|
30
|
+
async function requireActiveSchoolMembers(schoolId: string, userIds: string[]) {
|
|
31
|
+
const uniqueUserIds = [...new Set(userIds)];
|
|
32
|
+
if (!uniqueUserIds.length) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const memberships = await prisma.schoolMembership.findMany({
|
|
37
|
+
where: {
|
|
38
|
+
schoolId,
|
|
39
|
+
status: "ACTIVE",
|
|
40
|
+
userId: { in: uniqueUserIds },
|
|
41
|
+
},
|
|
42
|
+
select: { userId: true },
|
|
43
|
+
});
|
|
44
|
+
const activeMemberIds = new Set(
|
|
45
|
+
memberships.map((membership) => membership.userId)
|
|
46
|
+
);
|
|
47
|
+
const hasNonMember = uniqueUserIds.some((id) => !activeMemberIds.has(id));
|
|
48
|
+
|
|
49
|
+
if (hasNonMember) {
|
|
50
|
+
throw new TRPCError({
|
|
51
|
+
code: "FORBIDDEN",
|
|
52
|
+
message: "All class members must belong to this school",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
/** Get all classes where user is teacher or student, with assignments and members. */
|
|
31
58
|
export async function getAllClasses(userId: string) {
|
|
32
59
|
const [teacherClasses, studentClasses] = await Promise.all([
|
|
@@ -121,22 +148,37 @@ export async function createClass(
|
|
|
121
148
|
section: string;
|
|
122
149
|
subject: string;
|
|
123
150
|
color?: string;
|
|
151
|
+
schoolId?: string;
|
|
124
152
|
}
|
|
125
153
|
) {
|
|
126
|
-
const { students, teachers, name, section, subject, color } = input;
|
|
154
|
+
const { students, teachers, name, section, subject, color, schoolId } = input;
|
|
127
155
|
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
section,
|
|
133
|
-
subject,
|
|
134
|
-
color,
|
|
135
|
-
teachers: { connect: teachers.map((id) => ({ id })) },
|
|
136
|
-
students: { connect: students.map((id) => ({ id })) },
|
|
137
|
-
},
|
|
138
|
-
include: { teachers: true, students: true },
|
|
156
|
+
if (schoolId) {
|
|
157
|
+
const membership = await prisma.schoolMembership.findFirst({
|
|
158
|
+
where: { userId, schoolId, status: "ACTIVE" },
|
|
159
|
+
select: { role: true },
|
|
139
160
|
});
|
|
161
|
+
|
|
162
|
+
if (!membership) {
|
|
163
|
+
throw new TRPCError({
|
|
164
|
+
code: "FORBIDDEN",
|
|
165
|
+
message: "You are not a member of this school",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (membership.role === "STUDENT") {
|
|
170
|
+
throw new TRPCError({
|
|
171
|
+
code: "FORBIDDEN",
|
|
172
|
+
message: "Students cannot create school classes",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const teacherIds = teachers?.length ? teachers : [userId];
|
|
178
|
+
const studentIds = students?.length ? students : [];
|
|
179
|
+
|
|
180
|
+
if (schoolId) {
|
|
181
|
+
await requireActiveSchoolMembers(schoolId, [...teacherIds, ...studentIds]);
|
|
140
182
|
}
|
|
141
183
|
|
|
142
184
|
return prisma.class.create({
|
|
@@ -145,8 +187,13 @@ export async function createClass(
|
|
|
145
187
|
section,
|
|
146
188
|
subject,
|
|
147
189
|
color,
|
|
148
|
-
|
|
190
|
+
schoolId,
|
|
191
|
+
teachers: { connect: teacherIds.map((id) => ({ id })) },
|
|
192
|
+
...(studentIds.length
|
|
193
|
+
? { students: { connect: studentIds.map((id) => ({ id })) } }
|
|
194
|
+
: {}),
|
|
149
195
|
},
|
|
196
|
+
include: { teachers: true, students: true },
|
|
150
197
|
});
|
|
151
198
|
}
|
|
152
199
|
|
|
@@ -629,4 +676,4 @@ export async function importClass(classId: string, userId: string, year: number,
|
|
|
629
676
|
cause: err,
|
|
630
677
|
});
|
|
631
678
|
}
|
|
632
|
-
}
|
|
679
|
+
}
|
package/src/services/comment.ts
CHANGED
|
@@ -41,7 +41,14 @@ function getPrivateStudentId(comment: any) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function isClassTeacher(comment: any, userId: string) {
|
|
44
|
-
|
|
44
|
+
const classData = getCommentClass(comment);
|
|
45
|
+
return Boolean(
|
|
46
|
+
classData?.teachers?.some((teacher: { id: string }) => teacher.id === userId) ||
|
|
47
|
+
classData?.school?.memberships?.some(
|
|
48
|
+
(admin: { userId: string }) => admin.userId === userId
|
|
49
|
+
) ||
|
|
50
|
+
classData?.school?.users?.some((admin: { id: string }) => admin.id === userId)
|
|
51
|
+
);
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
function isClassMember(comment: any, userId: string) {
|
|
@@ -49,6 +56,8 @@ function isClassMember(comment: any, userId: string) {
|
|
|
49
56
|
return (
|
|
50
57
|
classData?.teachers?.some((teacher: { id: string }) => teacher.id === userId) ||
|
|
51
58
|
classData?.students?.some((student: { id: string }) => student.id === userId) ||
|
|
59
|
+
classData?.school?.memberships?.some((admin: { userId: string }) => admin.userId === userId) ||
|
|
60
|
+
classData?.school?.users?.some((admin: { id: string }) => admin.id === userId) ||
|
|
52
61
|
false
|
|
53
62
|
);
|
|
54
63
|
}
|
package/src/services/event.ts
CHANGED
|
@@ -26,7 +26,7 @@ export async function getEvent(userId: string, id: string) {
|
|
|
26
26
|
message: "Event not found",
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
if (event
|
|
29
|
+
if (!canModifyEvent(event, userId)) {
|
|
30
30
|
throw new TRPCError({
|
|
31
31
|
code: "UNAUTHORIZED",
|
|
32
32
|
message: "You are not authorized to view this event",
|
|
@@ -92,7 +92,7 @@ export async function updateEventRecord(
|
|
|
92
92
|
message: "Event not found",
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
|
-
if (event
|
|
95
|
+
if (!canModifyEvent(event, userId)) {
|
|
96
96
|
throw new TRPCError({
|
|
97
97
|
code: "UNAUTHORIZED",
|
|
98
98
|
message: "You are not authorized to update this event",
|
|
@@ -120,7 +120,7 @@ export async function deleteEventRecord(userId: string, id: string) {
|
|
|
120
120
|
message: "Event not found",
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
|
-
if (event
|
|
123
|
+
if (!canModifyEvent(event, userId)) {
|
|
124
124
|
throw new TRPCError({
|
|
125
125
|
code: "UNAUTHORIZED",
|
|
126
126
|
message: "You are not authorized to delete this event",
|
|
@@ -132,11 +132,24 @@ export async function deleteEventRecord(userId: string, id: string) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function canModifyEvent(
|
|
135
|
-
event: {
|
|
135
|
+
event: {
|
|
136
|
+
userId: string | null;
|
|
137
|
+
class?: {
|
|
138
|
+
teachers: { id: string }[];
|
|
139
|
+
school?: {
|
|
140
|
+
users: { id: string }[];
|
|
141
|
+
memberships?: { userId: string }[];
|
|
142
|
+
} | null;
|
|
143
|
+
} | null;
|
|
144
|
+
},
|
|
136
145
|
userId: string
|
|
137
146
|
) {
|
|
138
147
|
if (event.userId === userId) return true;
|
|
139
|
-
return
|
|
148
|
+
return Boolean(
|
|
149
|
+
event.class?.teachers.some((t) => t.id === userId) ||
|
|
150
|
+
event.class?.school?.memberships?.some((admin) => admin.userId === userId) ||
|
|
151
|
+
event.class?.school?.users.some((admin) => admin.id === userId)
|
|
152
|
+
);
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
export async function attachAssignment(
|
|
@@ -165,7 +178,11 @@ export async function attachAssignment(
|
|
|
165
178
|
message: "Assignment not found",
|
|
166
179
|
});
|
|
167
180
|
}
|
|
168
|
-
if (
|
|
181
|
+
if (
|
|
182
|
+
!assignment.class.teachers.some((t) => t.id === userId) &&
|
|
183
|
+
!assignment.class.school?.memberships?.some((admin) => admin.userId === userId) &&
|
|
184
|
+
!assignment.class.school?.users.some((admin) => admin.id === userId)
|
|
185
|
+
) {
|
|
169
186
|
throw new TRPCError({
|
|
170
187
|
code: "UNAUTHORIZED",
|
|
171
188
|
message: "You are not authorized to modify this assignment",
|
package/src/services/file.ts
CHANGED
|
@@ -16,6 +16,15 @@ import {
|
|
|
16
16
|
} from "../models/file.js";
|
|
17
17
|
import { findConversationMembership } from "../models/message.js";
|
|
18
18
|
|
|
19
|
+
function isSchoolAdminForClass(classData: any, userId: string) {
|
|
20
|
+
return Boolean(
|
|
21
|
+
classData?.school?.memberships?.some(
|
|
22
|
+
(membership: { userId: string }) => membership.userId === userId
|
|
23
|
+
) ||
|
|
24
|
+
classData?.school?.users?.some((admin: { id: string }) => admin.id === userId)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
/** Generate a signed URL for file download. Validates user has access. */
|
|
20
29
|
export async function getFileSignedUrl(userId: string, fileId: string) {
|
|
21
30
|
const file = await findFileById(fileId);
|
|
@@ -35,31 +44,35 @@ export async function getFileSignedUrl(userId: string, fileId: string) {
|
|
|
35
44
|
const isStudent = file.assignment.class.students.some(
|
|
36
45
|
(s) => s.id === userId
|
|
37
46
|
);
|
|
47
|
+
const isSchoolAdmin = isSchoolAdminForClass(file.assignment.class, userId);
|
|
38
48
|
logger.info(
|
|
39
|
-
`Assignment file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}`
|
|
49
|
+
`Assignment file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}, isSchoolAdmin: ${isSchoolAdmin}`
|
|
40
50
|
);
|
|
41
|
-
hasAccess = isTeacher || isStudent;
|
|
51
|
+
hasAccess = isTeacher || isStudent || isSchoolAdmin;
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
if ((file as any).announcement?.class) {
|
|
45
55
|
const ann = (file as any).announcement;
|
|
46
56
|
const isTeacher = ann.class.teachers.some((t: any) => t.id === userId);
|
|
47
57
|
const isStudent = ann.class.students.some((s: any) => s.id === userId);
|
|
48
|
-
|
|
58
|
+
const isSchoolAdmin = isSchoolAdminForClass(ann.class, userId);
|
|
59
|
+
hasAccess = hasAccess || isTeacher || isStudent || isSchoolAdmin;
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
if (file.submission?.assignment?.classId) {
|
|
52
63
|
hasAccess =
|
|
53
64
|
hasAccess ||
|
|
54
65
|
file.submission.studentId === userId ||
|
|
55
|
-
file.submission.assignment.class.teachers.some((t) => t.id === userId)
|
|
66
|
+
file.submission.assignment.class.teachers.some((t) => t.id === userId) ||
|
|
67
|
+
isSchoolAdminForClass(file.submission.assignment.class, userId);
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
if (file.annotations?.assignment?.classId) {
|
|
59
71
|
hasAccess =
|
|
60
72
|
hasAccess ||
|
|
61
73
|
file.annotations.studentId === userId ||
|
|
62
|
-
file.annotations.assignment.class.teachers.some((t) => t.id === userId)
|
|
74
|
+
file.annotations.assignment.class.teachers.some((t) => t.id === userId) ||
|
|
75
|
+
isSchoolAdminForClass(file.annotations.assignment.class, userId);
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
if (file.userId === userId) {
|
|
@@ -74,7 +87,8 @@ export async function getFileSignedUrl(userId: string, fileId: string) {
|
|
|
74
87
|
if (file.folder?.class) {
|
|
75
88
|
const isTeacher = file.folder.class.teachers.some((t) => t.id === userId);
|
|
76
89
|
const isStudent = file.folder.class.students.some((s) => s.id === userId);
|
|
77
|
-
|
|
90
|
+
const isSchoolAdmin = isSchoolAdminForClass(file.folder.class, userId);
|
|
91
|
+
hasAccess = hasAccess || isTeacher || isStudent || isSchoolAdmin;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
if (!hasAccess) {
|