@studious-lms/server 1.0.4 → 1.0.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/API_SPECIFICATION.md +1117 -0
- package/dist/exportType.js +1 -2
- package/dist/index.js +25 -30
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +31 -29
- package/dist/lib/googleCloudStorage.js +9 -14
- package/dist/lib/prisma.js +4 -7
- package/dist/lib/thumbnailGenerator.js +12 -20
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +17 -22
- package/dist/middleware/logging.js +5 -9
- package/dist/routers/_app.d.ts +3483 -1801
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +28 -27
- package/dist/routers/agenda.d.ts +13 -8
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +14 -17
- package/dist/routers/announcement.d.ts +4 -3
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +28 -31
- package/dist/routers/assignment.d.ts +282 -196
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +256 -202
- package/dist/routers/attendance.d.ts +5 -4
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +31 -34
- package/dist/routers/auth.d.ts +1 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +80 -75
- package/dist/routers/class.d.ts +284 -14
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +435 -164
- package/dist/routers/event.d.ts +47 -38
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/event.js +76 -79
- package/dist/routers/file.d.ts +71 -1
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +267 -32
- package/dist/routers/folder.d.ts +296 -0
- package/dist/routers/folder.d.ts.map +1 -0
- package/dist/routers/folder.js +693 -0
- package/dist/routers/notifications.d.ts +103 -0
- package/dist/routers/notifications.d.ts.map +1 -0
- package/dist/routers/notifications.js +91 -0
- package/dist/routers/school.d.ts +208 -0
- package/dist/routers/school.d.ts.map +1 -0
- package/dist/routers/school.js +481 -0
- package/dist/routers/section.d.ts +1 -0
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +30 -33
- package/dist/routers/user.d.ts +2 -1
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +21 -24
- package/dist/seedDatabase.d.ts +22 -0
- package/dist/seedDatabase.d.ts.map +1 -0
- package/dist/seedDatabase.js +57 -0
- package/dist/socket/handlers.js +26 -30
- package/dist/trpc.d.ts +5 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +35 -26
- package/dist/types/trpc.js +1 -2
- package/dist/utils/email.js +2 -8
- package/dist/utils/generateInviteCode.js +1 -5
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +13 -9
- package/dist/utils/prismaErrorHandler.d.ts +9 -0
- package/dist/utils/prismaErrorHandler.d.ts.map +1 -0
- package/dist/utils/prismaErrorHandler.js +234 -0
- package/dist/utils/prismaWrapper.d.ts +14 -0
- package/dist/utils/prismaWrapper.d.ts.map +1 -0
- package/dist/utils/prismaWrapper.js +64 -0
- package/package.json +17 -4
- package/prisma/migrations/20250807062924_init/migration.sql +436 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +67 -0
- package/src/index.ts +2 -2
- package/src/lib/fileUpload.ts +16 -7
- package/src/middleware/auth.ts +0 -2
- package/src/routers/_app.ts +5 -1
- package/src/routers/assignment.ts +82 -22
- package/src/routers/auth.ts +80 -54
- package/src/routers/class.ts +330 -36
- package/src/routers/file.ts +283 -20
- package/src/routers/folder.ts +755 -0
- package/src/routers/notifications.ts +93 -0
- package/src/seedDatabase.ts +66 -0
- package/src/socket/handlers.ts +4 -4
- package/src/trpc.ts +13 -0
- package/src/utils/logger.ts +14 -4
- package/src/utils/prismaErrorHandler.ts +275 -0
- package/src/utils/prismaWrapper.ts +91 -0
- package/tests/auth.test.ts +25 -0
- package/tests/class.test.ts +281 -0
- package/tests/setup.ts +98 -0
- package/tests/startup.test.ts +5 -0
- package/tsconfig.json +2 -1
- package/vitest.config.ts +11 -0
- package/dist/logger.d.ts +0 -26
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -135
- package/src/logger.ts +0 -163
package/src/routers/file.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
|
2
|
+
import { createTRPCRouter, protectedProcedure, protectedTeacherProcedure } from "../trpc";
|
|
3
3
|
import { TRPCError } from "@trpc/server";
|
|
4
|
-
import { getSignedUrl } from "../lib/googleCloudStorage";
|
|
4
|
+
import { getSignedUrl, deleteFile } from "../lib/googleCloudStorage";
|
|
5
5
|
import type { User } from "@prisma/client";
|
|
6
6
|
import { prisma } from "../lib/prisma";
|
|
7
|
+
import { logger } from "../utils/logger";
|
|
7
8
|
|
|
8
9
|
export const fileRouter = createTRPCRouter({
|
|
9
10
|
getSignedUrl: protectedProcedure
|
|
@@ -48,6 +49,30 @@ export const fileRouter = createTRPCRouter({
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
},
|
|
53
|
+
annotations: {
|
|
54
|
+
include: {
|
|
55
|
+
student: true,
|
|
56
|
+
assignment: {
|
|
57
|
+
include: {
|
|
58
|
+
class: {
|
|
59
|
+
include: {
|
|
60
|
+
teachers: true,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
folder: {
|
|
68
|
+
include: {
|
|
69
|
+
class: {
|
|
70
|
+
include: {
|
|
71
|
+
students: true,
|
|
72
|
+
teachers: true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
51
76
|
}
|
|
52
77
|
}
|
|
53
78
|
});
|
|
@@ -59,20 +84,39 @@ export const fileRouter = createTRPCRouter({
|
|
|
59
84
|
});
|
|
60
85
|
}
|
|
61
86
|
|
|
62
|
-
// Check if user has access to
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
// Check if user has access to this file
|
|
88
|
+
let hasAccess = false;
|
|
89
|
+
|
|
90
|
+
let classId: string | null = null;
|
|
91
|
+
|
|
92
|
+
// Check if user is a teacher of the class
|
|
93
|
+
if (file.assignment?.class) {
|
|
94
|
+
classId = file.assignment.class.id;
|
|
95
|
+
hasAccess = file.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (file.submission?.assignment?.classId) {
|
|
99
|
+
classId = file.submission.assignment.classId;
|
|
100
|
+
hasAccess = file.submission?.studentId === userId || false;
|
|
101
|
+
if (!hasAccess) hasAccess = file.submission.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (file.annotations?.assignment?.classId) {
|
|
105
|
+
classId = file.annotations?.assignment.classId;
|
|
106
|
+
hasAccess = file.annotations?.studentId === userId || false;
|
|
107
|
+
if (!hasAccess) hasAccess = file.annotations.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check if user is the file owner
|
|
111
|
+
if (file.userId === userId) {
|
|
112
|
+
hasAccess = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check if file is in a folder and user has access to the class
|
|
116
|
+
if (file.folder?.class) {
|
|
117
|
+
hasAccess = hasAccess || file.folder.class.teachers.some(teacher => teacher.id === userId);
|
|
118
|
+
hasAccess = hasAccess || file.folder.class.students.some(student => student.id === userId);
|
|
119
|
+
}
|
|
76
120
|
|
|
77
121
|
if (!hasAccess) {
|
|
78
122
|
throw new TRPCError({
|
|
@@ -82,15 +126,234 @@ export const fileRouter = createTRPCRouter({
|
|
|
82
126
|
}
|
|
83
127
|
|
|
84
128
|
try {
|
|
85
|
-
// Generate a signed URL with short expiration
|
|
86
129
|
const signedUrl = await getSignedUrl(file.path);
|
|
87
|
-
return { signedUrl };
|
|
130
|
+
return { url: signedUrl };
|
|
88
131
|
} catch (error) {
|
|
89
|
-
|
|
132
|
+
logger.error('Error generating signed URL:', error as Record<string, any>);
|
|
90
133
|
throw new TRPCError({
|
|
91
134
|
code: "INTERNAL_SERVER_ERROR",
|
|
92
|
-
message: "Failed to generate
|
|
135
|
+
message: "Failed to generate download URL",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}),
|
|
139
|
+
|
|
140
|
+
move: protectedTeacherProcedure
|
|
141
|
+
.input(z.object({
|
|
142
|
+
fileId: z.string(),
|
|
143
|
+
targetFolderId: z.string(),
|
|
144
|
+
classId: z.string(),
|
|
145
|
+
}))
|
|
146
|
+
.mutation(async ({ ctx, input }) => {
|
|
147
|
+
const { fileId, targetFolderId } = input;
|
|
148
|
+
|
|
149
|
+
// Get the file
|
|
150
|
+
const file = await prisma.file.findUnique({
|
|
151
|
+
where: { id: fileId },
|
|
152
|
+
include: {
|
|
153
|
+
folder: {
|
|
154
|
+
include: {
|
|
155
|
+
class: true,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!file) {
|
|
162
|
+
throw new TRPCError({
|
|
163
|
+
code: "NOT_FOUND",
|
|
164
|
+
message: "File not found",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get the target folder
|
|
169
|
+
const targetFolder = await prisma.folder.findUnique({
|
|
170
|
+
where: { id: targetFolderId },
|
|
171
|
+
include: {
|
|
172
|
+
class: true,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!targetFolder) {
|
|
177
|
+
throw new TRPCError({
|
|
178
|
+
code: "NOT_FOUND",
|
|
179
|
+
message: "Target folder not found",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Move the file
|
|
184
|
+
const updatedFile = await prisma.file.update({
|
|
185
|
+
where: { id: fileId },
|
|
186
|
+
data: {
|
|
187
|
+
folderId: targetFolderId,
|
|
188
|
+
},
|
|
189
|
+
include: {
|
|
190
|
+
user: {
|
|
191
|
+
select: {
|
|
192
|
+
id: true,
|
|
193
|
+
username: true,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return updatedFile;
|
|
200
|
+
}),
|
|
201
|
+
|
|
202
|
+
rename: protectedTeacherProcedure
|
|
203
|
+
.input(z.object({
|
|
204
|
+
fileId: z.string(),
|
|
205
|
+
newName: z.string(),
|
|
206
|
+
classId: z.string(),
|
|
207
|
+
}))
|
|
208
|
+
.mutation(async ({ ctx, input }) => {
|
|
209
|
+
const { fileId, newName, classId } = input;
|
|
210
|
+
|
|
211
|
+
// Verify user is a teacher of the class
|
|
212
|
+
const classData = await prisma.class.findFirst({
|
|
213
|
+
where: {
|
|
214
|
+
id: classId,
|
|
215
|
+
teachers: {
|
|
216
|
+
some: {
|
|
217
|
+
id: ctx.user!.id,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!classData) {
|
|
224
|
+
throw new TRPCError({
|
|
225
|
+
code: "FORBIDDEN",
|
|
226
|
+
message: "You must be a teacher of this class to rename files",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Get the file
|
|
231
|
+
const file = await prisma.file.findUnique({
|
|
232
|
+
where: { id: fileId },
|
|
233
|
+
include: {
|
|
234
|
+
folder: {
|
|
235
|
+
include: {
|
|
236
|
+
class: true,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!file) {
|
|
243
|
+
throw new TRPCError({
|
|
244
|
+
code: "NOT_FOUND",
|
|
245
|
+
message: "File not found",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Verify the file belongs to this class
|
|
250
|
+
if (file.folder?.classId !== classId) {
|
|
251
|
+
throw new TRPCError({
|
|
252
|
+
code: "FORBIDDEN",
|
|
253
|
+
message: "File does not belong to this class",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Validate new name
|
|
258
|
+
if (!newName.trim()) {
|
|
259
|
+
throw new TRPCError({
|
|
260
|
+
code: "BAD_REQUEST",
|
|
261
|
+
message: "File name cannot be empty",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Rename the file
|
|
266
|
+
const updatedFile = await prisma.file.update({
|
|
267
|
+
where: { id: fileId },
|
|
268
|
+
data: {
|
|
269
|
+
name: newName.trim(),
|
|
270
|
+
},
|
|
271
|
+
include: {
|
|
272
|
+
user: {
|
|
273
|
+
select: {
|
|
274
|
+
id: true,
|
|
275
|
+
username: true,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return updatedFile;
|
|
282
|
+
}),
|
|
283
|
+
|
|
284
|
+
delete: protectedTeacherProcedure
|
|
285
|
+
.input(z.object({
|
|
286
|
+
fileId: z.string(),
|
|
287
|
+
classId: z.string(),
|
|
288
|
+
}))
|
|
289
|
+
.mutation(async ({ ctx, input }) => {
|
|
290
|
+
const { fileId, classId } = input;
|
|
291
|
+
|
|
292
|
+
// Verify user is a teacher of the class
|
|
293
|
+
const classData = await prisma.class.findFirst({
|
|
294
|
+
where: {
|
|
295
|
+
id: classId,
|
|
296
|
+
teachers: {
|
|
297
|
+
some: {
|
|
298
|
+
id: ctx.user!.id,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!classData) {
|
|
305
|
+
throw new TRPCError({
|
|
306
|
+
code: "FORBIDDEN",
|
|
307
|
+
message: "You must be a teacher of this class to delete files",
|
|
93
308
|
});
|
|
94
309
|
}
|
|
310
|
+
|
|
311
|
+
// Get the file
|
|
312
|
+
const file = await prisma.file.findUnique({
|
|
313
|
+
where: { id: fileId },
|
|
314
|
+
include: {
|
|
315
|
+
folder: {
|
|
316
|
+
include: {
|
|
317
|
+
class: true,
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
thumbnail: true,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (!file) {
|
|
325
|
+
throw new TRPCError({
|
|
326
|
+
code: "NOT_FOUND",
|
|
327
|
+
message: "File not found",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Verify the file belongs to this class
|
|
332
|
+
if (file.folder?.classId !== classId) {
|
|
333
|
+
throw new TRPCError({
|
|
334
|
+
code: "FORBIDDEN",
|
|
335
|
+
message: "File does not belong to this class",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Delete files from storage
|
|
340
|
+
try {
|
|
341
|
+
// Delete the main file
|
|
342
|
+
await deleteFile(file.path);
|
|
343
|
+
|
|
344
|
+
// Delete thumbnail if it exists
|
|
345
|
+
if (file.thumbnail) {
|
|
346
|
+
await deleteFile(file.thumbnail.path);
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
logger.warn(`Failed to delete file ${file.path}:`, error as Record<string, any>);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Delete the file record from database
|
|
353
|
+
await prisma.file.delete({
|
|
354
|
+
where: { id: fileId },
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return { success: true };
|
|
95
358
|
}),
|
|
96
359
|
});
|