@goscribe/server 1.1.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goscribe/server",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -42,6 +42,7 @@
42
42
  "pusher-js": "^8.4.0",
43
43
  "socket.io": "^4.8.1",
44
44
  "superjson": "^2.2.2",
45
+ "uuid": "^13.0.0",
45
46
  "zod": "^4.1.1"
46
47
  },
47
48
  "devDependencies": {
@@ -1,11 +1,10 @@
1
-
2
1
  generator client {
3
2
  provider = "prisma-client-js"
4
3
  }
5
4
 
6
5
  datasource db {
7
- provider = "postgresql"
8
- url = env("DATABASE_URL")
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
9
8
  directUrl = env("DIRECT_URL") // for shadow db in migrations
10
9
  }
11
10
 
@@ -39,49 +38,51 @@ enum QuestionType {
39
38
  // NextAuth-compatible auth models (minimal)
40
39
  //
41
40
  model User {
42
- id String @id @default(cuid())
43
- name String?
44
- email String? @unique
45
- emailVerified DateTime?
46
- passwordHash String? // for credentials login
47
- image String?
48
- session Session[]
41
+ id String @id @default(cuid())
42
+ name String?
43
+ email String? @unique
44
+ emailVerified DateTime?
45
+ passwordHash String? // for credentials login
46
+ profilePicture FileAsset? @relation(fields: [fileAssetId], references: [id])
47
+ session Session[]
49
48
 
50
49
  // Ownership
51
- folders Folder[] @relation("UserFolders")
52
- workspaces Workspace[] @relation("UserWorkspaces")
53
- invitedInWorkspaces Workspace[] @relation("WorkspaceSharedWith") // many-to-many (deprecated)
50
+ folders Folder[] @relation("UserFolders")
51
+ workspaces Workspace[] @relation("UserWorkspaces")
52
+ invitedInWorkspaces Workspace[] @relation("WorkspaceSharedWith") // many-to-many (deprecated)
54
53
  workspaceMemberships WorkspaceMember[] // proper member management
55
- uploads FileAsset[] @relation("UserUploads")
56
- artifacts Artifact[] @relation("UserArtifacts")
57
- versions ArtifactVersion[] @relation("UserArtifactVersions")
58
-
54
+ uploads FileAsset[] @relation("UserUploads")
55
+ artifacts Artifact[] @relation("UserArtifacts")
56
+ versions ArtifactVersion[] @relation("UserArtifactVersions")
57
+
59
58
  // Progress tracking
60
- flashcardProgress FlashcardProgress[] @relation("UserFlashcardProgress")
59
+ flashcardProgress FlashcardProgress[] @relation("UserFlashcardProgress")
61
60
  worksheetQuestionProgress WorksheetQuestionProgress[]
62
-
61
+
63
62
  // Invitations
64
63
  sentInvitations WorkspaceInvitation[] @relation("UserInvitations")
65
64
 
66
65
  notifications Notification[]
67
66
  chats Chat[]
68
- createdAt DateTime @default(now())
69
- updatedAt DateTime @updatedAt
67
+ createdAt DateTime @default(now())
68
+ updatedAt DateTime @updatedAt
69
+ fileAssetId String?
70
70
  }
71
71
 
72
72
  model Notification {
73
- id String @id @default(cuid())
74
- userId String
75
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
76
- content String
77
- read Boolean @default(false)
73
+ id String @id @default(cuid())
74
+ userId String
75
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
76
+ content String
77
+ read Boolean @default(false)
78
78
  createdAt DateTime @default(now())
79
79
  updatedAt DateTime @updatedAt
80
80
  }
81
+
81
82
  model Session {
82
- id String @id @default(cuid())
83
- userId String
84
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
83
+ id String @id @default(cuid())
84
+ userId String
85
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
85
86
  expires DateTime
86
87
  }
87
88
 
@@ -97,62 +98,62 @@ model VerificationToken {
97
98
  // Filesystem-like structure
98
99
  //
99
100
  model Folder {
100
- id String @id @default(cuid())
101
- name String
102
- ownerId String
103
- owner User @relation("UserFolders", fields: [ownerId], references: [id], onDelete: Cascade)
101
+ id String @id @default(cuid())
102
+ name String
103
+ ownerId String
104
+ owner User @relation("UserFolders", fields: [ownerId], references: [id], onDelete: Cascade)
104
105
 
105
106
  // Nested folders
106
- parentId String?
107
- parent Folder? @relation("FolderChildren", fields: [parentId], references: [id], onDelete: Cascade)
108
- children Folder[] @relation("FolderChildren")
109
- color String @default("#9D00FF")
107
+ parentId String?
108
+ parent Folder? @relation("FolderChildren", fields: [parentId], references: [id], onDelete: Cascade)
109
+ children Folder[] @relation("FolderChildren")
110
+ color String @default("#9D00FF")
110
111
 
111
112
  // Files (workspaces) inside folders
112
113
  workspaces Workspace[]
113
114
 
114
115
  // Metadata
115
- createdAt DateTime @default(now())
116
- updatedAt DateTime @updatedAt
116
+ createdAt DateTime @default(now())
117
+ updatedAt DateTime @updatedAt
117
118
 
118
119
  // Helpful composite index: folders per owner + parent
119
120
  @@index([ownerId, parentId])
120
121
  }
121
122
 
122
123
  model Workspace {
123
- id String @id @default(cuid())
124
+ id String @id @default(cuid())
124
125
  title String
125
- description String? // optional notes/description for the "file"
126
+ description String? // optional notes/description for the "file"
126
127
  ownerId String
127
- owner User @relation("UserWorkspaces", fields: [ownerId], references: [id], onDelete: Cascade)
128
- icon String @default("📄")
129
- color String @default("#9D00FF")
128
+ owner User @relation("UserWorkspaces", fields: [ownerId], references: [id], onDelete: Cascade)
129
+ icon String @default("📄")
130
+ color String @default("#9D00FF")
130
131
 
131
132
  // A workspace (file) lives in a folder (nullable = root)
132
- folderId String?
133
- folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
134
-
135
- channels Channel[]
133
+ folderId String?
134
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
135
+
136
+ channels Channel[]
136
137
 
137
- sharedWith User[] @relation("WorkspaceSharedWith") // many-to-many for sharing (deprecated)
138
- members WorkspaceMember[] // proper member management with roles
139
- fileBeingAnalyzed Boolean @default(false)
138
+ sharedWith User[] @relation("WorkspaceSharedWith") // many-to-many for sharing (deprecated)
139
+ members WorkspaceMember[] // proper member management with roles
140
+ fileBeingAnalyzed Boolean @default(false)
140
141
 
141
142
  analysisProgress Json?
142
143
 
143
144
  needsAnalysis Boolean @default(false)
144
145
 
145
146
  // Raw uploads attached to this workspace
146
- uploads FileAsset[]
147
+ uploads FileAsset[]
147
148
 
148
149
  // AI outputs for this workspace (study guides, flashcards, etc.)
149
- artifacts Artifact[]
150
-
150
+ artifacts Artifact[]
151
+
151
152
  // Invitations
152
153
  invitations WorkspaceInvitation[]
153
154
 
154
- createdAt DateTime @default(now())
155
- updatedAt DateTime @updatedAt
155
+ createdAt DateTime @default(now())
156
+ updatedAt DateTime @updatedAt
156
157
 
157
158
  @@index([ownerId, folderId])
158
159
  }
@@ -162,25 +163,25 @@ model Channel {
162
163
  workspaceId String
163
164
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
164
165
 
165
- name String
166
+ name String
166
167
 
167
- createdAt DateTime @default(now())
168
- chats Chat[]
168
+ createdAt DateTime @default(now())
169
+ chats Chat[]
169
170
 
170
171
  @@index([workspaceId])
171
172
  }
172
173
 
173
174
  model Chat {
174
- id String @id @default(cuid())
175
- channelId String
176
- channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
175
+ id String @id @default(cuid())
176
+ channelId String
177
+ channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
177
178
 
178
- user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
179
- userId String?
180
- message String // chat message content
179
+ user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
180
+ userId String?
181
+ message String // chat message content
181
182
 
182
- updatedAt DateTime @updatedAt
183
- createdAt DateTime @default(now())
183
+ updatedAt DateTime @updatedAt
184
+ createdAt DateTime @default(now())
184
185
 
185
186
  @@index([channelId, createdAt])
186
187
  }
@@ -189,25 +190,26 @@ model Chat {
189
190
  // User uploads (source materials for AI)
190
191
  //
191
192
  model FileAsset {
192
- id String @id @default(cuid())
193
- workspaceId String
194
- workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
193
+ id String @id @default(cuid())
194
+ workspaceId String?
195
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
195
196
 
196
- userId String
197
- user User @relation("UserUploads", fields: [userId], references: [id], onDelete: Cascade)
197
+ userId String?
198
+ user User? @relation("UserUploads", fields: [userId], references: [id], onDelete: Cascade)
198
199
 
199
- name String
200
- mimeType String
201
- size Int
202
- bucket String?
203
- objectKey String?
204
- url String? // optional if serving via signed GET per-view
205
- checksum String? // optional server-side integrity
206
- aiTranscription Json? @default("{}")
200
+ name String
201
+ mimeType String
202
+ size Int
203
+ bucket String?
204
+ objectKey String?
205
+ url String? // optional if serving via signed GET per-view
206
+ checksum String? // optional server-side integrity
207
+ aiTranscription Json? @default("{}")
207
208
 
208
- meta Json? // arbitrary metadata
209
+ meta Json? // arbitrary metadata
209
210
 
210
211
  createdAt DateTime @default(now())
212
+ User User[]
211
213
 
212
214
  @@index([workspaceId])
213
215
  @@index([userId, createdAt])
@@ -219,56 +221,56 @@ model FileAsset {
219
221
  // - Some artifact types (flashcards, worksheet) have child rows
220
222
  //
221
223
  model Artifact {
222
- id String @id @default(cuid())
224
+ id String @id @default(cuid())
223
225
  workspaceId String
224
- workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
226
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
225
227
 
226
- type ArtifactType
227
- title String
228
- isArchived Boolean @default(false)
228
+ type ArtifactType
229
+ title String
230
+ isArchived Boolean @default(false)
229
231
 
230
- generating Boolean @default(false)
231
- generatingMetadata Json?
232
+ generating Boolean @default(false)
233
+ generatingMetadata Json?
232
234
 
233
235
  // Worksheet-specific fields
234
- difficulty Difficulty? // only meaningful for WORKSHEET
235
- estimatedTime String? // only meaningful for WORKSHEET
236
+ difficulty Difficulty? // only meaningful for WORKSHEET
237
+ estimatedTime String? // only meaningful for WORKSHEET
236
238
 
237
239
  imageObjectKey String?
238
- description String?
240
+ description String?
239
241
 
240
242
  createdById String?
241
- createdBy User? @relation("UserArtifacts", fields: [createdById], references: [id], onDelete: SetNull)
243
+ createdBy User? @relation("UserArtifacts", fields: [createdById], references: [id], onDelete: SetNull)
242
244
 
243
- versions ArtifactVersion[] // text/transcript versions etc.
244
- flashcards Flashcard[] // only meaningful for FLASHCARD_SET
245
- questions WorksheetQuestion[] // only meaningful for WORKSHEET
245
+ versions ArtifactVersion[] // text/transcript versions etc.
246
+ flashcards Flashcard[] // only meaningful for FLASHCARD_SET
247
+ questions WorksheetQuestion[] // only meaningful for WORKSHEET
246
248
  podcastSegments PodcastSegment[] // only meaningful for PODCAST_EPISODE
247
249
 
248
- createdAt DateTime @default(now())
249
- updatedAt DateTime @updatedAt
250
+ createdAt DateTime @default(now())
251
+ updatedAt DateTime @updatedAt
250
252
 
251
253
  @@index([workspaceId, type])
252
254
  }
253
255
 
254
256
  model ArtifactVersion {
255
- id String @id @default(cuid())
256
- artifactId String
257
- artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
257
+ id String @id @default(cuid())
258
+ artifactId String
259
+ artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
258
260
 
259
261
  // Plain text content (e.g., Study Guide body, Meeting Summary text, Podcast transcript)
260
- content String // rich text serialized as markdown/HTML stored as TEXT
262
+ content String // rich text serialized as markdown/HTML stored as TEXT
261
263
 
262
264
  // For Podcast episodes or other media, store URLs / durations / etc. in data
263
- data Json? // e.g., { "audioUrl": "...", "durationSec": 312, "voice": "..." }
265
+ data Json? // e.g., { "audioUrl": "...", "durationSec": 312, "voice": "..." }
264
266
 
265
267
  // Version sequencing (auto-increment per artifact)
266
- version Int
268
+ version Int
267
269
 
268
270
  createdById String?
269
- createdBy User? @relation("UserArtifactVersions", fields: [createdById], references: [id], onDelete: SetNull)
271
+ createdBy User? @relation("UserArtifactVersions", fields: [createdById], references: [id], onDelete: SetNull)
270
272
 
271
- createdAt DateTime @default(now())
273
+ createdAt DateTime @default(now())
272
274
 
273
275
  @@unique([artifactId, version]) // each artifact has 1,2,3...
274
276
  @@index([artifactId])
@@ -282,15 +284,15 @@ model Flashcard {
282
284
  artifactId String
283
285
  artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
284
286
 
285
- front String // question/term
286
- back String // answer/definition
287
- tags String[] // optional keywords
288
- order Int @default(0)
287
+ front String // question/term
288
+ back String // answer/definition
289
+ tags String[] // optional keywords
290
+ order Int @default(0)
289
291
 
290
292
  // User progress tracking
291
- progress FlashcardProgress[]
293
+ progress FlashcardProgress[]
292
294
 
293
- createdAt DateTime @default(now())
295
+ createdAt DateTime @default(now())
294
296
 
295
297
  @@index([artifactId])
296
298
  }
@@ -299,33 +301,33 @@ model Flashcard {
299
301
  // User Progress on Flashcards (spaced repetition, mastery tracking)
300
302
  //
301
303
  model FlashcardProgress {
302
- id String @id @default(cuid())
303
- userId String
304
- user User @relation("UserFlashcardProgress", fields: [userId], references: [id], onDelete: Cascade)
305
-
304
+ id String @id @default(cuid())
305
+ userId String
306
+ user User @relation("UserFlashcardProgress", fields: [userId], references: [id], onDelete: Cascade)
307
+
306
308
  flashcardId String
307
309
  flashcard Flashcard @relation(fields: [flashcardId], references: [id], onDelete: Cascade)
308
310
 
309
311
  // Study statistics
310
- timesStudied Int @default(0)
311
- timesCorrect Int @default(0)
312
- timesIncorrect Int @default(0)
313
- timesIncorrectConsecutive Int @default(0) // Track consecutive failures
314
-
312
+ timesStudied Int @default(0)
313
+ timesCorrect Int @default(0)
314
+ timesIncorrect Int @default(0)
315
+ timesIncorrectConsecutive Int @default(0) // Track consecutive failures
316
+
315
317
  // Spaced repetition data
316
- easeFactor Float @default(2.5) // SM-2 algorithm ease factor
317
- interval Int @default(0) // Days until next review
318
- repetitions Int @default(0) // Consecutive correct answers
319
-
318
+ easeFactor Float @default(2.5) // SM-2 algorithm ease factor
319
+ interval Int @default(0) // Days until next review
320
+ repetitions Int @default(0) // Consecutive correct answers
321
+
320
322
  // Mastery level (0-100)
321
- masteryLevel Int @default(0)
322
-
323
+ masteryLevel Int @default(0)
324
+
323
325
  // Timestamps
324
- lastStudiedAt DateTime?
325
- nextReviewAt DateTime?
326
-
327
- createdAt DateTime @default(now())
328
- updatedAt DateTime @updatedAt
326
+ lastStudiedAt DateTime?
327
+ nextReviewAt DateTime?
328
+
329
+ createdAt DateTime @default(now())
330
+ updatedAt DateTime @updatedAt
329
331
 
330
332
  @@unique([userId, flashcardId])
331
333
  @@index([userId, nextReviewAt])
@@ -336,9 +338,9 @@ model FlashcardProgress {
336
338
  // Worksheet Questions (child items of a WORKSHEET Artifact)
337
339
  //
338
340
  model WorksheetQuestion {
339
- id String @id @default(cuid())
341
+ id String @id @default(cuid())
340
342
  artifactId String
341
- artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
343
+ artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
342
344
 
343
345
  prompt String
344
346
  answer String?
@@ -346,35 +348,35 @@ model WorksheetQuestion {
346
348
  difficulty Difficulty @default(MEDIUM)
347
349
  order Int @default(0)
348
350
 
349
- meta Json? // e.g., { "choices": ["A","B","C","D"], "correct": 1, "options": [...] }
351
+ meta Json? // e.g., { "choices": ["A","B","C","D"], "correct": 1, "options": [...] }
350
352
 
351
- createdAt DateTime @default(now())
353
+ createdAt DateTime @default(now())
354
+ progress WorksheetQuestionProgress[]
352
355
 
353
356
  @@index([artifactId])
354
- progress WorksheetQuestionProgress[]
355
357
  }
356
358
 
357
359
  //
358
360
  // Per-user progress for Worksheet Questions
359
361
  //
360
362
  model WorksheetQuestionProgress {
361
- id String @id @default(cuid())
362
- worksheetQuestionId String
363
- worksheetQuestion WorksheetQuestion @relation(fields: [worksheetQuestionId], references: [id], onDelete: Cascade)
363
+ id String @id @default(cuid())
364
+ worksheetQuestionId String
365
+ worksheetQuestion WorksheetQuestion @relation(fields: [worksheetQuestionId], references: [id], onDelete: Cascade)
364
366
 
365
- userId String
366
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
367
+ userId String
368
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
367
369
 
368
- modified Boolean @default(false)
369
- userAnswer String?
370
- correct Boolean? @default(false)
371
- completedAt DateTime?
372
- attempts Int @default(0)
373
- timeSpentSec Int?
374
- meta Json?
370
+ modified Boolean @default(false)
371
+ userAnswer String?
372
+ correct Boolean? @default(false)
373
+ completedAt DateTime?
374
+ attempts Int @default(0)
375
+ timeSpentSec Int?
376
+ meta Json?
375
377
 
376
- createdAt DateTime @default(now())
377
- updatedAt DateTime @updatedAt
378
+ createdAt DateTime @default(now())
379
+ updatedAt DateTime @updatedAt
378
380
 
379
381
  @@unique([worksheetQuestionId, userId])
380
382
  @@index([userId])
@@ -387,15 +389,15 @@ model WorkspaceMember {
387
389
  id String @id @default(cuid())
388
390
  workspaceId String
389
391
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
390
-
391
- userId String
392
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
393
-
394
- role String @default("member") // "owner", "admin", "member"
395
-
396
- joinedAt DateTime @default(now())
397
- updatedAt DateTime @updatedAt
398
-
392
+
393
+ userId String
394
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
395
+
396
+ role String @default("member") // "owner", "admin", "member"
397
+
398
+ joinedAt DateTime @default(now())
399
+ updatedAt DateTime @updatedAt
400
+
399
401
  @@unique([workspaceId, userId]) // One membership per user per workspace
400
402
  @@index([workspaceId])
401
403
  @@index([userId])
@@ -408,20 +410,20 @@ model WorkspaceInvitation {
408
410
  id String @id @default(cuid())
409
411
  workspaceId String
410
412
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
411
-
412
- email String
413
- role String @default("member") // "owner", "admin", "member"
414
- token String @unique @default(cuid()) // UUID for invitation link
415
-
413
+
414
+ email String
415
+ role String @default("member") // "owner", "admin", "member"
416
+ token String @unique @default(cuid()) // UUID for invitation link
417
+
416
418
  invitedById String
417
- invitedBy User @relation("UserInvitations", fields: [invitedById], references: [id], onDelete: Cascade)
418
-
419
- acceptedAt DateTime?
420
- expiresAt DateTime @default(dbgenerated("NOW() + INTERVAL '7 days'"))
421
-
422
- createdAt DateTime @default(now())
423
- updatedAt DateTime @updatedAt
424
-
419
+ invitedBy User @relation("UserInvitations", fields: [invitedById], references: [id], onDelete: Cascade)
420
+
421
+ acceptedAt DateTime?
422
+ expiresAt DateTime @default(dbgenerated("NOW() + INTERVAL '7 days'"))
423
+
424
+ createdAt DateTime @default(now())
425
+ updatedAt DateTime @updatedAt
426
+
425
427
  @@unique([workspaceId, email]) // One invitation per email per workspace
426
428
  @@index([token])
427
429
  @@index([workspaceId])
@@ -435,26 +437,26 @@ model PodcastSegment {
435
437
  artifactId String
436
438
  artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
437
439
 
438
- title String
439
- content String // Full text content of the segment
440
- startTime Int // Start time in seconds
441
- duration Int // Duration in seconds
442
- order Int // Display order within the episode
443
-
440
+ title String
441
+ content String // Full text content of the segment
442
+ startTime Int // Start time in seconds
443
+ duration Int // Duration in seconds
444
+ order Int // Display order within the episode
445
+
444
446
  // Audio file reference
445
- objectKey String? // Google Cloud Storage object key
446
- audioUrl String? // Cached signed URL (temporary)
447
-
447
+ objectKey String? // Google Cloud Storage object key
448
+ audioUrl String? // Cached signed URL (temporary)
449
+
448
450
  // Metadata
449
- keyPoints String[] // Array of key points
450
- meta Json? // Additional metadata (voice settings, etc.)
451
-
452
- generating Boolean @default(false)
451
+ keyPoints String[] // Array of key points
452
+ meta Json? // Additional metadata (voice settings, etc.)
453
+
454
+ generating Boolean @default(false)
453
455
  generatingMetadata Json? // Additional metadata (voice settings, etc.)
454
-
455
- createdAt DateTime @default(now())
456
- updatedAt DateTime @updatedAt
456
+
457
+ createdAt DateTime @default(now())
458
+ updatedAt DateTime @updatedAt
457
459
 
458
460
  @@index([artifactId, order]) // For efficient ordering
459
461
  @@index([artifactId, startTime]) // For time-based queries
460
- }
462
+ }
@@ -3,6 +3,8 @@ import { router, publicProcedure, authedProcedure } from '../trpc.js';
3
3
  import bcrypt from 'bcryptjs';
4
4
  import { serialize } from 'cookie';
5
5
  import crypto from 'node:crypto';
6
+ import { TRPCError } from '@trpc/server';
7
+ import { supabaseClient } from 'src/lib/storage.js';
6
8
 
7
9
  // Helper to create custom auth token
8
10
  function createCustomAuthToken(userId: string): string {
@@ -19,6 +21,54 @@ function createCustomAuthToken(userId: string): string {
19
21
  }
20
22
 
21
23
  export const auth = router({
24
+ updateProfile: publicProcedure
25
+ .input(z.object({
26
+ name: z.string().min(1),
27
+ }))
28
+ .mutation(async ({ctx, input}) => {
29
+ const { name } = input;
30
+
31
+ await ctx.db.user.update({
32
+ where: {
33
+ id: ctx.session.user.id,
34
+ },
35
+ data: {
36
+ name: name,
37
+ }
38
+ });
39
+
40
+ return {
41
+ success: true,
42
+ message: 'Profile updated successfully',
43
+ };
44
+ }),
45
+ uploadProfilePicture: publicProcedure
46
+ .mutation(async ({ctx, input}) => {
47
+ const objectKey = `profile_picture_${ctx.session.user.id}`;
48
+ const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
49
+ .from('media')
50
+ .createSignedUploadUrl(objectKey); // 5 minutes
51
+ if (signedUrlError) {
52
+ throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
53
+ }
54
+
55
+ await ctx.db.fileAsset.create({
56
+ data: {
57
+ userId: ctx.session.user.id,
58
+ name: 'Profile Picture',
59
+ mimeType: 'image/jpeg',
60
+ size: 0,
61
+ bucket: 'media',
62
+ objectKey: objectKey,
63
+ },
64
+ });
65
+
66
+ return {
67
+ success: true,
68
+ message: 'Profile picture uploaded successfully',
69
+ signedUrl: signedUrlData.signedUrl,
70
+ };
71
+ }),
22
72
  signup: publicProcedure
23
73
  .input(z.object({
24
74
  name: z.string().min(1),
@@ -85,7 +135,6 @@ export const auth = router({
85
135
  id: user.id,
86
136
  email: user.email,
87
137
  name: user.name,
88
- image: user.image,
89
138
  token: authToken
90
139
  };
91
140
  }),
@@ -108,7 +157,6 @@ export const auth = router({
108
157
  id: user.id,
109
158
  email: user.email,
110
159
  name: user.name,
111
- image: user.image
112
160
  }
113
161
  };
114
162
  }),
@@ -38,7 +38,13 @@ export const members = router({
38
38
  id: true,
39
39
  name: true,
40
40
  email: true,
41
- image: true,
41
+ profilePicture: {
42
+ select: {
43
+ id: true,
44
+ name: true,
45
+ url: true,
46
+ }
47
+ }
42
48
  }
43
49
  },
44
50
  members: {
@@ -48,7 +54,13 @@ export const members = router({
48
54
  id: true,
49
55
  name: true,
50
56
  email: true,
51
- image: true,
57
+ profilePicture: {
58
+ select: {
59
+ id: true,
60
+ name: true,
61
+ url: true,
62
+ }
63
+ }
52
64
  }
53
65
  }
54
66
  }
@@ -69,7 +81,6 @@ export const members = router({
69
81
  id: workspace.owner.id,
70
82
  name: workspace.owner.name || 'Unknown',
71
83
  email: workspace.owner.email || '',
72
- image: workspace.owner.image,
73
84
  role: 'owner' as const,
74
85
  joinedAt: workspace.createdAt,
75
86
  },
@@ -77,7 +88,6 @@ export const members = router({
77
88
  id: membership.user.id,
78
89
  name: membership.user.name || 'Unknown',
79
90
  email: membership.user.email || '',
80
- image: membership.user.image,
81
91
  role: membership.role as 'admin' | 'member',
82
92
  joinedAt: membership.joinedAt,
83
93
  }))
package/src/server.ts CHANGED
@@ -10,6 +10,7 @@ import { appRouter } from './routers/_app.js';
10
10
  import { createContext } from './context.js';
11
11
  import { prisma } from './lib/prisma.js';
12
12
  import { logger } from './lib/logger.js';
13
+ import { supabaseClient } from './lib/storage.js';
13
14
 
14
15
  const PORT = process.env.PORT ? Number(process.env.PORT) : 3001;
15
16
 
@@ -45,6 +46,18 @@ async function main() {
45
46
  res.json({ ok: true, service: 'trpc-express', ts: Date.now() });
46
47
  });
47
48
 
49
+ app.get('/profile-picture/:objectKey', async (req, res) => {
50
+ const { objectKey } = req.params;
51
+ const signedUrl = await supabaseClient.storage
52
+ .from('media')
53
+ .createSignedUrl(objectKey, 60 * 60 * 24 * 30);
54
+ if (signedUrl.error) {
55
+ return res.status(500).json({ error: 'Failed to generate signed URL' });
56
+ }
57
+ // res.json({ url: signedUrl.data.signedUrl });
58
+ res.redirect(signedUrl.data.signedUrl);
59
+ });
60
+
48
61
  // tRPC mounted under /trpc
49
62
  app.use(
50
63
  '/trpc',