@studious-lms/server 1.0.6 → 1.0.8

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 (115) hide show
  1. package/API_SPECIFICATION.md +1461 -0
  2. package/dist/exportType.d.ts +3 -3
  3. package/dist/exportType.d.ts.map +1 -1
  4. package/dist/exportType.js +1 -2
  5. package/dist/index.js +25 -30
  6. package/dist/lib/fileUpload.d.ts.map +1 -1
  7. package/dist/lib/fileUpload.js +31 -29
  8. package/dist/lib/googleCloudStorage.js +9 -14
  9. package/dist/lib/prisma.js +4 -7
  10. package/dist/lib/thumbnailGenerator.js +12 -20
  11. package/dist/middleware/auth.d.ts.map +1 -1
  12. package/dist/middleware/auth.js +17 -22
  13. package/dist/middleware/logging.js +5 -9
  14. package/dist/routers/_app.d.ts +3619 -1937
  15. package/dist/routers/_app.d.ts.map +1 -1
  16. package/dist/routers/_app.js +28 -27
  17. package/dist/routers/agenda.d.ts +14 -9
  18. package/dist/routers/agenda.d.ts.map +1 -1
  19. package/dist/routers/agenda.js +14 -17
  20. package/dist/routers/announcement.d.ts +5 -4
  21. package/dist/routers/announcement.d.ts.map +1 -1
  22. package/dist/routers/announcement.js +28 -31
  23. package/dist/routers/assignment.d.ts +283 -197
  24. package/dist/routers/assignment.d.ts.map +1 -1
  25. package/dist/routers/assignment.js +256 -202
  26. package/dist/routers/attendance.d.ts +6 -5
  27. package/dist/routers/attendance.d.ts.map +1 -1
  28. package/dist/routers/attendance.js +31 -34
  29. package/dist/routers/auth.d.ts +2 -1
  30. package/dist/routers/auth.d.ts.map +1 -1
  31. package/dist/routers/auth.js +80 -75
  32. package/dist/routers/class.d.ts +285 -15
  33. package/dist/routers/class.d.ts.map +1 -1
  34. package/dist/routers/class.js +440 -164
  35. package/dist/routers/event.d.ts +48 -39
  36. package/dist/routers/event.d.ts.map +1 -1
  37. package/dist/routers/event.js +76 -79
  38. package/dist/routers/file.d.ts +72 -2
  39. package/dist/routers/file.d.ts.map +1 -1
  40. package/dist/routers/file.js +260 -32
  41. package/dist/routers/folder.d.ts +296 -0
  42. package/dist/routers/folder.d.ts.map +1 -0
  43. package/dist/routers/folder.js +693 -0
  44. package/dist/routers/notifications.d.ts +103 -0
  45. package/dist/routers/notifications.d.ts.map +1 -0
  46. package/dist/routers/notifications.js +91 -0
  47. package/dist/routers/school.d.ts +208 -0
  48. package/dist/routers/school.d.ts.map +1 -0
  49. package/dist/routers/school.js +481 -0
  50. package/dist/routers/section.d.ts +2 -1
  51. package/dist/routers/section.d.ts.map +1 -1
  52. package/dist/routers/section.js +30 -33
  53. package/dist/routers/user.d.ts +3 -2
  54. package/dist/routers/user.d.ts.map +1 -1
  55. package/dist/routers/user.js +21 -24
  56. package/dist/seedDatabase.d.ts +22 -0
  57. package/dist/seedDatabase.d.ts.map +1 -0
  58. package/dist/seedDatabase.js +75 -0
  59. package/dist/socket/handlers.js +26 -30
  60. package/dist/trpc.d.ts +5 -0
  61. package/dist/trpc.d.ts.map +1 -1
  62. package/dist/trpc.js +35 -26
  63. package/dist/types/trpc.d.ts +1 -1
  64. package/dist/types/trpc.d.ts.map +1 -1
  65. package/dist/types/trpc.js +1 -2
  66. package/dist/utils/email.js +2 -8
  67. package/dist/utils/generateInviteCode.js +1 -5
  68. package/dist/utils/logger.d.ts.map +1 -1
  69. package/dist/utils/logger.js +13 -9
  70. package/dist/utils/prismaErrorHandler.d.ts +9 -0
  71. package/dist/utils/prismaErrorHandler.d.ts.map +1 -0
  72. package/dist/utils/prismaErrorHandler.js +234 -0
  73. package/dist/utils/prismaWrapper.d.ts +14 -0
  74. package/dist/utils/prismaWrapper.d.ts.map +1 -0
  75. package/dist/utils/prismaWrapper.js +64 -0
  76. package/package.json +12 -4
  77. package/prisma/migrations/20250807062924_init/migration.sql +436 -0
  78. package/prisma/migrations/migration_lock.toml +3 -0
  79. package/prisma/schema.prisma +68 -1
  80. package/src/exportType.ts +3 -3
  81. package/src/index.ts +6 -6
  82. package/src/lib/fileUpload.ts +19 -10
  83. package/src/lib/thumbnailGenerator.ts +2 -2
  84. package/src/middleware/auth.ts +2 -4
  85. package/src/middleware/logging.ts +2 -2
  86. package/src/routers/_app.ts +17 -13
  87. package/src/routers/agenda.ts +2 -2
  88. package/src/routers/announcement.ts +2 -2
  89. package/src/routers/assignment.ts +86 -26
  90. package/src/routers/attendance.ts +2 -2
  91. package/src/routers/auth.ts +83 -57
  92. package/src/routers/class.ts +339 -39
  93. package/src/routers/event.ts +2 -2
  94. package/src/routers/file.ts +276 -21
  95. package/src/routers/folder.ts +755 -0
  96. package/src/routers/notifications.ts +93 -0
  97. package/src/routers/section.ts +2 -2
  98. package/src/routers/user.ts +3 -3
  99. package/src/seedDatabase.ts +88 -0
  100. package/src/socket/handlers.ts +5 -5
  101. package/src/trpc.ts +17 -4
  102. package/src/types/trpc.ts +1 -1
  103. package/src/utils/logger.ts +14 -4
  104. package/src/utils/prismaErrorHandler.ts +275 -0
  105. package/src/utils/prismaWrapper.ts +91 -0
  106. package/tests/auth.test.ts +25 -0
  107. package/tests/class.test.ts +281 -0
  108. package/tests/setup.ts +98 -0
  109. package/tests/startup.test.ts +5 -0
  110. package/tsconfig.json +2 -1
  111. package/vitest.config.ts +11 -0
  112. package/dist/logger.d.ts +0 -26
  113. package/dist/logger.d.ts.map +0 -1
  114. package/dist/logger.js +0 -135
  115. 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.js";
3
3
  import { TRPCError } from "@trpc/server";
4
- import { getSignedUrl } from "../lib/googleCloudStorage";
4
+ import { getSignedUrl, deleteFile } from "../lib/googleCloudStorage.js";
5
5
  import type { User } from "@prisma/client";
6
- import { prisma } from "../lib/prisma";
6
+ import { prisma } from "../lib/prisma.js";
7
+ import { logger } from "../utils/logger.js";
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,226 @@ 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",
93
180
  });
94
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
+ // Validate new name
250
+ if (!newName.trim()) {
251
+ throw new TRPCError({
252
+ code: "BAD_REQUEST",
253
+ message: "File name cannot be empty",
254
+ });
255
+ }
256
+
257
+ // Rename the file
258
+ const updatedFile = await prisma.file.update({
259
+ where: { id: fileId },
260
+ data: {
261
+ name: newName.trim(),
262
+ },
263
+ include: {
264
+ user: {
265
+ select: {
266
+ id: true,
267
+ username: true,
268
+ },
269
+ },
270
+ },
271
+ });
272
+
273
+ return updatedFile;
274
+ }),
275
+
276
+ delete: protectedTeacherProcedure
277
+ .input(z.object({
278
+ fileId: z.string(),
279
+ classId: z.string(),
280
+ }))
281
+ .mutation(async ({ ctx, input }) => {
282
+ const { fileId, classId } = input;
283
+
284
+ // Verify user is a teacher of the class
285
+ const classData = await prisma.class.findFirst({
286
+ where: {
287
+ id: classId,
288
+ teachers: {
289
+ some: {
290
+ id: ctx.user!.id,
291
+ },
292
+ },
293
+ },
294
+ });
295
+
296
+ if (!classData) {
297
+ throw new TRPCError({
298
+ code: "FORBIDDEN",
299
+ message: "You must be a teacher of this class to delete files",
300
+ });
301
+ }
302
+
303
+ // Get the file
304
+ const file = await prisma.file.findUnique({
305
+ where: { id: fileId },
306
+ include: {
307
+ folder: {
308
+ include: {
309
+ class: true,
310
+ },
311
+ },
312
+ thumbnail: true,
313
+ },
314
+ });
315
+
316
+ if (!file) {
317
+ throw new TRPCError({
318
+ code: "NOT_FOUND",
319
+ message: "File not found",
320
+ });
321
+ }
322
+
323
+ // Verify the file belongs to this class
324
+ if (file.folder?.classId !== classId) {
325
+ throw new TRPCError({
326
+ code: "FORBIDDEN",
327
+ message: "File does not belong to this class",
328
+ });
329
+ }
330
+
331
+ // Delete files from storage
332
+ try {
333
+ // Delete the main file
334
+ await deleteFile(file.path);
335
+
336
+ // Delete thumbnail if it exists
337
+ if (file.thumbnail) {
338
+ await deleteFile(file.thumbnail.path);
339
+ }
340
+ } catch (error) {
341
+ logger.warn(`Failed to delete file ${file.path}:`, error as Record<string, any>);
342
+ }
343
+
344
+ // Delete the file record from database
345
+ await prisma.file.delete({
346
+ where: { id: fileId },
347
+ });
348
+
349
+ return { success: true };
95
350
  }),
96
351
  });