@goscribe/server 1.1.3 → 1.1.5

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.
@@ -43,6 +43,25 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
43
43
  };
44
44
  transformer: true;
45
45
  }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
46
+ updateProfile: import("@trpc/server").TRPCMutationProcedure<{
47
+ input: {
48
+ name: string;
49
+ };
50
+ output: {
51
+ success: boolean;
52
+ message: string;
53
+ };
54
+ meta: object;
55
+ }>;
56
+ uploadProfilePicture: import("@trpc/server").TRPCMutationProcedure<{
57
+ input: void;
58
+ output: {
59
+ success: boolean;
60
+ message: string;
61
+ signedUrl: string;
62
+ };
63
+ meta: object;
64
+ }>;
46
65
  signup: import("@trpc/server").TRPCMutationProcedure<{
47
66
  input: {
48
67
  name: string;
@@ -65,7 +84,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
65
84
  id: string;
66
85
  email: string | null;
67
86
  name: string | null;
68
- image: string | null;
69
87
  token: string;
70
88
  };
71
89
  meta: object;
@@ -77,7 +95,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
77
95
  id: string;
78
96
  email: string | null;
79
97
  name: string | null;
80
- image: string | null;
81
98
  };
82
99
  };
83
100
  meta: object;
@@ -233,8 +250,8 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
233
250
  name: string;
234
251
  id: string;
235
252
  createdAt: Date;
236
- userId: string;
237
- workspaceId: string;
253
+ userId: string | null;
254
+ workspaceId: string | null;
238
255
  mimeType: string;
239
256
  size: number;
240
257
  bucket: string | null;
@@ -499,14 +516,12 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
499
516
  id: string;
500
517
  name: string;
501
518
  email: string;
502
- image: string | null;
503
519
  role: "admin" | "member";
504
520
  joinedAt: Date;
505
521
  } | {
506
522
  id: string;
507
523
  name: string;
508
524
  email: string;
509
- image: string | null;
510
525
  role: "owner";
511
526
  joinedAt: Date;
512
527
  })[];
@@ -1724,7 +1739,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1724
1739
  user: {
1725
1740
  name: string | null;
1726
1741
  id: string;
1727
- image: string | null;
1728
1742
  } | null;
1729
1743
  } & {
1730
1744
  id: string;
@@ -1763,7 +1777,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1763
1777
  user: {
1764
1778
  name: string | null;
1765
1779
  id: string;
1766
- image: string | null;
1767
1780
  } | null;
1768
1781
  } & {
1769
1782
  id: string;
@@ -1791,7 +1804,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1791
1804
  user: {
1792
1805
  name: string | null;
1793
1806
  id: string;
1794
- image: string | null;
1795
1807
  } | null;
1796
1808
  } & {
1797
1809
  id: string;
@@ -1818,7 +1830,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1818
1830
  user: {
1819
1831
  name: string | null;
1820
1832
  id: string;
1821
- image: string | null;
1822
1833
  } | null;
1823
1834
  } & {
1824
1835
  id: string;
@@ -1839,7 +1850,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1839
1850
  user: {
1840
1851
  name: string | null;
1841
1852
  id: string;
1842
- image: string | null;
1843
1853
  } | null;
1844
1854
  } & {
1845
1855
  id: string;
@@ -20,6 +20,25 @@ export declare const auth: import("@trpc/server").TRPCBuiltRouter<{
20
20
  };
21
21
  transformer: true;
22
22
  }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
23
+ updateProfile: import("@trpc/server").TRPCMutationProcedure<{
24
+ input: {
25
+ name: string;
26
+ };
27
+ output: {
28
+ success: boolean;
29
+ message: string;
30
+ };
31
+ meta: object;
32
+ }>;
33
+ uploadProfilePicture: import("@trpc/server").TRPCMutationProcedure<{
34
+ input: void;
35
+ output: {
36
+ success: boolean;
37
+ message: string;
38
+ signedUrl: string;
39
+ };
40
+ meta: object;
41
+ }>;
23
42
  signup: import("@trpc/server").TRPCMutationProcedure<{
24
43
  input: {
25
44
  name: string;
@@ -42,7 +61,6 @@ export declare const auth: import("@trpc/server").TRPCBuiltRouter<{
42
61
  id: string;
43
62
  email: string | null;
44
63
  name: string | null;
45
- image: string | null;
46
64
  token: string;
47
65
  };
48
66
  meta: object;
@@ -54,7 +72,6 @@ export declare const auth: import("@trpc/server").TRPCBuiltRouter<{
54
72
  id: string;
55
73
  email: string | null;
56
74
  name: string | null;
57
- image: string | null;
58
75
  };
59
76
  };
60
77
  meta: object;
@@ -3,6 +3,8 @@ import { router, publicProcedure } 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
  // Helper to create custom auth token
7
9
  function createCustomAuthToken(userId) {
8
10
  const secret = process.env.AUTH_SECRET;
@@ -16,6 +18,50 @@ function createCustomAuthToken(userId) {
16
18
  return `${base64UserId}.${signature}`;
17
19
  }
18
20
  export const auth = router({
21
+ updateProfile: publicProcedure
22
+ .input(z.object({
23
+ name: z.string().min(1),
24
+ }))
25
+ .mutation(async ({ ctx, input }) => {
26
+ const { name } = input;
27
+ await ctx.db.user.update({
28
+ where: {
29
+ id: ctx.session.user.id,
30
+ },
31
+ data: {
32
+ name: name,
33
+ }
34
+ });
35
+ return {
36
+ success: true,
37
+ message: 'Profile updated successfully',
38
+ };
39
+ }),
40
+ uploadProfilePicture: publicProcedure
41
+ .mutation(async ({ ctx, input }) => {
42
+ const objectKey = `profile_picture_${ctx.session.user.id}`;
43
+ const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
44
+ .from('media')
45
+ .createSignedUploadUrl(objectKey); // 5 minutes
46
+ if (signedUrlError) {
47
+ throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
48
+ }
49
+ await ctx.db.fileAsset.create({
50
+ data: {
51
+ userId: ctx.session.user.id,
52
+ name: 'Profile Picture',
53
+ mimeType: 'image/jpeg',
54
+ size: 0,
55
+ bucket: 'media',
56
+ objectKey: objectKey,
57
+ },
58
+ });
59
+ return {
60
+ success: true,
61
+ message: 'Profile picture uploaded successfully',
62
+ signedUrl: signedUrlData.signedUrl,
63
+ };
64
+ }),
19
65
  signup: publicProcedure
20
66
  .input(z.object({
21
67
  name: z.string().min(1),
@@ -72,7 +118,6 @@ export const auth = router({
72
118
  id: user.id,
73
119
  email: user.email,
74
120
  name: user.name,
75
- image: user.image,
76
121
  token: authToken
77
122
  };
78
123
  }),
@@ -92,7 +137,6 @@ export const auth = router({
92
137
  id: user.id,
93
138
  email: user.email,
94
139
  name: user.name,
95
- image: user.image
96
140
  }
97
141
  };
98
142
  }),
@@ -42,7 +42,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
42
42
  user: {
43
43
  name: string | null;
44
44
  id: string;
45
- image: string | null;
46
45
  } | null;
47
46
  } & {
48
47
  id: string;
@@ -81,7 +80,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
81
80
  user: {
82
81
  name: string | null;
83
82
  id: string;
84
- image: string | null;
85
83
  } | null;
86
84
  } & {
87
85
  id: string;
@@ -109,7 +107,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
109
107
  user: {
110
108
  name: string | null;
111
109
  id: string;
112
- image: string | null;
113
110
  } | null;
114
111
  } & {
115
112
  id: string;
@@ -136,7 +133,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
136
133
  user: {
137
134
  name: string | null;
138
135
  id: string;
139
- image: string | null;
140
136
  } | null;
141
137
  } & {
142
138
  id: string;
@@ -157,7 +153,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
157
153
  user: {
158
154
  name: string | null;
159
155
  id: string;
160
- image: string | null;
161
156
  } | null;
162
157
  } & {
163
158
  id: string;
@@ -14,7 +14,13 @@ export const chat = router({
14
14
  select: {
15
15
  id: true,
16
16
  name: true,
17
- image: true,
17
+ profilePicture: {
18
+ select: {
19
+ id: true,
20
+ name: true,
21
+ url: true,
22
+ }
23
+ }
18
24
  }
19
25
  }
20
26
  }
@@ -40,7 +46,6 @@ export const chat = router({
40
46
  select: {
41
47
  id: true,
42
48
  name: true,
43
- image: true,
44
49
  }
45
50
  }
46
51
  }
@@ -62,7 +67,6 @@ export const chat = router({
62
67
  select: {
63
68
  id: true,
64
69
  name: true,
65
- image: true,
66
70
  }
67
71
  }
68
72
  }
@@ -96,7 +100,6 @@ export const chat = router({
96
100
  select: {
97
101
  id: true,
98
102
  name: true,
99
- image: true,
100
103
  }
101
104
  },
102
105
  }
@@ -122,7 +125,6 @@ export const chat = router({
122
125
  select: {
123
126
  id: true,
124
127
  name: true,
125
- image: true,
126
128
  }
127
129
  }
128
130
  }
@@ -150,7 +152,6 @@ export const chat = router({
150
152
  select: {
151
153
  id: true,
152
154
  name: true,
153
- image: true,
154
155
  }
155
156
  }
156
157
  }
@@ -171,7 +172,6 @@ export const chat = router({
171
172
  select: {
172
173
  id: true,
173
174
  name: true,
174
- image: true,
175
175
  }
176
176
  }
177
177
  }
@@ -190,7 +190,6 @@ export const chat = router({
190
190
  select: {
191
191
  id: true,
192
192
  name: true,
193
- image: true,
194
193
  }
195
194
  }
196
195
  }
@@ -209,7 +208,6 @@ export const chat = router({
209
208
  select: {
210
209
  id: true,
211
210
  name: true,
212
- image: true,
213
211
  }
214
212
  }
215
213
  }
@@ -228,7 +226,6 @@ export const chat = router({
228
226
  select: {
229
227
  id: true,
230
228
  name: true,
231
- image: true,
232
229
  }
233
230
  },
234
231
  }
@@ -42,14 +42,12 @@ export declare const members: import("@trpc/server").TRPCBuiltRouter<{
42
42
  id: string;
43
43
  name: string;
44
44
  email: string;
45
- image: string | null;
46
45
  role: "admin" | "member";
47
46
  joinedAt: Date;
48
47
  } | {
49
48
  id: string;
50
49
  name: string;
51
50
  email: string;
52
- image: string | null;
53
51
  role: "owner";
54
52
  joinedAt: Date;
55
53
  })[];
@@ -37,7 +37,13 @@ export const members = router({
37
37
  id: true,
38
38
  name: true,
39
39
  email: true,
40
- image: true,
40
+ profilePicture: {
41
+ select: {
42
+ id: true,
43
+ name: true,
44
+ url: true,
45
+ }
46
+ }
41
47
  }
42
48
  },
43
49
  members: {
@@ -47,7 +53,13 @@ export const members = router({
47
53
  id: true,
48
54
  name: true,
49
55
  email: true,
50
- image: true,
56
+ profilePicture: {
57
+ select: {
58
+ id: true,
59
+ name: true,
60
+ url: true,
61
+ }
62
+ }
51
63
  }
52
64
  }
53
65
  }
@@ -66,7 +78,6 @@ export const members = router({
66
78
  id: workspace.owner.id,
67
79
  name: workspace.owner.name || 'Unknown',
68
80
  email: workspace.owner.email || '',
69
- image: workspace.owner.image,
70
81
  role: 'owner',
71
82
  joinedAt: workspace.createdAt,
72
83
  },
@@ -74,7 +85,6 @@ export const members = router({
74
85
  id: membership.user.id,
75
86
  name: membership.user.name || 'Unknown',
76
87
  email: membership.user.email || '',
77
- image: membership.user.image,
78
88
  role: membership.role,
79
89
  joinedAt: membership.joinedAt,
80
90
  }))
@@ -142,8 +142,8 @@ export declare const workspace: import("@trpc/server").TRPCBuiltRouter<{
142
142
  name: string;
143
143
  id: string;
144
144
  createdAt: Date;
145
- userId: string;
146
- workspaceId: string;
145
+ userId: string | null;
146
+ workspaceId: string | null;
147
147
  mimeType: string;
148
148
  size: number;
149
149
  bucket: string | null;
@@ -408,14 +408,12 @@ export declare const workspace: import("@trpc/server").TRPCBuiltRouter<{
408
408
  id: string;
409
409
  name: string;
410
410
  email: string;
411
- image: string | null;
412
411
  role: "admin" | "member";
413
412
  joinedAt: Date;
414
413
  } | {
415
414
  id: string;
416
415
  name: string;
417
416
  email: string;
418
- image: string | null;
419
417
  role: "owner";
420
418
  joinedAt: Date;
421
419
  })[];
package/dist/server.js CHANGED
@@ -8,6 +8,7 @@ import * as trpcExpress from '@trpc/server/adapters/express';
8
8
  import { appRouter } from './routers/_app.js';
9
9
  import { createContext } from './context.js';
10
10
  import { logger } from './lib/logger.js';
11
+ import { supabaseClient } from './lib/storage.js';
11
12
  const PORT = process.env.PORT ? Number(process.env.PORT) : 3001;
12
13
  async function main() {
13
14
  const app = express();
@@ -35,6 +36,17 @@ async function main() {
35
36
  app.get('/', (_req, res) => {
36
37
  res.json({ ok: true, service: 'trpc-express', ts: Date.now() });
37
38
  });
39
+ app.get('/profile-picture/:objectKey', async (req, res) => {
40
+ const { objectKey } = req.params;
41
+ const signedUrl = await supabaseClient.storage
42
+ .from('media')
43
+ .createSignedUrl(objectKey, 60 * 60 * 24 * 30);
44
+ if (signedUrl.error) {
45
+ return res.status(500).json({ error: 'Failed to generate signed URL' });
46
+ }
47
+ // res.json({ url: signedUrl.data.signedUrl });
48
+ res.redirect(signedUrl.data.signedUrl);
49
+ });
38
50
  // tRPC mounted under /trpc
39
51
  app.use('/trpc', trpcExpress.createExpressMiddleware({
40
52
  router: appRouter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goscribe/server",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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
  }),
@@ -15,7 +15,13 @@ export const chat = router({
15
15
  select: {
16
16
  id: true,
17
17
  name: true,
18
- image: true,
18
+ profilePicture: {
19
+ select: {
20
+ id: true,
21
+ name: true,
22
+ url: true,
23
+ }
24
+ }
19
25
  }
20
26
  }
21
27
  }
@@ -41,7 +47,6 @@ export const chat = router({
41
47
  select: {
42
48
  id: true,
43
49
  name: true,
44
- image: true,
45
50
  }
46
51
  }
47
52
  }
@@ -65,7 +70,6 @@ export const chat = router({
65
70
  select: {
66
71
  id: true,
67
72
  name: true,
68
- image: true,
69
73
  }
70
74
  }
71
75
  }
@@ -101,7 +105,6 @@ export const chat = router({
101
105
  select: {
102
106
  id: true,
103
107
  name: true,
104
- image: true,
105
108
  }
106
109
  },
107
110
  }
@@ -127,7 +130,6 @@ export const chat = router({
127
130
  select: {
128
131
  id: true,
129
132
  name: true,
130
- image: true,
131
133
  }
132
134
  }
133
135
  }
@@ -155,7 +157,6 @@ export const chat = router({
155
157
  select: {
156
158
  id: true,
157
159
  name: true,
158
- image: true,
159
160
  }
160
161
  }
161
162
  }
@@ -176,7 +177,6 @@ export const chat = router({
176
177
  select: {
177
178
  id: true,
178
179
  name: true,
179
- image: true,
180
180
  }
181
181
  }
182
182
  }
@@ -195,7 +195,6 @@ export const chat = router({
195
195
  select: {
196
196
  id: true,
197
197
  name: true,
198
- image: true,
199
198
  }
200
199
  }
201
200
  }
@@ -214,7 +213,6 @@ export const chat = router({
214
213
  select: {
215
214
  id: true,
216
215
  name: true,
217
- image: true,
218
216
  }
219
217
  }
220
218
  }
@@ -233,7 +231,6 @@ export const chat = router({
233
231
  select: {
234
232
  id: true,
235
233
  name: true,
236
- image: true,
237
234
  }
238
235
  },
239
236
  }
@@ -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',