@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,10 +1,11 @@
1
1
  import { z } from "zod";
2
2
  export declare const fileRouter: import("@trpc/server").TRPCBuiltRouter<{
3
- ctx: import("../trpc").Context;
3
+ ctx: import("../trpc.js").Context;
4
4
  meta: object;
5
5
  errorShape: {
6
6
  data: {
7
7
  zodError: z.typeToFlattenedError<any, string> | null;
8
+ prismaError: import("../utils/prismaErrorHandler.js").PrismaErrorInfo | null;
8
9
  code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
9
10
  httpStatus: number;
10
11
  path?: string;
@@ -20,7 +21,76 @@ export declare const fileRouter: import("@trpc/server").TRPCBuiltRouter<{
20
21
  fileId: string;
21
22
  };
22
23
  output: {
23
- signedUrl: string;
24
+ url: string;
25
+ };
26
+ meta: object;
27
+ }>;
28
+ move: import("@trpc/server").TRPCMutationProcedure<{
29
+ input: {
30
+ [x: string]: unknown;
31
+ classId: string;
32
+ fileId: string;
33
+ targetFolderId: string;
34
+ };
35
+ output: {
36
+ user: {
37
+ id: string;
38
+ username: string;
39
+ } | null;
40
+ } & {
41
+ path: string;
42
+ type: string;
43
+ id: string;
44
+ name: string;
45
+ size: number | null;
46
+ uploadedAt: Date | null;
47
+ assignmentId: string | null;
48
+ submissionId: string | null;
49
+ userId: string | null;
50
+ thumbnailId: string | null;
51
+ annotationId: string | null;
52
+ classDraftId: string | null;
53
+ folderId: string | null;
54
+ };
55
+ meta: object;
56
+ }>;
57
+ rename: import("@trpc/server").TRPCMutationProcedure<{
58
+ input: {
59
+ [x: string]: unknown;
60
+ classId: string;
61
+ fileId: string;
62
+ newName: string;
63
+ };
64
+ output: {
65
+ user: {
66
+ id: string;
67
+ username: string;
68
+ } | null;
69
+ } & {
70
+ path: string;
71
+ type: string;
72
+ id: string;
73
+ name: string;
74
+ size: number | null;
75
+ uploadedAt: Date | null;
76
+ assignmentId: string | null;
77
+ submissionId: string | null;
78
+ userId: string | null;
79
+ thumbnailId: string | null;
80
+ annotationId: string | null;
81
+ classDraftId: string | null;
82
+ folderId: string | null;
83
+ };
84
+ meta: object;
85
+ }>;
86
+ delete: import("@trpc/server").TRPCMutationProcedure<{
87
+ input: {
88
+ [x: string]: unknown;
89
+ classId: string;
90
+ fileId: string;
91
+ };
92
+ output: {
93
+ success: boolean;
24
94
  };
25
95
  meta: object;
26
96
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/routers/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;GAwFrB,CAAC"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/routers/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsVrB,CAAC"}
@@ -1,27 +1,25 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fileRouter = void 0;
4
- const zod_1 = require("zod");
5
- const trpc_1 = require("../trpc");
6
- const server_1 = require("@trpc/server");
7
- const googleCloudStorage_1 = require("../lib/googleCloudStorage");
8
- const prisma_1 = require("../lib/prisma");
9
- exports.fileRouter = (0, trpc_1.createTRPCRouter)({
10
- getSignedUrl: trpc_1.protectedProcedure
11
- .input(zod_1.z.object({
12
- fileId: zod_1.z.string(),
1
+ import { z } from "zod";
2
+ import { createTRPCRouter, protectedProcedure, protectedTeacherProcedure } from "../trpc.js";
3
+ import { TRPCError } from "@trpc/server";
4
+ import { getSignedUrl, deleteFile } from "../lib/googleCloudStorage.js";
5
+ import { prisma } from "../lib/prisma.js";
6
+ import { logger } from "../utils/logger.js";
7
+ export const fileRouter = createTRPCRouter({
8
+ getSignedUrl: protectedProcedure
9
+ .input(z.object({
10
+ fileId: z.string(),
13
11
  }))
14
12
  .mutation(async ({ ctx, input }) => {
15
13
  const { fileId } = input;
16
14
  const userId = ctx.user?.id;
17
15
  if (!userId) {
18
- throw new server_1.TRPCError({
16
+ throw new TRPCError({
19
17
  code: "UNAUTHORIZED",
20
18
  message: "You must be logged in to access files",
21
19
  });
22
20
  }
23
21
  // Get file metadata from database
24
- const file = await prisma_1.prisma.file.findUnique({
22
+ const file = await prisma.file.findUnique({
25
23
  where: { id: fileId },
26
24
  include: {
27
25
  assignment: {
@@ -47,42 +45,272 @@ exports.fileRouter = (0, trpc_1.createTRPCRouter)({
47
45
  }
48
46
  }
49
47
  }
48
+ },
49
+ annotations: {
50
+ include: {
51
+ student: true,
52
+ assignment: {
53
+ include: {
54
+ class: {
55
+ include: {
56
+ teachers: true,
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ },
63
+ folder: {
64
+ include: {
65
+ class: {
66
+ include: {
67
+ students: true,
68
+ teachers: true
69
+ }
70
+ }
71
+ }
50
72
  }
51
73
  }
52
74
  });
53
75
  if (!file) {
54
- throw new server_1.TRPCError({
76
+ throw new TRPCError({
55
77
  code: "NOT_FOUND",
56
78
  message: "File does not exist",
57
79
  });
58
80
  }
59
- // Check if user has access to the file
60
- const hasAccess =
61
- // File owner
62
- file.userId === userId ||
63
- // Assignment file - student in class or teacher
64
- (file.assignment && (file.assignment.class.students.some((s) => s.id === userId) ||
65
- file.assignment.class.teachers.some((t) => t.id === userId))) ||
66
- // Submission file - student who submitted or teacher of class
67
- (file.submission && (file.submission.student.id === userId ||
68
- file.submission.assignment.class.teachers.some((t) => t.id === userId)));
81
+ // Check if user has access to this file
82
+ let hasAccess = false;
83
+ let classId = null;
84
+ // Check if user is a teacher of the class
85
+ if (file.assignment?.class) {
86
+ classId = file.assignment.class.id;
87
+ hasAccess = file.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
88
+ }
89
+ if (file.submission?.assignment?.classId) {
90
+ classId = file.submission.assignment.classId;
91
+ hasAccess = file.submission?.studentId === userId || false;
92
+ if (!hasAccess)
93
+ hasAccess = file.submission.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
94
+ }
95
+ if (file.annotations?.assignment?.classId) {
96
+ classId = file.annotations?.assignment.classId;
97
+ hasAccess = file.annotations?.studentId === userId || false;
98
+ if (!hasAccess)
99
+ hasAccess = file.annotations.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
100
+ }
101
+ // Check if user is the file owner
102
+ if (file.userId === userId) {
103
+ hasAccess = true;
104
+ }
105
+ // Check if file is in a folder and user has access to the class
106
+ if (file.folder?.class) {
107
+ hasAccess = hasAccess || file.folder.class.teachers.some(teacher => teacher.id === userId);
108
+ hasAccess = hasAccess || file.folder.class.students.some(student => student.id === userId);
109
+ }
69
110
  if (!hasAccess) {
70
- throw new server_1.TRPCError({
111
+ throw new TRPCError({
71
112
  code: "FORBIDDEN",
72
113
  message: "You do not have access to this file",
73
114
  });
74
115
  }
75
116
  try {
76
- // Generate a signed URL with short expiration
77
- const signedUrl = await (0, googleCloudStorage_1.getSignedUrl)(file.path);
78
- return { signedUrl };
117
+ const signedUrl = await getSignedUrl(file.path);
118
+ return { url: signedUrl };
79
119
  }
80
120
  catch (error) {
81
- console.error('Error generating signed URL:', error);
82
- throw new server_1.TRPCError({
121
+ logger.error('Error generating signed URL:', error);
122
+ throw new TRPCError({
83
123
  code: "INTERNAL_SERVER_ERROR",
84
- message: "Failed to generate signed URL",
124
+ message: "Failed to generate download URL",
125
+ });
126
+ }
127
+ }),
128
+ move: protectedTeacherProcedure
129
+ .input(z.object({
130
+ fileId: z.string(),
131
+ targetFolderId: z.string(),
132
+ classId: z.string(),
133
+ }))
134
+ .mutation(async ({ ctx, input }) => {
135
+ const { fileId, targetFolderId } = input;
136
+ // Get the file
137
+ const file = await prisma.file.findUnique({
138
+ where: { id: fileId },
139
+ include: {
140
+ folder: {
141
+ include: {
142
+ class: true,
143
+ },
144
+ },
145
+ },
146
+ });
147
+ if (!file) {
148
+ throw new TRPCError({
149
+ code: "NOT_FOUND",
150
+ message: "File not found",
151
+ });
152
+ }
153
+ // Get the target folder
154
+ const targetFolder = await prisma.folder.findUnique({
155
+ where: { id: targetFolderId },
156
+ include: {
157
+ class: true,
158
+ },
159
+ });
160
+ if (!targetFolder) {
161
+ throw new TRPCError({
162
+ code: "NOT_FOUND",
163
+ message: "Target folder not found",
85
164
  });
86
165
  }
166
+ // Move the file
167
+ const updatedFile = await prisma.file.update({
168
+ where: { id: fileId },
169
+ data: {
170
+ folderId: targetFolderId,
171
+ },
172
+ include: {
173
+ user: {
174
+ select: {
175
+ id: true,
176
+ username: true,
177
+ },
178
+ },
179
+ },
180
+ });
181
+ return updatedFile;
182
+ }),
183
+ rename: protectedTeacherProcedure
184
+ .input(z.object({
185
+ fileId: z.string(),
186
+ newName: z.string(),
187
+ classId: z.string(),
188
+ }))
189
+ .mutation(async ({ ctx, input }) => {
190
+ const { fileId, newName, classId } = input;
191
+ // Verify user is a teacher of the class
192
+ const classData = await prisma.class.findFirst({
193
+ where: {
194
+ id: classId,
195
+ teachers: {
196
+ some: {
197
+ id: ctx.user.id,
198
+ },
199
+ },
200
+ },
201
+ });
202
+ if (!classData) {
203
+ throw new TRPCError({
204
+ code: "FORBIDDEN",
205
+ message: "You must be a teacher of this class to rename files",
206
+ });
207
+ }
208
+ // Get the file
209
+ const file = await prisma.file.findUnique({
210
+ where: { id: fileId },
211
+ include: {
212
+ folder: {
213
+ include: {
214
+ class: true,
215
+ },
216
+ },
217
+ },
218
+ });
219
+ if (!file) {
220
+ throw new TRPCError({
221
+ code: "NOT_FOUND",
222
+ message: "File not found",
223
+ });
224
+ }
225
+ // Validate new name
226
+ if (!newName.trim()) {
227
+ throw new TRPCError({
228
+ code: "BAD_REQUEST",
229
+ message: "File name cannot be empty",
230
+ });
231
+ }
232
+ // Rename the file
233
+ const updatedFile = await prisma.file.update({
234
+ where: { id: fileId },
235
+ data: {
236
+ name: newName.trim(),
237
+ },
238
+ include: {
239
+ user: {
240
+ select: {
241
+ id: true,
242
+ username: true,
243
+ },
244
+ },
245
+ },
246
+ });
247
+ return updatedFile;
248
+ }),
249
+ delete: protectedTeacherProcedure
250
+ .input(z.object({
251
+ fileId: z.string(),
252
+ classId: z.string(),
253
+ }))
254
+ .mutation(async ({ ctx, input }) => {
255
+ const { fileId, classId } = input;
256
+ // Verify user is a teacher of the class
257
+ const classData = await prisma.class.findFirst({
258
+ where: {
259
+ id: classId,
260
+ teachers: {
261
+ some: {
262
+ id: ctx.user.id,
263
+ },
264
+ },
265
+ },
266
+ });
267
+ if (!classData) {
268
+ throw new TRPCError({
269
+ code: "FORBIDDEN",
270
+ message: "You must be a teacher of this class to delete files",
271
+ });
272
+ }
273
+ // Get the file
274
+ const file = await prisma.file.findUnique({
275
+ where: { id: fileId },
276
+ include: {
277
+ folder: {
278
+ include: {
279
+ class: true,
280
+ },
281
+ },
282
+ thumbnail: true,
283
+ },
284
+ });
285
+ if (!file) {
286
+ throw new TRPCError({
287
+ code: "NOT_FOUND",
288
+ message: "File not found",
289
+ });
290
+ }
291
+ // Verify the file belongs to this class
292
+ if (file.folder?.classId !== classId) {
293
+ throw new TRPCError({
294
+ code: "FORBIDDEN",
295
+ message: "File does not belong to this class",
296
+ });
297
+ }
298
+ // Delete files from storage
299
+ try {
300
+ // Delete the main file
301
+ await deleteFile(file.path);
302
+ // Delete thumbnail if it exists
303
+ if (file.thumbnail) {
304
+ await deleteFile(file.thumbnail.path);
305
+ }
306
+ }
307
+ catch (error) {
308
+ logger.warn(`Failed to delete file ${file.path}:`, error);
309
+ }
310
+ // Delete the file record from database
311
+ await prisma.file.delete({
312
+ where: { id: fileId },
313
+ });
314
+ return { success: true };
87
315
  }),
88
316
  });