@studious-lms/server 1.0.6 → 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.
Files changed (101) hide show
  1. package/API_SPECIFICATION.md +1117 -0
  2. package/dist/exportType.js +1 -2
  3. package/dist/index.js +25 -30
  4. package/dist/lib/fileUpload.d.ts.map +1 -1
  5. package/dist/lib/fileUpload.js +31 -29
  6. package/dist/lib/googleCloudStorage.js +9 -14
  7. package/dist/lib/prisma.js +4 -7
  8. package/dist/lib/thumbnailGenerator.js +12 -20
  9. package/dist/middleware/auth.d.ts.map +1 -1
  10. package/dist/middleware/auth.js +17 -22
  11. package/dist/middleware/logging.js +5 -9
  12. package/dist/routers/_app.d.ts +3483 -1801
  13. package/dist/routers/_app.d.ts.map +1 -1
  14. package/dist/routers/_app.js +28 -27
  15. package/dist/routers/agenda.d.ts +13 -8
  16. package/dist/routers/agenda.d.ts.map +1 -1
  17. package/dist/routers/agenda.js +14 -17
  18. package/dist/routers/announcement.d.ts +4 -3
  19. package/dist/routers/announcement.d.ts.map +1 -1
  20. package/dist/routers/announcement.js +28 -31
  21. package/dist/routers/assignment.d.ts +282 -196
  22. package/dist/routers/assignment.d.ts.map +1 -1
  23. package/dist/routers/assignment.js +256 -202
  24. package/dist/routers/attendance.d.ts +5 -4
  25. package/dist/routers/attendance.d.ts.map +1 -1
  26. package/dist/routers/attendance.js +31 -34
  27. package/dist/routers/auth.d.ts +1 -0
  28. package/dist/routers/auth.d.ts.map +1 -1
  29. package/dist/routers/auth.js +80 -75
  30. package/dist/routers/class.d.ts +284 -14
  31. package/dist/routers/class.d.ts.map +1 -1
  32. package/dist/routers/class.js +435 -164
  33. package/dist/routers/event.d.ts +47 -38
  34. package/dist/routers/event.d.ts.map +1 -1
  35. package/dist/routers/event.js +76 -79
  36. package/dist/routers/file.d.ts +71 -1
  37. package/dist/routers/file.d.ts.map +1 -1
  38. package/dist/routers/file.js +267 -32
  39. package/dist/routers/folder.d.ts +296 -0
  40. package/dist/routers/folder.d.ts.map +1 -0
  41. package/dist/routers/folder.js +693 -0
  42. package/dist/routers/notifications.d.ts +103 -0
  43. package/dist/routers/notifications.d.ts.map +1 -0
  44. package/dist/routers/notifications.js +91 -0
  45. package/dist/routers/school.d.ts +208 -0
  46. package/dist/routers/school.d.ts.map +1 -0
  47. package/dist/routers/school.js +481 -0
  48. package/dist/routers/section.d.ts +1 -0
  49. package/dist/routers/section.d.ts.map +1 -1
  50. package/dist/routers/section.js +30 -33
  51. package/dist/routers/user.d.ts +2 -1
  52. package/dist/routers/user.d.ts.map +1 -1
  53. package/dist/routers/user.js +21 -24
  54. package/dist/seedDatabase.d.ts +22 -0
  55. package/dist/seedDatabase.d.ts.map +1 -0
  56. package/dist/seedDatabase.js +57 -0
  57. package/dist/socket/handlers.js +26 -30
  58. package/dist/trpc.d.ts +5 -0
  59. package/dist/trpc.d.ts.map +1 -1
  60. package/dist/trpc.js +35 -26
  61. package/dist/types/trpc.js +1 -2
  62. package/dist/utils/email.js +2 -8
  63. package/dist/utils/generateInviteCode.js +1 -5
  64. package/dist/utils/logger.d.ts.map +1 -1
  65. package/dist/utils/logger.js +13 -9
  66. package/dist/utils/prismaErrorHandler.d.ts +9 -0
  67. package/dist/utils/prismaErrorHandler.d.ts.map +1 -0
  68. package/dist/utils/prismaErrorHandler.js +234 -0
  69. package/dist/utils/prismaWrapper.d.ts +14 -0
  70. package/dist/utils/prismaWrapper.d.ts.map +1 -0
  71. package/dist/utils/prismaWrapper.js +64 -0
  72. package/package.json +11 -4
  73. package/prisma/migrations/20250807062924_init/migration.sql +436 -0
  74. package/prisma/migrations/migration_lock.toml +3 -0
  75. package/prisma/schema.prisma +67 -0
  76. package/src/index.ts +2 -2
  77. package/src/lib/fileUpload.ts +16 -7
  78. package/src/middleware/auth.ts +0 -2
  79. package/src/routers/_app.ts +5 -1
  80. package/src/routers/assignment.ts +82 -22
  81. package/src/routers/auth.ts +80 -54
  82. package/src/routers/class.ts +330 -36
  83. package/src/routers/file.ts +283 -20
  84. package/src/routers/folder.ts +755 -0
  85. package/src/routers/notifications.ts +93 -0
  86. package/src/seedDatabase.ts +66 -0
  87. package/src/socket/handlers.ts +4 -4
  88. package/src/trpc.ts +13 -0
  89. package/src/utils/logger.ts +14 -4
  90. package/src/utils/prismaErrorHandler.ts +275 -0
  91. package/src/utils/prismaWrapper.ts +91 -0
  92. package/tests/auth.test.ts +25 -0
  93. package/tests/class.test.ts +281 -0
  94. package/tests/setup.ts +98 -0
  95. package/tests/startup.test.ts +5 -0
  96. package/tsconfig.json +2 -1
  97. package/vitest.config.ts +11 -0
  98. package/dist/logger.d.ts +0 -26
  99. package/dist/logger.d.ts.map +0 -1
  100. package/dist/logger.js +0 -135
  101. package/src/logger.ts +0 -163
@@ -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 the file
63
- const hasAccess =
64
- // File owner
65
- file.userId === userId ||
66
- // Assignment file - student in class or teacher
67
- (file.assignment && (
68
- file.assignment.class.students.some((s: User) => s.id === userId) ||
69
- file.assignment.class.teachers.some((t: User) => t.id === userId)
70
- )) ||
71
- // Submission file - student who submitted or teacher of class
72
- (file.submission && (
73
- file.submission.student.id === userId ||
74
- file.submission.assignment.class.teachers.some((t: User) => t.id === userId)
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
- console.error('Error generating signed URL:', error);
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 signed URL",
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
  });