@studious-lms/server 1.1.22 → 1.1.23
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 +146 -10
- package/BASE64_REMOVAL_SUMMARY.md +11 -0
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +11 -68
- package/dist/lib/googleCloudStorage.d.ts +0 -7
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +0 -19
- package/dist/routers/_app.d.ts +114 -48
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +31 -24
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +99 -25
- package/dist/routers/folder.d.ts +27 -0
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +93 -0
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +1 -2
- package/dist/utils/logger.d.ts +0 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +2 -27
- package/package.json +2 -2
- package/src/lib/fileUpload.ts +11 -69
- package/src/lib/googleCloudStorage.ts +0 -19
- package/src/routers/assignment.ts +121 -26
- package/src/routers/folder.ts +109 -0
- package/src/seedDatabase.ts +1 -2
- package/src/utils/logger.ts +2 -29
- package/tests/setup.ts +9 -3
|
@@ -474,18 +474,6 @@ export declare const assignmentRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
474
474
|
student: {
|
|
475
475
|
id: string;
|
|
476
476
|
username: string;
|
|
477
|
-
profile: {
|
|
478
|
-
id: string;
|
|
479
|
-
location: string | null;
|
|
480
|
-
userId: string;
|
|
481
|
-
createdAt: Date;
|
|
482
|
-
displayName: string | null;
|
|
483
|
-
bio: string | null;
|
|
484
|
-
website: string | null;
|
|
485
|
-
profilePicture: string | null;
|
|
486
|
-
profilePictureThumbnail: string | null;
|
|
487
|
-
updatedAt: Date;
|
|
488
|
-
} | null;
|
|
489
477
|
};
|
|
490
478
|
attachments: {
|
|
491
479
|
path: string;
|
|
@@ -605,18 +593,6 @@ export declare const assignmentRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
605
593
|
student: {
|
|
606
594
|
id: string;
|
|
607
595
|
username: string;
|
|
608
|
-
profile: {
|
|
609
|
-
id: string;
|
|
610
|
-
location: string | null;
|
|
611
|
-
userId: string;
|
|
612
|
-
createdAt: Date;
|
|
613
|
-
displayName: string | null;
|
|
614
|
-
bio: string | null;
|
|
615
|
-
website: string | null;
|
|
616
|
-
profilePicture: string | null;
|
|
617
|
-
profilePictureThumbnail: string | null;
|
|
618
|
-
updatedAt: Date;
|
|
619
|
-
} | null;
|
|
620
596
|
};
|
|
621
597
|
attachments: {
|
|
622
598
|
path: string;
|
|
@@ -1587,6 +1563,37 @@ export declare const assignmentRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
1587
1563
|
};
|
|
1588
1564
|
meta: object;
|
|
1589
1565
|
}>;
|
|
1566
|
+
getAnnotationUploadUrls: import("@trpc/server").TRPCMutationProcedure<{
|
|
1567
|
+
input: {
|
|
1568
|
+
[x: string]: unknown;
|
|
1569
|
+
classId: string;
|
|
1570
|
+
files: {
|
|
1571
|
+
type: string;
|
|
1572
|
+
name: string;
|
|
1573
|
+
size: number;
|
|
1574
|
+
}[];
|
|
1575
|
+
submissionId: string;
|
|
1576
|
+
};
|
|
1577
|
+
output: {
|
|
1578
|
+
success: boolean;
|
|
1579
|
+
uploadFiles: DirectUploadFile[];
|
|
1580
|
+
};
|
|
1581
|
+
meta: object;
|
|
1582
|
+
}>;
|
|
1583
|
+
confirmAnnotationUpload: import("@trpc/server").TRPCMutationProcedure<{
|
|
1584
|
+
input: {
|
|
1585
|
+
[x: string]: unknown;
|
|
1586
|
+
classId: string;
|
|
1587
|
+
fileId: string;
|
|
1588
|
+
uploadSuccess: boolean;
|
|
1589
|
+
errorMessage?: string | undefined;
|
|
1590
|
+
};
|
|
1591
|
+
output: {
|
|
1592
|
+
success: boolean;
|
|
1593
|
+
message: string;
|
|
1594
|
+
};
|
|
1595
|
+
meta: object;
|
|
1596
|
+
}>;
|
|
1590
1597
|
updateUploadProgress: import("@trpc/server").TRPCMutationProcedure<{
|
|
1591
1598
|
input: {
|
|
1592
1599
|
fileId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assignment.d.ts","sourceRoot":"","sources":["../../src/routers/assignment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAA2B,KAAK,gBAAgB,EAAgE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"assignment.d.ts","sourceRoot":"","sources":["../../src/routers/assignment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAA2B,KAAK,gBAAgB,EAAgE,MAAM,sBAAsB,CAAC;AAgIpJ,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqgE3B,CAAC"}
|
|
@@ -100,6 +100,16 @@ const confirmSubmissionUploadSchema = z.object({
|
|
|
100
100
|
uploadSuccess: z.boolean(),
|
|
101
101
|
errorMessage: z.string().optional(),
|
|
102
102
|
});
|
|
103
|
+
const getAnnotationUploadUrlsSchema = z.object({
|
|
104
|
+
submissionId: z.string(),
|
|
105
|
+
classId: z.string(),
|
|
106
|
+
files: z.array(directFileSchema),
|
|
107
|
+
});
|
|
108
|
+
const confirmAnnotationUploadSchema = z.object({
|
|
109
|
+
fileId: z.string(),
|
|
110
|
+
uploadSuccess: z.boolean(),
|
|
111
|
+
errorMessage: z.string().optional(),
|
|
112
|
+
});
|
|
103
113
|
const updateUploadProgressSchema = z.object({
|
|
104
114
|
fileId: z.string(),
|
|
105
115
|
progress: z.number().min(0).max(100),
|
|
@@ -668,7 +678,6 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
668
678
|
select: {
|
|
669
679
|
id: true,
|
|
670
680
|
username: true,
|
|
671
|
-
profile: true,
|
|
672
681
|
},
|
|
673
682
|
},
|
|
674
683
|
assignment: {
|
|
@@ -770,7 +779,6 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
770
779
|
select: {
|
|
771
780
|
id: true,
|
|
772
781
|
username: true,
|
|
773
|
-
profile: true,
|
|
774
782
|
},
|
|
775
783
|
},
|
|
776
784
|
assignment: {
|
|
@@ -1167,30 +1175,13 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1167
1175
|
},
|
|
1168
1176
|
});
|
|
1169
1177
|
}
|
|
1170
|
-
|
|
1178
|
+
// NOTE: Teacher annotation files are now handled via direct upload endpoints
|
|
1179
|
+
// Use getAnnotationUploadUrls and confirmAnnotationUpload endpoints instead
|
|
1180
|
+
// The newAttachments field is deprecated for annotations
|
|
1171
1181
|
if (newAttachments && newAttachments.length > 0) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
// Update submission with new file attachments
|
|
1176
|
-
if (uploadedFiles.length > 0) {
|
|
1177
|
-
await prisma.submission.update({
|
|
1178
|
-
where: { id: submission.id },
|
|
1179
|
-
data: {
|
|
1180
|
-
annotations: {
|
|
1181
|
-
create: uploadedFiles.map(file => ({
|
|
1182
|
-
name: file.name,
|
|
1183
|
-
type: file.type,
|
|
1184
|
-
size: file.size,
|
|
1185
|
-
path: file.path,
|
|
1186
|
-
...(file.thumbnailId && {
|
|
1187
|
-
thumbnail: {
|
|
1188
|
-
connect: { id: file.thumbnailId }
|
|
1189
|
-
}
|
|
1190
|
-
})
|
|
1191
|
-
}))
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1182
|
+
throw new TRPCError({
|
|
1183
|
+
code: "BAD_REQUEST",
|
|
1184
|
+
message: "Direct file upload is deprecated. Use getAnnotationUploadUrls endpoint instead.",
|
|
1194
1185
|
});
|
|
1195
1186
|
}
|
|
1196
1187
|
// Connect existing files if provided
|
|
@@ -1864,6 +1855,89 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1864
1855
|
message: uploadSuccess ? "Upload confirmed successfully" : "Upload failed",
|
|
1865
1856
|
};
|
|
1866
1857
|
}),
|
|
1858
|
+
getAnnotationUploadUrls: protectedTeacherProcedure
|
|
1859
|
+
.input(getAnnotationUploadUrlsSchema)
|
|
1860
|
+
.mutation(async ({ ctx, input }) => {
|
|
1861
|
+
const { submissionId, classId, files } = input;
|
|
1862
|
+
if (!ctx.user) {
|
|
1863
|
+
throw new TRPCError({
|
|
1864
|
+
code: "UNAUTHORIZED",
|
|
1865
|
+
message: "You must be logged in to upload files",
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
// Verify submission exists and user is a teacher of the class
|
|
1869
|
+
const submission = await prisma.submission.findFirst({
|
|
1870
|
+
where: {
|
|
1871
|
+
id: submissionId,
|
|
1872
|
+
assignment: {
|
|
1873
|
+
classId: classId,
|
|
1874
|
+
class: {
|
|
1875
|
+
teachers: {
|
|
1876
|
+
some: {
|
|
1877
|
+
id: ctx.user.id,
|
|
1878
|
+
},
|
|
1879
|
+
},
|
|
1880
|
+
},
|
|
1881
|
+
},
|
|
1882
|
+
},
|
|
1883
|
+
});
|
|
1884
|
+
if (!submission) {
|
|
1885
|
+
throw new TRPCError({
|
|
1886
|
+
code: "NOT_FOUND",
|
|
1887
|
+
message: "Submission not found or you are not a teacher of this class",
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
// Create direct upload files for annotations
|
|
1891
|
+
// Note: We pass submissionId as the 5th parameter, but these are annotations not submission files
|
|
1892
|
+
// We need to store them separately, so we'll use a different approach
|
|
1893
|
+
const directUploadFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, // No specific directory
|
|
1894
|
+
undefined, // No assignment ID
|
|
1895
|
+
undefined // Don't link to submission yet (will be linked in confirmAnnotationUpload)
|
|
1896
|
+
);
|
|
1897
|
+
// Store the submissionId in the file record so we can link it to annotations later
|
|
1898
|
+
await Promise.all(directUploadFiles.map(file => prisma.file.update({
|
|
1899
|
+
where: { id: file.id },
|
|
1900
|
+
data: {
|
|
1901
|
+
annotationId: submissionId, // Store as annotation
|
|
1902
|
+
}
|
|
1903
|
+
})));
|
|
1904
|
+
return {
|
|
1905
|
+
success: true,
|
|
1906
|
+
uploadFiles: directUploadFiles,
|
|
1907
|
+
};
|
|
1908
|
+
}),
|
|
1909
|
+
confirmAnnotationUpload: protectedTeacherProcedure
|
|
1910
|
+
.input(confirmAnnotationUploadSchema)
|
|
1911
|
+
.mutation(async ({ ctx, input }) => {
|
|
1912
|
+
const { fileId, uploadSuccess, errorMessage } = input;
|
|
1913
|
+
if (!ctx.user) {
|
|
1914
|
+
throw new TRPCError({
|
|
1915
|
+
code: "UNAUTHORIZED",
|
|
1916
|
+
message: "You must be logged in",
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
// Verify file belongs to user and is an annotation file
|
|
1920
|
+
const file = await prisma.file.findFirst({
|
|
1921
|
+
where: {
|
|
1922
|
+
id: fileId,
|
|
1923
|
+
userId: ctx.user.id,
|
|
1924
|
+
annotationId: {
|
|
1925
|
+
not: null,
|
|
1926
|
+
},
|
|
1927
|
+
},
|
|
1928
|
+
});
|
|
1929
|
+
if (!file) {
|
|
1930
|
+
throw new TRPCError({
|
|
1931
|
+
code: "NOT_FOUND",
|
|
1932
|
+
message: "File not found or you don't have permission",
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
await confirmDirectUpload(fileId, uploadSuccess, errorMessage);
|
|
1936
|
+
return {
|
|
1937
|
+
success: true,
|
|
1938
|
+
message: uploadSuccess ? "Annotation upload confirmed successfully" : "Annotation upload failed",
|
|
1939
|
+
};
|
|
1940
|
+
}),
|
|
1867
1941
|
updateUploadProgress: protectedProcedure
|
|
1868
1942
|
.input(updateUploadProgressSchema)
|
|
1869
1943
|
.mutation(async ({ ctx, input }) => {
|
package/dist/routers/folder.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { type DirectUploadFile } from "../lib/fileUpload.js";
|
|
2
3
|
export declare const folderRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
3
4
|
ctx: import("../trpc.js").Context;
|
|
4
5
|
meta: object;
|
|
@@ -316,5 +317,31 @@ export declare const folderRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
316
317
|
}[];
|
|
317
318
|
meta: object;
|
|
318
319
|
}>;
|
|
320
|
+
getFolderUploadUrls: import("@trpc/server").TRPCMutationProcedure<{
|
|
321
|
+
input: {
|
|
322
|
+
[x: string]: unknown;
|
|
323
|
+
classId: string;
|
|
324
|
+
files: {
|
|
325
|
+
type: string;
|
|
326
|
+
name: string;
|
|
327
|
+
size: number;
|
|
328
|
+
}[];
|
|
329
|
+
folderId: string;
|
|
330
|
+
};
|
|
331
|
+
output: DirectUploadFile[];
|
|
332
|
+
meta: object;
|
|
333
|
+
}>;
|
|
334
|
+
confirmFolderUpload: import("@trpc/server").TRPCMutationProcedure<{
|
|
335
|
+
input: {
|
|
336
|
+
[x: string]: unknown;
|
|
337
|
+
classId: string;
|
|
338
|
+
fileId: string;
|
|
339
|
+
uploadSuccess: boolean;
|
|
340
|
+
};
|
|
341
|
+
output: {
|
|
342
|
+
success: boolean;
|
|
343
|
+
};
|
|
344
|
+
meta: object;
|
|
345
|
+
}>;
|
|
319
346
|
}>>;
|
|
320
347
|
//# sourceMappingURL=folder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"folder.d.ts","sourceRoot":"","sources":["../../src/routers/folder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"folder.d.ts","sourceRoot":"","sources":["../../src/routers/folder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAA2B,KAAK,gBAAgB,EAAqB,MAAM,sBAAsB,CAAC;AA6BzG,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAu2BvB,CAAC"}
|
package/dist/routers/folder.js
CHANGED
|
@@ -726,4 +726,97 @@ export const folderRouter = createTRPCRouter({
|
|
|
726
726
|
}
|
|
727
727
|
return parents;
|
|
728
728
|
}),
|
|
729
|
+
getFolderUploadUrls: protectedTeacherProcedure
|
|
730
|
+
.input(z.object({
|
|
731
|
+
classId: z.string(),
|
|
732
|
+
folderId: z.string(),
|
|
733
|
+
files: z.array(directFileSchema),
|
|
734
|
+
}))
|
|
735
|
+
.mutation(async ({ ctx, input }) => {
|
|
736
|
+
const { classId, folderId, files } = input;
|
|
737
|
+
if (!ctx.user) {
|
|
738
|
+
throw new TRPCError({
|
|
739
|
+
code: "UNAUTHORIZED",
|
|
740
|
+
message: "You must be logged in to upload files",
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
// Verify user is a teacher of the class
|
|
744
|
+
const classData = await prisma.class.findFirst({
|
|
745
|
+
where: {
|
|
746
|
+
id: classId,
|
|
747
|
+
teachers: {
|
|
748
|
+
some: {
|
|
749
|
+
id: ctx.user.id,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
if (!classData) {
|
|
755
|
+
throw new TRPCError({
|
|
756
|
+
code: "NOT_FOUND",
|
|
757
|
+
message: "Class not found or you are not a teacher",
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
// Verify folder exists
|
|
761
|
+
const folder = await prisma.folder.findUnique({
|
|
762
|
+
where: {
|
|
763
|
+
id: folderId,
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
if (!folder) {
|
|
767
|
+
throw new TRPCError({
|
|
768
|
+
code: "NOT_FOUND",
|
|
769
|
+
message: "Folder not found",
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
// Verify folder belongs to the class by traversing parent hierarchy
|
|
773
|
+
// Only root folders have classId, child folders use parentFolderId
|
|
774
|
+
let currentFolder = folder;
|
|
775
|
+
let belongsToClass = false;
|
|
776
|
+
while (currentFolder) {
|
|
777
|
+
// Check if we've reached a root folder with the matching classId
|
|
778
|
+
if (currentFolder.classId === classId) {
|
|
779
|
+
belongsToClass = true;
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
// If this folder has a parent, traverse up
|
|
783
|
+
if (currentFolder.parentFolderId) {
|
|
784
|
+
currentFolder = await prisma.folder.findUnique({
|
|
785
|
+
where: { id: currentFolder.parentFolderId },
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
// Reached a root folder without matching classId
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!belongsToClass) {
|
|
794
|
+
throw new TRPCError({
|
|
795
|
+
code: "FORBIDDEN",
|
|
796
|
+
message: "Folder does not belong to this class",
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
// Create direct upload files
|
|
800
|
+
const uploadFiles = await createDirectUploadFiles(files, ctx.user.id, folder.id);
|
|
801
|
+
return uploadFiles;
|
|
802
|
+
}),
|
|
803
|
+
confirmFolderUpload: protectedTeacherProcedure
|
|
804
|
+
.input(z.object({
|
|
805
|
+
fileId: z.string(),
|
|
806
|
+
uploadSuccess: z.boolean(),
|
|
807
|
+
}))
|
|
808
|
+
.mutation(async ({ ctx, input }) => {
|
|
809
|
+
const { fileId, uploadSuccess } = input;
|
|
810
|
+
if (!ctx.user) {
|
|
811
|
+
throw new TRPCError({
|
|
812
|
+
code: "UNAUTHORIZED",
|
|
813
|
+
message: "You must be logged in to confirm uploads",
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
// Import the confirmDirectUpload function
|
|
817
|
+
const { confirmDirectUpload } = await import("../lib/fileUpload.js");
|
|
818
|
+
// Confirm the upload
|
|
819
|
+
await confirmDirectUpload(fileId, uploadSuccess);
|
|
820
|
+
return { success: true };
|
|
821
|
+
}),
|
|
729
822
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seedDatabase.d.ts","sourceRoot":"","sources":["../src/seedDatabase.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,
|
|
1
|
+
{"version":3,"file":"seedDatabase.d.ts","sourceRoot":"","sources":["../src/seedDatabase.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,kBAsClC;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;GAOjF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;;;;;;;GAQnF;AAED,eAAO,MAAM,YAAY,qBAq+CxB,CAAC"}
|
package/dist/seedDatabase.js
CHANGED
|
@@ -4,7 +4,6 @@ import { logger } from "./utils/logger.js";
|
|
|
4
4
|
export async function clearDatabase() {
|
|
5
5
|
// Delete in order to respect foreign key constraints
|
|
6
6
|
// Delete notifications first (they reference users)
|
|
7
|
-
logger.info('Clearing database');
|
|
8
7
|
await prisma.notification.deleteMany();
|
|
9
8
|
// Delete chat-related records
|
|
10
9
|
await prisma.mention.deleteMany();
|
|
@@ -78,7 +77,7 @@ export const seedDatabase = async () => {
|
|
|
78
77
|
]);
|
|
79
78
|
// 3. Create Students (realistic names)
|
|
80
79
|
const students = await Promise.all([
|
|
81
|
-
createUser('alex.martinez@student.
|
|
80
|
+
createUser('alex.martinez@student.riverside.edu', 'student123', 'alex.martinez'),
|
|
82
81
|
createUser('sophia.williams@student.riverside.edu', 'student123', 'sophia.williams'),
|
|
83
82
|
createUser('james.brown@student.riverside.edu', 'student123', 'james.brown'),
|
|
84
83
|
createUser('olivia.taylor@student.riverside.edu', 'student123', 'olivia.taylor'),
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,oBAAY,QAAQ;IAClB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;CAChB;AAED,KAAK,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,oBAAY,QAAQ;IAClB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;CAChB;AAED,KAAK,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAwB3D,cAAM,MAAM;IACV,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAS;IAChC,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,WAAW,CAA2B;IAE9C,OAAO;WAuBO,WAAW,IAAI,MAAM;IAO5B,OAAO,CAAC,IAAI,EAAE,OAAO;IAI5B,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,GAAG;IAqCJ,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAInD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAInD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAIpD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;CAG5D;AAED,eAAO,MAAM,MAAM,QAAuB,CAAC"}
|
package/dist/utils/logger.js
CHANGED
|
@@ -17,24 +17,7 @@ const colors = {
|
|
|
17
17
|
magenta: '\x1b[35m',
|
|
18
18
|
cyan: '\x1b[36m',
|
|
19
19
|
white: '\x1b[37m',
|
|
20
|
-
gray: '\x1b[90m'
|
|
21
|
-
// Background colors
|
|
22
|
-
bgRed: '\x1b[41m',
|
|
23
|
-
bgGreen: '\x1b[42m',
|
|
24
|
-
bgYellow: '\x1b[43m',
|
|
25
|
-
bgBlue: '\x1b[44m',
|
|
26
|
-
bgMagenta: '\x1b[45m',
|
|
27
|
-
bgCyan: '\x1b[46m',
|
|
28
|
-
bgWhite: '\x1b[47m',
|
|
29
|
-
bgGray: '\x1b[100m',
|
|
30
|
-
// Bright background colors
|
|
31
|
-
bgBrightRed: '\x1b[101m',
|
|
32
|
-
bgBrightGreen: '\x1b[102m',
|
|
33
|
-
bgBrightYellow: '\x1b[103m',
|
|
34
|
-
bgBrightBlue: '\x1b[104m',
|
|
35
|
-
bgBrightMagenta: '\x1b[105m',
|
|
36
|
-
bgBrightCyan: '\x1b[106m',
|
|
37
|
-
bgBrightWhite: '\x1b[107m'
|
|
20
|
+
gray: '\x1b[90m'
|
|
38
21
|
};
|
|
39
22
|
class Logger {
|
|
40
23
|
constructor() {
|
|
@@ -47,12 +30,6 @@ class Logger {
|
|
|
47
30
|
[LogLevel.ERROR]: colors.red,
|
|
48
31
|
[LogLevel.DEBUG]: colors.magenta
|
|
49
32
|
};
|
|
50
|
-
this.levelBgColors = {
|
|
51
|
-
[LogLevel.INFO]: colors.bgBlue,
|
|
52
|
-
[LogLevel.WARN]: colors.bgYellow,
|
|
53
|
-
[LogLevel.ERROR]: colors.bgRed,
|
|
54
|
-
[LogLevel.DEBUG]: colors.bgMagenta
|
|
55
|
-
};
|
|
56
33
|
this.levelEmojis = {
|
|
57
34
|
[LogLevel.INFO]: 'ℹ️',
|
|
58
35
|
[LogLevel.WARN]: '⚠️',
|
|
@@ -93,11 +70,9 @@ class Logger {
|
|
|
93
70
|
formatMessage(logMessage) {
|
|
94
71
|
const { level, message, timestamp, context } = logMessage;
|
|
95
72
|
const color = this.levelColors[level];
|
|
96
|
-
const bgColor = this.levelBgColors[level];
|
|
97
73
|
const emoji = this.levelEmojis[level];
|
|
98
74
|
const timestampStr = colors.gray + `[${timestamp}]` + colors.reset;
|
|
99
|
-
|
|
100
|
-
const levelStr = colors.white + bgColor + ` ${level.toUpperCase()} ` + colors.reset;
|
|
75
|
+
const levelStr = color + `[${level.toUpperCase()}]` + colors.reset;
|
|
101
76
|
const emojiStr = emoji + ' ';
|
|
102
77
|
const messageStr = colors.bright + message + colors.reset;
|
|
103
78
|
const contextStr = context
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studious-lms/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.23",
|
|
4
4
|
"description": "Backend server for Studious application",
|
|
5
5
|
"main": "dist/exportType.js",
|
|
6
6
|
"types": "dist/exportType.d.ts",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"generate": "npx prisma generate",
|
|
19
19
|
"prepublishOnly": "npm run generate && npm run build",
|
|
20
20
|
"test": "vitest",
|
|
21
|
-
"seed": "
|
|
21
|
+
"seed": "node dist/seedDatabase.js"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@google-cloud/storage": "^7.16.0",
|
package/src/lib/fileUpload.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TRPCError } from "@trpc/server";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
|
-
import { getSignedUrl
|
|
3
|
+
import { getSignedUrl } from "./googleCloudStorage.js";
|
|
4
4
|
import { generateMediaThumbnail } from "./thumbnailGenerator.js";
|
|
5
5
|
import { prisma } from "./prisma.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
@@ -182,11 +182,7 @@ export async function createDirectUploadFile(
|
|
|
182
182
|
uploadSessionId
|
|
183
183
|
};
|
|
184
184
|
} catch (error) {
|
|
185
|
-
|
|
186
|
-
name: error.name,
|
|
187
|
-
message: error.message,
|
|
188
|
-
stack: error.stack,
|
|
189
|
-
} : error});
|
|
185
|
+
console.error('Error creating direct upload file:', error);
|
|
190
186
|
throw new TRPCError({
|
|
191
187
|
code: 'INTERNAL_SERVER_ERROR',
|
|
192
188
|
message: 'Failed to create direct upload file',
|
|
@@ -206,53 +202,17 @@ export async function confirmDirectUpload(
|
|
|
206
202
|
errorMessage?: string
|
|
207
203
|
): Promise<void> {
|
|
208
204
|
try {
|
|
209
|
-
// First fetch the file record to get the object path
|
|
210
|
-
const fileRecord = await prisma.file.findUnique({
|
|
211
|
-
where: { id: fileId },
|
|
212
|
-
select: { path: true }
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
if (!fileRecord) {
|
|
216
|
-
throw new TRPCError({
|
|
217
|
-
code: 'NOT_FOUND',
|
|
218
|
-
message: 'File record not found',
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
let actualUploadSuccess = uploadSuccess;
|
|
223
|
-
let actualErrorMessage = errorMessage;
|
|
224
|
-
|
|
225
|
-
// If uploadSuccess is true, verify the object actually exists in GCS
|
|
226
|
-
if (uploadSuccess) {
|
|
227
|
-
try {
|
|
228
|
-
const exists = await objectExists(process.env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
|
|
229
|
-
if (!exists) {
|
|
230
|
-
actualUploadSuccess = false;
|
|
231
|
-
actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';
|
|
232
|
-
logger.error(`File upload verification failed for ${fileId}: object ${fileRecord.path} not found in GCS`);
|
|
233
|
-
}
|
|
234
|
-
} catch (error) {
|
|
235
|
-
logger.error(`Error verifying file existence in GCS for ${fileId}:`, {error: error instanceof Error ? {
|
|
236
|
-
name: error.name,
|
|
237
|
-
message: error.message,
|
|
238
|
-
stack: error.stack,
|
|
239
|
-
} : error});
|
|
240
|
-
actualUploadSuccess = false;
|
|
241
|
-
actualErrorMessage = 'Failed to verify file existence in Google Cloud Storage';
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
205
|
const updateData: any = {
|
|
246
|
-
uploadStatus:
|
|
247
|
-
uploadProgress:
|
|
206
|
+
uploadStatus: uploadSuccess ? 'COMPLETED' : 'FAILED',
|
|
207
|
+
uploadProgress: uploadSuccess ? 100 : 0,
|
|
248
208
|
};
|
|
249
209
|
|
|
250
|
-
if (!
|
|
251
|
-
updateData.uploadError =
|
|
210
|
+
if (!uploadSuccess && errorMessage) {
|
|
211
|
+
updateData.uploadError = errorMessage;
|
|
252
212
|
updateData.uploadRetryCount = { increment: 1 };
|
|
253
213
|
}
|
|
254
214
|
|
|
255
|
-
if (
|
|
215
|
+
if (uploadSuccess) {
|
|
256
216
|
updateData.uploadedAt = new Date();
|
|
257
217
|
}
|
|
258
218
|
|
|
@@ -261,7 +221,7 @@ export async function confirmDirectUpload(
|
|
|
261
221
|
data: updateData
|
|
262
222
|
});
|
|
263
223
|
} catch (error) {
|
|
264
|
-
|
|
224
|
+
console.error('Error confirming direct upload:', error);
|
|
265
225
|
throw new TRPCError({
|
|
266
226
|
code: 'INTERNAL_SERVER_ERROR',
|
|
267
227
|
message: 'Failed to confirm upload',
|
|
@@ -279,29 +239,15 @@ export async function updateUploadProgress(
|
|
|
279
239
|
progress: number
|
|
280
240
|
): Promise<void> {
|
|
281
241
|
try {
|
|
282
|
-
// await prisma.file.update({
|
|
283
|
-
// where: { id: fileId },
|
|
284
|
-
// data: {
|
|
285
|
-
// uploadStatus: 'UPLOADING',
|
|
286
|
-
// uploadProgress: Math.min(100, Math.max(0, progress))
|
|
287
|
-
// }
|
|
288
|
-
// });
|
|
289
|
-
const current = await prisma.file.findUnique({ where: { id: fileId }, select: { uploadStatus: true } });
|
|
290
|
-
if (!current || ['COMPLETED','FAILED','CANCELLED'].includes(current.uploadStatus as string)) return;
|
|
291
|
-
const clamped = Math.min(100, Math.max(0, progress));
|
|
292
242
|
await prisma.file.update({
|
|
293
243
|
where: { id: fileId },
|
|
294
244
|
data: {
|
|
295
245
|
uploadStatus: 'UPLOADING',
|
|
296
|
-
uploadProgress:
|
|
246
|
+
uploadProgress: Math.min(100, Math.max(0, progress))
|
|
297
247
|
}
|
|
298
248
|
});
|
|
299
249
|
} catch (error) {
|
|
300
|
-
|
|
301
|
-
name: error.name,
|
|
302
|
-
message: error.message,
|
|
303
|
-
stack: error.stack,
|
|
304
|
-
} : error});
|
|
250
|
+
console.error('Error updating upload progress:', error);
|
|
305
251
|
throw new TRPCError({
|
|
306
252
|
code: 'INTERNAL_SERVER_ERROR',
|
|
307
253
|
message: 'Failed to update upload progress',
|
|
@@ -331,11 +277,7 @@ export async function createDirectUploadFiles(
|
|
|
331
277
|
);
|
|
332
278
|
return await Promise.all(uploadPromises);
|
|
333
279
|
} catch (error) {
|
|
334
|
-
|
|
335
|
-
name: error.name,
|
|
336
|
-
message: error.message,
|
|
337
|
-
stack: error.stack,
|
|
338
|
-
} : error});
|
|
280
|
+
console.error('Error creating direct upload files:', error);
|
|
339
281
|
throw new TRPCError({
|
|
340
282
|
code: 'INTERNAL_SERVER_ERROR',
|
|
341
283
|
message: 'Failed to create direct upload files',
|
|
@@ -62,23 +62,4 @@ export async function deleteFile(filePath: string): Promise<void> {
|
|
|
62
62
|
message: 'Failed to delete file from storage',
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Checks if an object exists in Google Cloud Storage
|
|
69
|
-
* @param bucketName The name of the bucket (unused, uses default bucket)
|
|
70
|
-
* @param objectPath The path of the object to check
|
|
71
|
-
* @returns Promise<boolean> True if the object exists, false otherwise
|
|
72
|
-
*/
|
|
73
|
-
export async function objectExists(bucketName: string, objectPath: string): Promise<boolean> {
|
|
74
|
-
try {
|
|
75
|
-
const [exists] = await bucket.file(objectPath).exists();
|
|
76
|
-
return exists;
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error('Error checking if object exists in Google Cloud Storage:', error);
|
|
79
|
-
throw new TRPCError({
|
|
80
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
81
|
-
message: 'Failed to check object existence',
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
65
|
}
|