@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.
- package/dist/index.js +99 -4
- package/dist/lib/fileUpload.d.ts +5 -0
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +19 -3
- package/dist/lib/googleCloudStorage.d.ts +2 -1
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +12 -5
- package/dist/routers/_app.d.ts +238 -22
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +79 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +39 -1
- package/dist/routers/folder.d.ts +1 -0
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +1 -0
- package/dist/routers/user.d.ts +39 -11
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +185 -25
- package/package.json +1 -1
- package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +15 -0
- package/prisma/schema.prisma +10 -1
- package/src/index.ts +111 -4
- package/src/lib/fileUpload.ts +30 -4
- package/src/lib/googleCloudStorage.ts +14 -5
- package/src/routers/assignment.ts +43 -1
- package/src/routers/folder.ts +1 -0
- package/src/routers/user.ts +200 -25
|
@@ -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;
|
|
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
|
});
|
package/dist/routers/folder.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/routers/folder.js
CHANGED
package/dist/routers/user.d.ts
CHANGED
|
@@ -22,29 +22,57 @@ export declare const userRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
22
22
|
id: string;
|
|
23
23
|
username: string;
|
|
24
24
|
profile: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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;
|
|
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"}
|
package/dist/routers/user.js
CHANGED
|
@@ -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 {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
profile: true,
|
|
169
|
+
update: {
|
|
170
|
+
...updateData,
|
|
171
|
+
updatedAt: new Date(),
|
|
67
172
|
},
|
|
68
173
|
});
|
|
69
|
-
|
|
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
|
@@ -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;
|
package/prisma/schema.prisma
CHANGED
|
@@ -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 {
|