@studious-lms/server 1.1.20 → 1.1.22

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.
@@ -2,14 +2,18 @@ import { z } from "zod";
2
2
  import { createTRPCRouter, protectedProcedure, protectedClassMemberProcedure, protectedTeacherProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
- import { uploadFiles, type UploadedFile } from "../lib/fileUpload.js";
5
+ import { createDirectUploadFiles, type DirectUploadFile, confirmDirectUpload, updateUploadProgress, type UploadedFile } from "../lib/fileUpload.js";
6
6
  import { deleteFile } from "../lib/googleCloudStorage.js";
7
7
 
8
- const fileSchema = z.object({
8
+ // DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
9
+ // Use directFileSchema instead
10
+
11
+ // New schema for direct file uploads (no base64 data)
12
+ const directFileSchema = z.object({
9
13
  name: z.string(),
10
14
  type: z.string(),
11
15
  size: z.number(),
12
- data: z.string(), // base64 encoded file data
16
+ // No data field - for direct file uploads
13
17
  });
14
18
 
15
19
  const createAssignmentSchema = z.object({
@@ -17,7 +21,7 @@ const createAssignmentSchema = z.object({
17
21
  title: z.string(),
18
22
  instructions: z.string(),
19
23
  dueDate: z.string(),
20
- files: z.array(fileSchema).optional(),
24
+ files: z.array(directFileSchema).optional(), // Use direct file schema
21
25
  existingFileIds: z.array(z.string()).optional(),
22
26
  maxGrade: z.number().optional(),
23
27
  graded: z.boolean().optional(),
@@ -35,7 +39,7 @@ const updateAssignmentSchema = z.object({
35
39
  title: z.string().optional(),
36
40
  instructions: z.string().optional(),
37
41
  dueDate: z.string().optional(),
38
- files: z.array(fileSchema).optional(),
42
+ files: z.array(directFileSchema).optional(), // Use direct file schema
39
43
  existingFileIds: z.array(z.string()).optional(),
40
44
  removedAttachments: z.array(z.string()).optional(),
41
45
  maxGrade: z.number().optional(),
@@ -61,7 +65,7 @@ const submissionSchema = z.object({
61
65
  classId: z.string(),
62
66
  submissionId: z.string(),
63
67
  submit: z.boolean().optional(),
64
- newAttachments: z.array(fileSchema).optional(),
68
+ newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
65
69
  existingFileIds: z.array(z.string()).optional(),
66
70
  removedAttachments: z.array(z.string()).optional(),
67
71
  });
@@ -72,7 +76,7 @@ const updateSubmissionSchema = z.object({
72
76
  submissionId: z.string(),
73
77
  return: z.boolean().optional(),
74
78
  gradeReceived: z.number().nullable().optional(),
75
- newAttachments: z.array(fileSchema).optional(),
79
+ newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
76
80
  existingFileIds: z.array(z.string()).optional(),
77
81
  removedAttachments: z.array(z.string()).optional(),
78
82
  feedback: z.string().optional(),
@@ -84,6 +88,36 @@ const updateSubmissionSchema = z.object({
84
88
  })).optional(),
85
89
  });
86
90
 
91
+ // New schemas for direct upload functionality
92
+ const getAssignmentUploadUrlsSchema = z.object({
93
+ assignmentId: z.string(),
94
+ classId: z.string(),
95
+ files: z.array(directFileSchema),
96
+ });
97
+
98
+ const getSubmissionUploadUrlsSchema = z.object({
99
+ submissionId: z.string(),
100
+ classId: z.string(),
101
+ files: z.array(directFileSchema),
102
+ });
103
+
104
+ const confirmAssignmentUploadSchema = z.object({
105
+ fileId: z.string(),
106
+ uploadSuccess: z.boolean(),
107
+ errorMessage: z.string().optional(),
108
+ });
109
+
110
+ const confirmSubmissionUploadSchema = z.object({
111
+ fileId: z.string(),
112
+ uploadSuccess: z.boolean(),
113
+ errorMessage: z.string().optional(),
114
+ });
115
+
116
+ const updateUploadProgressSchema = z.object({
117
+ fileId: z.string(),
118
+ progress: z.number().min(0).max(100),
119
+ });
120
+
87
121
  export const assignmentRouter = createTRPCRouter({
88
122
  order: protectedTeacherProcedure
89
123
  .input(z.object({
@@ -109,7 +143,7 @@ export const assignmentRouter = createTRPCRouter({
109
143
  targetSectionId: z.string(),
110
144
  }))
111
145
  .mutation(async ({ ctx, input }) => {
112
- const { id, targetSectionId, } = input;
146
+ const { id, targetSectionId } = input;
113
147
 
114
148
 
115
149
  const assignments = await prisma.assignment.findMany({
@@ -288,11 +322,13 @@ export const assignmentRouter = createTRPCRouter({
288
322
  )
289
323
  );
290
324
 
291
- // Upload files if provided
325
+ // NOTE: Files are now handled via direct upload endpoints
326
+ // The files field in the schema is for metadata only
327
+ // Actual file uploads should use getAssignmentUploadUrls endpoint
292
328
  let uploadedFiles: UploadedFile[] = [];
293
329
  if (files && files.length > 0) {
294
- // Store files in a class and assignment specific directory
295
- uploadedFiles = await uploadFiles(files, ctx.user.id);
330
+ // Create direct upload files instead of processing base64
331
+ uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id);
296
332
  }
297
333
 
298
334
  // Update assignment with new file attachments
@@ -380,11 +416,11 @@ export const assignmentRouter = createTRPCRouter({
380
416
  });
381
417
  }
382
418
 
383
- // Upload new files if provided
419
+ // NOTE: Files are now handled via direct upload endpoints
384
420
  let uploadedFiles: UploadedFile[] = [];
385
421
  if (files && files.length > 0) {
386
- // Store files in a class and assignment specific directory
387
- uploadedFiles = await uploadFiles(files, ctx.user.id);
422
+ // Create direct upload files instead of processing base64
423
+ uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
388
424
  }
389
425
 
390
426
  // Update assignment
@@ -955,7 +991,7 @@ export const assignmentRouter = createTRPCRouter({
955
991
  let uploadedFiles: UploadedFile[] = [];
956
992
  if (newAttachments && newAttachments.length > 0) {
957
993
  // Store files in a class and assignment specific directory
958
- uploadedFiles = await uploadFiles(newAttachments, ctx.user.id);
994
+ uploadedFiles = await createDirectUploadFiles(newAttachments, ctx.user.id, undefined, undefined, submission.id);
959
995
  }
960
996
 
961
997
  // Update submission with new file attachments
@@ -1237,7 +1273,7 @@ export const assignmentRouter = createTRPCRouter({
1237
1273
  let uploadedFiles: UploadedFile[] = [];
1238
1274
  if (newAttachments && newAttachments.length > 0) {
1239
1275
  // Store files in a class and assignment specific directory
1240
- uploadedFiles = await uploadFiles(newAttachments, ctx.user.id);
1276
+ uploadedFiles = await createDirectUploadFiles(newAttachments, ctx.user.id, undefined, undefined, submission.id);
1241
1277
  }
1242
1278
 
1243
1279
  // Update submission with new file attachments
@@ -1813,5 +1849,244 @@ export const assignmentRouter = createTRPCRouter({
1813
1849
 
1814
1850
  return updatedAssignment;
1815
1851
  }),
1852
+
1853
+ // New direct upload endpoints
1854
+ getAssignmentUploadUrls: protectedTeacherProcedure
1855
+ .input(getAssignmentUploadUrlsSchema)
1856
+ .mutation(async ({ ctx, input }) => {
1857
+ const { assignmentId, classId, files } = input;
1858
+
1859
+ if (!ctx.user) {
1860
+ throw new TRPCError({
1861
+ code: "UNAUTHORIZED",
1862
+ message: "You must be logged in to upload files",
1863
+ });
1864
+ }
1865
+
1866
+ // Verify user is a teacher of the class
1867
+ const classData = await prisma.class.findFirst({
1868
+ where: {
1869
+ id: classId,
1870
+ teachers: {
1871
+ some: {
1872
+ id: ctx.user.id,
1873
+ },
1874
+ },
1875
+ },
1876
+ });
1877
+
1878
+ if (!classData) {
1879
+ throw new TRPCError({
1880
+ code: "NOT_FOUND",
1881
+ message: "Class not found or you are not a teacher",
1882
+ });
1883
+ }
1884
+
1885
+ // Verify assignment exists and belongs to the class
1886
+ const assignment = await prisma.assignment.findFirst({
1887
+ where: {
1888
+ id: assignmentId,
1889
+ classId: classId,
1890
+ },
1891
+ });
1892
+
1893
+ if (!assignment) {
1894
+ throw new TRPCError({
1895
+ code: "NOT_FOUND",
1896
+ message: "Assignment not found",
1897
+ });
1898
+ }
1899
+
1900
+ // Create direct upload files
1901
+ const directUploadFiles = await createDirectUploadFiles(
1902
+ files,
1903
+ ctx.user.id,
1904
+ undefined, // No specific directory
1905
+ assignmentId
1906
+ );
1907
+
1908
+ return {
1909
+ success: true,
1910
+ uploadFiles: directUploadFiles,
1911
+ };
1912
+ }),
1913
+
1914
+ getSubmissionUploadUrls: protectedClassMemberProcedure
1915
+ .input(getSubmissionUploadUrlsSchema)
1916
+ .mutation(async ({ ctx, input }) => {
1917
+ const { submissionId, classId, files } = input;
1918
+
1919
+ if (!ctx.user) {
1920
+ throw new TRPCError({
1921
+ code: "UNAUTHORIZED",
1922
+ message: "You must be logged in to upload files",
1923
+ });
1924
+ }
1925
+
1926
+ // Verify submission exists and user has access
1927
+ const submission = await prisma.submission.findFirst({
1928
+ where: {
1929
+ id: submissionId,
1930
+ assignment: {
1931
+ classId: classId,
1932
+ },
1933
+ },
1934
+ include: {
1935
+ assignment: {
1936
+ include: {
1937
+ class: {
1938
+ include: {
1939
+ students: true,
1940
+ teachers: true,
1941
+ },
1942
+ },
1943
+ },
1944
+ },
1945
+ },
1946
+ });
1947
+
1948
+ if (!submission) {
1949
+ throw new TRPCError({
1950
+ code: "NOT_FOUND",
1951
+ message: "Submission not found",
1952
+ });
1953
+ }
1954
+
1955
+ // Check if user is the student who owns the submission or a teacher of the class
1956
+ const isStudent = submission.studentId === ctx.user.id;
1957
+ const isTeacher = submission.assignment.class.teachers.some(teacher => teacher.id === ctx.user?.id);
1958
+
1959
+ if (!isStudent && !isTeacher) {
1960
+ throw new TRPCError({
1961
+ code: "FORBIDDEN",
1962
+ message: "You don't have permission to upload files to this submission",
1963
+ });
1964
+ }
1965
+
1966
+ // Create direct upload files
1967
+ const directUploadFiles = await createDirectUploadFiles(
1968
+ files,
1969
+ ctx.user.id,
1970
+ undefined, // No specific directory
1971
+ undefined, // No assignment ID
1972
+ submissionId
1973
+ );
1974
+
1975
+ return {
1976
+ success: true,
1977
+ uploadFiles: directUploadFiles,
1978
+ };
1979
+ }),
1980
+
1981
+ confirmAssignmentUpload: protectedTeacherProcedure
1982
+ .input(confirmAssignmentUploadSchema)
1983
+ .mutation(async ({ ctx, input }) => {
1984
+ const { fileId, uploadSuccess, errorMessage } = input;
1985
+
1986
+ if (!ctx.user) {
1987
+ throw new TRPCError({
1988
+ code: "UNAUTHORIZED",
1989
+ message: "You must be logged in",
1990
+ });
1991
+ }
1992
+
1993
+ // Verify file belongs to user and is an assignment file
1994
+ const file = await prisma.file.findFirst({
1995
+ where: {
1996
+ id: fileId,
1997
+ userId: ctx.user.id,
1998
+ assignment: {
1999
+ isNot: null,
2000
+ },
2001
+ },
2002
+ });
2003
+
2004
+ if (!file) {
2005
+ throw new TRPCError({
2006
+ code: "NOT_FOUND",
2007
+ message: "File not found or you don't have permission",
2008
+ });
2009
+ }
2010
+
2011
+ await confirmDirectUpload(fileId, uploadSuccess, errorMessage);
2012
+
2013
+ return {
2014
+ success: true,
2015
+ message: uploadSuccess ? "Upload confirmed successfully" : "Upload failed",
2016
+ };
2017
+ }),
2018
+
2019
+ confirmSubmissionUpload: protectedClassMemberProcedure
2020
+ .input(confirmSubmissionUploadSchema)
2021
+ .mutation(async ({ ctx, input }) => {
2022
+ const { fileId, uploadSuccess, errorMessage } = input;
2023
+
2024
+ if (!ctx.user) {
2025
+ throw new TRPCError({
2026
+ code: "UNAUTHORIZED",
2027
+ message: "You must be logged in",
2028
+ });
2029
+ }
2030
+
2031
+ // Verify file belongs to user and is a submission file
2032
+ const file = await prisma.file.findFirst({
2033
+ where: {
2034
+ id: fileId,
2035
+ userId: ctx.user.id,
2036
+ submission: {
2037
+ isNot: null,
2038
+ },
2039
+ },
2040
+ });
2041
+
2042
+ if (!file) {
2043
+ throw new TRPCError({
2044
+ code: "NOT_FOUND",
2045
+ message: "File not found or you don't have permission",
2046
+ });
2047
+ }
2048
+
2049
+ await confirmDirectUpload(fileId, uploadSuccess, errorMessage);
2050
+
2051
+ return {
2052
+ success: true,
2053
+ message: uploadSuccess ? "Upload confirmed successfully" : "Upload failed",
2054
+ };
2055
+ }),
2056
+
2057
+ updateUploadProgress: protectedProcedure
2058
+ .input(updateUploadProgressSchema)
2059
+ .mutation(async ({ ctx, input }) => {
2060
+ const { fileId, progress } = input;
2061
+
2062
+ if (!ctx.user) {
2063
+ throw new TRPCError({
2064
+ code: "UNAUTHORIZED",
2065
+ message: "You must be logged in",
2066
+ });
2067
+ }
2068
+
2069
+ // Verify file belongs to user
2070
+ const file = await prisma.file.findFirst({
2071
+ where: {
2072
+ id: fileId,
2073
+ userId: ctx.user.id,
2074
+ },
2075
+ });
2076
+
2077
+ if (!file) {
2078
+ throw new TRPCError({
2079
+ code: "NOT_FOUND",
2080
+ message: "File not found or you don't have permission",
2081
+ });
2082
+ }
2083
+
2084
+ await updateUploadProgress(fileId, progress);
2085
+
2086
+ return {
2087
+ success: true,
2088
+ progress,
2089
+ };
2090
+ }),
1816
2091
  });
1817
2092
 
@@ -142,7 +142,7 @@ export const authRouter = createTRPCRouter({
142
142
  if (!user) {
143
143
  throw new TRPCError({
144
144
  code: "UNAUTHORIZED",
145
- message: "Invalid username or password",
145
+ message: "user doesn't exists.",
146
146
  });
147
147
  }
148
148
 
@@ -2,15 +2,11 @@ import { z } from "zod";
2
2
  import { createTRPCRouter, protectedProcedure, protectedClassMemberProcedure, protectedTeacherProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
- import { uploadFiles, type UploadedFile } from "../lib/fileUpload.js";
5
+ import { createDirectUploadFiles, type DirectUploadFile, type UploadedFile } from "../lib/fileUpload.js";
6
6
  import { type Folder } from "@prisma/client";
7
7
 
8
- const fileSchema = z.object({
9
- name: z.string(),
10
- type: z.string(),
11
- size: z.number(),
12
- data: z.string(), // base64 encoded file data
13
- });
8
+ // DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
9
+ // Use directFileSchema instead
14
10
 
15
11
  const createFolderSchema = z.object({
16
12
  name: z.string(),
@@ -18,9 +14,17 @@ const createFolderSchema = z.object({
18
14
  color: z.string().optional(),
19
15
  });
20
16
 
17
+ // New schema for direct file uploads (no base64 data)
18
+ const directFileSchema = z.object({
19
+ name: z.string(),
20
+ type: z.string(),
21
+ size: z.number(),
22
+ // No data field - for direct file uploads
23
+ });
24
+
21
25
  const uploadFilesToFolderSchema = z.object({
22
26
  folderId: z.string(),
23
- files: z.array(fileSchema),
27
+ files: z.array(directFileSchema), // Use direct file schema
24
28
  });
25
29
 
26
30
  const getRootFolderSchema = z.object({
@@ -469,7 +473,7 @@ export const folderRouter = createTRPCRouter({
469
473
  }
470
474
 
471
475
  // Upload files
472
- const uploadedFiles = await uploadFiles(files, ctx.user.id, folder.id);
476
+ const uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, folder.id);
473
477
 
474
478
  // Create file records in database
475
479
  // const fileRecords = await prisma.file.createMany({
@@ -10,7 +10,8 @@ import {
10
10
  } from '../utils/inference.js';
11
11
  import { logger } from '../utils/logger.js';
12
12
  import { isAIUser } from '../utils/aiUser.js';
13
- import { uploadFile } from '../lib/googleCloudStorage.js';
13
+ // DEPRECATED: uploadFile removed - use direct upload instead
14
+ // import { uploadFile } from '../lib/googleCloudStorage.js';
14
15
  import { createPdf } from "../lib/jsonConversion.js"
15
16
  import OpenAI from 'openai';
16
17
  import { v4 as uuidv4 } from "uuid";
@@ -924,7 +925,8 @@ WHEN CREATING COURSE MATERIALS (docs field):
924
925
 
925
926
 
926
927
  logger.info(`PDF ${i + 1} generated successfully`, { labChatId, title: doc.title });
927
- const gcpResult = await uploadFile(Buffer.from(pdfBytes).toString('base64'), `class/generated/${fullLabChat.classId}/${filename}`, 'application/pdf');
928
+ // DEPRECATED: Base64 upload removed - use direct upload instead
929
+ // const gcpResult = await uploadFile(Buffer.from(pdfBytes).toString('base64'), `class/generated/${fullLabChat.classId}/${filename}`, 'application/pdf');
928
930
  logger.info(`PDF ${i + 1} uploaded successfully`, { labChatId, filename });
929
931
 
930
932
  const file = await prisma.file.create({
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
- import { uploadFiles, type UploadedFile } from "../lib/fileUpload.js";
5
+ import { createDirectUploadFiles, type DirectUploadFile } from "../lib/fileUpload.js";
6
6
  import { getSignedUrl } from "../lib/googleCloudStorage.js";
7
7
  import { logger } from "../utils/logger.js";
8
8
  import { bucket } from "../lib/googleCloudStorage.js";