@studious-lms/server 1.1.2 → 1.1.4

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.
@@ -771,6 +771,7 @@ export declare const assignmentRouter: import("@trpc/server").TRPCBuiltRouter<{
771
771
  classId: string;
772
772
  assignmentId: string;
773
773
  submissionId: string;
774
+ feedback?: string | undefined;
774
775
  gradeReceived?: number | null | undefined;
775
776
  existingFileIds?: string[] | undefined;
776
777
  removedAttachments?: string[] | undefined;
@@ -1235,5 +1236,83 @@ export declare const assignmentRouter: import("@trpc/server").TRPCBuiltRouter<{
1235
1236
  };
1236
1237
  meta: object;
1237
1238
  }>;
1239
+ detachGradingBoundary: import("@trpc/server").TRPCMutationProcedure<{
1240
+ input: {
1241
+ [x: string]: unknown;
1242
+ classId: string;
1243
+ assignmentId: string;
1244
+ };
1245
+ output: {
1246
+ section: {
1247
+ id: string;
1248
+ name: string;
1249
+ color: string | null;
1250
+ classId: string;
1251
+ order: number | null;
1252
+ } | null;
1253
+ teacher: {
1254
+ id: string;
1255
+ username: string;
1256
+ email: string;
1257
+ password: string;
1258
+ verified: boolean;
1259
+ role: import(".prisma/client").$Enums.UserRole;
1260
+ profileId: string | null;
1261
+ schoolId: string | null;
1262
+ };
1263
+ attachments: {
1264
+ path: string;
1265
+ type: string;
1266
+ id: string;
1267
+ name: string;
1268
+ size: number | null;
1269
+ uploadedAt: Date | null;
1270
+ assignmentId: string | null;
1271
+ submissionId: string | null;
1272
+ userId: string | null;
1273
+ thumbnailId: string | null;
1274
+ annotationId: string | null;
1275
+ classDraftId: string | null;
1276
+ folderId: string | null;
1277
+ }[];
1278
+ eventAttached: {
1279
+ id: string;
1280
+ name: string | null;
1281
+ color: string | null;
1282
+ location: string | null;
1283
+ startTime: Date;
1284
+ endTime: Date;
1285
+ remarks: string | null;
1286
+ classId: string | null;
1287
+ userId: string | null;
1288
+ } | null;
1289
+ gradingBoundary: {
1290
+ id: string;
1291
+ classId: string;
1292
+ structured: string;
1293
+ } | null;
1294
+ } & {
1295
+ type: import(".prisma/client").$Enums.AssignmentType;
1296
+ id: string;
1297
+ title: string;
1298
+ dueDate: Date;
1299
+ maxGrade: number | null;
1300
+ classId: string;
1301
+ eventId: string | null;
1302
+ markSchemeId: string | null;
1303
+ gradingBoundaryId: string | null;
1304
+ instructions: string;
1305
+ weight: number;
1306
+ graded: boolean;
1307
+ sectionId: string | null;
1308
+ template: boolean;
1309
+ createdAt: Date | null;
1310
+ modifiedAt: Date | null;
1311
+ teacherId: string;
1312
+ inProgress: boolean;
1313
+ order: number | null;
1314
+ };
1315
+ meta: object;
1316
+ }>;
1238
1317
  }>>;
1239
1318
  //# sourceMappingURL=assignment.d.ts.map
@@ -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;AAqFxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAioD3B,CAAC"}
1
+ {"version":3,"file":"assignment.d.ts","sourceRoot":"","sources":["../../src/routers/assignment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAsFxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0qD3B,CAAC"}
@@ -68,6 +68,7 @@ const updateSubmissionSchema = z.object({
68
68
  newAttachments: z.array(fileSchema).optional(),
69
69
  existingFileIds: z.array(z.string()).optional(),
70
70
  removedAttachments: z.array(z.string()).optional(),
71
+ feedback: z.string().optional(),
71
72
  rubricGrades: z.array(z.object({
72
73
  criteriaId: z.string(),
73
74
  selectedLevelId: z.string(),
@@ -1037,7 +1038,7 @@ export const assignmentRouter = createTRPCRouter({
1037
1038
  message: "User must be authenticated",
1038
1039
  });
1039
1040
  }
1040
- const { submissionId, return: returnSubmission, gradeReceived, newAttachments, existingFileIds, removedAttachments, rubricGrades } = input;
1041
+ const { submissionId, return: returnSubmission, gradeReceived, newAttachments, existingFileIds, removedAttachments, rubricGrades, feedback } = input;
1041
1042
  const submission = await prisma.submission.findFirst({
1042
1043
  where: {
1043
1044
  id: submissionId,
@@ -1181,6 +1182,7 @@ export const assignmentRouter = createTRPCRouter({
1181
1182
  data: {
1182
1183
  ...(gradeReceived !== undefined && { gradeReceived }),
1183
1184
  ...(rubricGrades && { rubricState: JSON.stringify(rubricGrades) }),
1185
+ ...(feedback && { teacherComments: feedback }),
1184
1186
  ...(removedAttachments && removedAttachments.length > 0 && {
1185
1187
  annotations: {
1186
1188
  deleteMany: {
@@ -1188,6 +1190,7 @@ export const assignmentRouter = createTRPCRouter({
1188
1190
  },
1189
1191
  },
1190
1192
  }),
1193
+ ...(returnSubmission && { returned: returnSubmission }),
1191
1194
  },
1192
1195
  include: {
1193
1196
  attachments: {
@@ -1605,4 +1608,39 @@ export const assignmentRouter = createTRPCRouter({
1605
1608
  });
1606
1609
  return updatedAssignment;
1607
1610
  }),
1611
+ detachGradingBoundary: protectedTeacherProcedure
1612
+ .input(z.object({
1613
+ classId: z.string(),
1614
+ assignmentId: z.string(),
1615
+ }))
1616
+ .mutation(async ({ ctx, input }) => {
1617
+ const { assignmentId } = input;
1618
+ const assignment = await prisma.assignment.findFirst({
1619
+ where: {
1620
+ id: assignmentId,
1621
+ },
1622
+ });
1623
+ if (!assignment) {
1624
+ throw new TRPCError({
1625
+ code: "NOT_FOUND",
1626
+ message: "Assignment not found",
1627
+ });
1628
+ }
1629
+ const updatedAssignment = await prisma.assignment.update({
1630
+ where: { id: assignmentId },
1631
+ data: {
1632
+ gradingBoundary: {
1633
+ disconnect: true,
1634
+ },
1635
+ },
1636
+ include: {
1637
+ attachments: true,
1638
+ section: true,
1639
+ teacher: true,
1640
+ eventAttached: true,
1641
+ gradingBoundary: true,
1642
+ },
1643
+ });
1644
+ return updatedAssignment;
1645
+ }),
1608
1646
  });
@@ -78,6 +78,7 @@ export declare const folderRouter: import("@trpc/server").TRPCBuiltRouter<{
78
78
  childFolders: number;
79
79
  };
80
80
  name: string;
81
+ color: string | null;
81
82
  }[];
82
83
  parentFolder: {
83
84
  id: string;
@@ -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;AA4BxB,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmuBvB,CAAC"}
1
+ {"version":3,"file":"folder.d.ts","sourceRoot":"","sources":["../../src/routers/folder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA4BxB,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAouBvB,CAAC"}
@@ -167,6 +167,7 @@ export const folderRouter = createTRPCRouter({
167
167
  select: {
168
168
  id: true,
169
169
  name: true,
170
+ color: true,
170
171
  _count: {
171
172
  select: {
172
173
  files: true,
@@ -22,29 +22,57 @@ export declare const userRouter: import("@trpc/server").TRPCBuiltRouter<{
22
22
  id: string;
23
23
  username: string;
24
24
  profile: {
25
- id: string;
26
- userId: string;
27
- } | null;
25
+ displayName: any;
26
+ bio: any;
27
+ location: any;
28
+ website: any;
29
+ profilePicture: string | null;
30
+ profilePictureThumbnail: string | null;
31
+ };
28
32
  };
29
33
  meta: object;
30
34
  }>;
31
35
  updateProfile: import("@trpc/server").TRPCMutationProcedure<{
32
36
  input: {
33
- profile: Record<string, any>;
37
+ profile?: {
38
+ location?: string | null | undefined;
39
+ displayName?: string | null | undefined;
40
+ bio?: string | null | undefined;
41
+ website?: string | null | undefined;
42
+ } | undefined;
34
43
  profilePicture?: {
35
- type: string;
36
- name: string;
37
- size: number;
38
- data: string;
44
+ filePath: string;
45
+ fileName: string;
46
+ fileType: string;
47
+ fileSize: number;
48
+ } | undefined;
49
+ dicebearAvatar?: {
50
+ url: string;
39
51
  } | undefined;
40
52
  };
41
53
  output: {
42
54
  id: string;
43
55
  username: string;
44
56
  profile: {
45
- id: string;
46
- userId: string;
47
- } | null;
57
+ displayName: any;
58
+ bio: any;
59
+ location: any;
60
+ website: any;
61
+ profilePicture: string | null;
62
+ profilePictureThumbnail: string | null;
63
+ };
64
+ };
65
+ meta: object;
66
+ }>;
67
+ getUploadUrl: import("@trpc/server").TRPCMutationProcedure<{
68
+ input: {
69
+ fileName: string;
70
+ fileType: string;
71
+ };
72
+ output: {
73
+ uploadUrl: string;
74
+ filePath: string;
75
+ fileName: string;
48
76
  };
49
77
  meta: object;
50
78
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkBxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DrB,CAAC"}
1
+ {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2DxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqMrB,CAAC"}
@@ -2,16 +2,50 @@ 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 } from "../lib/fileUpload.js";
6
- const fileSchema = z.object({
7
- name: z.string(),
8
- type: z.string(),
9
- size: z.number(),
10
- data: z.string(), // base64 encoded file data
5
+ import { getSignedUrl } from "../lib/googleCloudStorage.js";
6
+ import { logger } from "../utils/logger.js";
7
+ // Helper function to convert file path to backend proxy URL
8
+ function getFileUrl(filePath) {
9
+ if (!filePath)
10
+ return null;
11
+ // If it's already a full URL (DiceBear or external), return as is
12
+ if (filePath.startsWith('http')) {
13
+ return filePath;
14
+ }
15
+ // Convert GCS path to full backend proxy URL
16
+ const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
17
+ return `${backendUrl}/api/files/${encodeURIComponent(filePath)}`;
18
+ }
19
+ // For direct file uploads (file already uploaded to GCS)
20
+ const fileUploadSchema = z.object({
21
+ filePath: z.string().min(1, "File path is required"),
22
+ fileName: z.string().min(1, "File name is required"),
23
+ fileType: z.string().regex(/^image\/(jpeg|jpg|png|gif|webp)$/i, "Only image files (JPEG, PNG, GIF, WebP) are allowed"),
24
+ fileSize: z.number().max(5 * 1024 * 1024, "File size must be less than 5MB"),
25
+ });
26
+ // For DiceBear avatar URL
27
+ const dicebearSchema = z.object({
28
+ url: z.string().url("Invalid DiceBear avatar URL"),
29
+ });
30
+ const profileSchema = z.object({
31
+ displayName: z.string().nullable().optional().transform(val => val === null ? undefined : val),
32
+ bio: z.string().nullable().optional().transform(val => val === null ? undefined : val),
33
+ location: z.string().nullable().optional().transform(val => val === null ? undefined : val),
34
+ website: z.union([
35
+ z.string().url(),
36
+ z.literal(""),
37
+ z.null().transform(() => undefined)
38
+ ]).optional(),
11
39
  });
12
40
  const updateProfileSchema = z.object({
13
- profile: z.record(z.any()),
14
- profilePicture: fileSchema.optional(),
41
+ profile: profileSchema.optional(),
42
+ // Support both custom file upload and DiceBear avatar
43
+ profilePicture: fileUploadSchema.optional(),
44
+ dicebearAvatar: dicebearSchema.optional(),
45
+ });
46
+ const getUploadUrlSchema = z.object({
47
+ fileName: z.string().min(1, "File name is required"),
48
+ fileType: z.string().regex(/^image\/(jpeg|jpg|png|gif|webp)$/i, "Only image files are allowed"),
15
49
  });
16
50
  export const userRouter = createTRPCRouter({
17
51
  getProfile: protectedProcedure
@@ -27,7 +61,6 @@ export const userRouter = createTRPCRouter({
27
61
  select: {
28
62
  id: true,
29
63
  username: true,
30
- profile: true,
31
64
  },
32
65
  });
33
66
  if (!user) {
@@ -36,7 +69,29 @@ export const userRouter = createTRPCRouter({
36
69
  message: "User not found",
37
70
  });
38
71
  }
39
- return user;
72
+ // Get user profile separately
73
+ const userProfile = await prisma.userProfile.findUnique({
74
+ where: { userId: ctx.user.id },
75
+ });
76
+ return {
77
+ id: user.id,
78
+ username: user.username,
79
+ profile: userProfile ? {
80
+ displayName: userProfile.displayName || null,
81
+ bio: userProfile.bio || null,
82
+ location: userProfile.location || null,
83
+ website: userProfile.website || null,
84
+ profilePicture: getFileUrl(userProfile.profilePicture),
85
+ profilePictureThumbnail: getFileUrl(userProfile.profilePictureThumbnail),
86
+ } : {
87
+ displayName: null,
88
+ bio: null,
89
+ location: null,
90
+ website: null,
91
+ profilePicture: null,
92
+ profilePictureThumbnail: null,
93
+ },
94
+ };
40
95
  }),
41
96
  updateProfile: protectedProcedure
42
97
  .input(updateProfileSchema)
@@ -47,25 +102,130 @@ export const userRouter = createTRPCRouter({
47
102
  message: "User must be authenticated",
48
103
  });
49
104
  }
50
- let uploadedFiles = [];
105
+ // Get current profile to clean up old profile picture
106
+ const currentProfile = await prisma.userProfile.findUnique({
107
+ where: { userId: ctx.user.id },
108
+ });
109
+ let profilePictureUrl = null;
110
+ let profilePictureThumbnail = null;
111
+ // Handle custom profile picture (already uploaded to GCS)
51
112
  if (input.profilePicture) {
52
- // Store profile picture in a user-specific directory
53
- uploadedFiles = await uploadFiles([input.profilePicture], ctx.user.id, `users/${ctx.user.id}/profile`);
54
- // Add profile picture path to profile data
55
- input.profile.profilePicture = uploadedFiles[0].path;
56
- input.profile.profilePictureThumbnail = uploadedFiles[0].thumbnailId;
113
+ try {
114
+ // File is already uploaded to GCS, just use the path
115
+ profilePictureUrl = input.profilePicture.filePath;
116
+ // Generate thumbnail for the uploaded file
117
+ // TODO: Implement thumbnail generation for direct uploads
118
+ profilePictureThumbnail = null;
119
+ // Clean up old profile picture if it exists
120
+ if (currentProfile?.profilePicture) {
121
+ // TODO: Implement file deletion logic here
122
+ // await deleteFile((currentProfile as any).profilePicture);
123
+ }
124
+ }
125
+ catch (error) {
126
+ logger.error('Profile picture processing failed', {
127
+ userId: ctx.user.id,
128
+ error: error instanceof Error ? error.message : 'Unknown error'
129
+ });
130
+ throw new TRPCError({
131
+ code: "INTERNAL_SERVER_ERROR",
132
+ message: "Failed to process profile picture. Please try again.",
133
+ });
134
+ }
57
135
  }
58
- const updatedUser = await prisma.user.update({
59
- where: { id: ctx.user.id },
60
- data: {
61
- profile: input.profile,
136
+ // Handle DiceBear avatar URL
137
+ if (input.dicebearAvatar) {
138
+ profilePictureUrl = input.dicebearAvatar.url;
139
+ // No thumbnail for DiceBear avatars since they're SVG URLs
140
+ profilePictureThumbnail = null;
141
+ }
142
+ // Prepare update data
143
+ const updateData = {};
144
+ if (input.profile) {
145
+ if (input.profile.displayName !== undefined && input.profile.displayName !== null) {
146
+ updateData.displayName = input.profile.displayName;
147
+ }
148
+ if (input.profile.bio !== undefined && input.profile.bio !== null) {
149
+ updateData.bio = input.profile.bio;
150
+ }
151
+ if (input.profile.location !== undefined && input.profile.location !== null) {
152
+ updateData.location = input.profile.location;
153
+ }
154
+ if (input.profile.website !== undefined && input.profile.website !== null) {
155
+ updateData.website = input.profile.website;
156
+ }
157
+ }
158
+ if (profilePictureUrl !== null)
159
+ updateData.profilePicture = profilePictureUrl;
160
+ if (profilePictureThumbnail !== null)
161
+ updateData.profilePictureThumbnail = profilePictureThumbnail;
162
+ // Upsert user profile with structured data
163
+ const updatedProfile = await prisma.userProfile.upsert({
164
+ where: { userId: ctx.user.id },
165
+ create: {
166
+ userId: ctx.user.id,
167
+ ...updateData,
62
168
  },
63
- select: {
64
- id: true,
65
- username: true,
66
- profile: true,
169
+ update: {
170
+ ...updateData,
171
+ updatedAt: new Date(),
67
172
  },
68
173
  });
69
- return updatedUser;
174
+ // Get username for response
175
+ const user = await prisma.user.findUnique({
176
+ where: { id: ctx.user.id },
177
+ select: { username: true },
178
+ });
179
+ return {
180
+ id: ctx.user.id,
181
+ username: user?.username || '',
182
+ profile: {
183
+ displayName: updatedProfile.displayName || null,
184
+ bio: updatedProfile.bio || null,
185
+ location: updatedProfile.location || null,
186
+ website: updatedProfile.website || null,
187
+ profilePicture: getFileUrl(updatedProfile.profilePicture),
188
+ profilePictureThumbnail: getFileUrl(updatedProfile.profilePictureThumbnail),
189
+ },
190
+ };
191
+ }),
192
+ getUploadUrl: protectedProcedure
193
+ .input(getUploadUrlSchema)
194
+ .mutation(async ({ ctx, input }) => {
195
+ if (!ctx.user) {
196
+ throw new TRPCError({
197
+ code: "UNAUTHORIZED",
198
+ message: "User must be authenticated",
199
+ });
200
+ }
201
+ try {
202
+ // Generate unique filename
203
+ const fileExtension = input.fileName.split('.').pop();
204
+ const uniqueFilename = `${ctx.user.id}-${Date.now()}.${fileExtension}`;
205
+ const filePath = `users/${ctx.user.id}/profile/${uniqueFilename}`;
206
+ // Generate signed URL for direct upload (write permission)
207
+ const uploadUrl = await getSignedUrl(filePath, 'write', input.fileType);
208
+ logger.info('Generated upload URL', {
209
+ userId: ctx.user.id,
210
+ filePath,
211
+ fileName: uniqueFilename,
212
+ fileType: input.fileType
213
+ });
214
+ return {
215
+ uploadUrl,
216
+ filePath,
217
+ fileName: uniqueFilename,
218
+ };
219
+ }
220
+ catch (error) {
221
+ logger.error('Failed to generate upload URL', {
222
+ userId: ctx.user.id,
223
+ error: error instanceof Error ? error.message : 'Unknown error'
224
+ });
225
+ throw new TRPCError({
226
+ code: "INTERNAL_SERVER_ERROR",
227
+ message: "Failed to generate upload URL",
228
+ });
229
+ }
70
230
  }),
71
231
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -0,0 +1,15 @@
1
+ /*
2
+ Warnings:
3
+
4
+ - Added the required column `updatedAt` to the `UserProfile` table without a default value. This is not possible if the table is not empty.
5
+
6
+ */
7
+ -- AlterTable
8
+ ALTER TABLE "public"."UserProfile" ADD COLUMN "bio" TEXT,
9
+ ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ ADD COLUMN "displayName" TEXT,
11
+ ADD COLUMN "location" TEXT,
12
+ ADD COLUMN "profilePicture" TEXT,
13
+ ADD COLUMN "profilePictureThumbnail" TEXT,
14
+ ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
15
+ ADD COLUMN "website" TEXT;
@@ -82,7 +82,16 @@ model UserProfile {
82
82
  id String @id @default(uuid())
83
83
  userId String @unique
84
84
  user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
85
-
85
+
86
+ displayName String?
87
+ bio String?
88
+ location String?
89
+ website String?
90
+ profilePicture String?
91
+ profilePictureThumbnail String?
92
+
93
+ createdAt DateTime @default(now())
94
+ updatedAt DateTime @updatedAt
86
95
  }
87
96
 
88
97
  model Class {