@studious-lms/server 1.1.9 → 1.1.10

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 CHANGED
@@ -18,6 +18,8 @@ app.use(cors({
18
18
  'http://localhost:3001', // Server port
19
19
  'http://127.0.0.1:3000', // Alternative localhost
20
20
  'http://127.0.0.1:3001', // Alternative localhost
21
+ 'https://www.studious.sh', // Production frontend
22
+ 'https://studious.sh', // Production frontend (without www)
21
23
  process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
22
24
  ],
23
25
  credentials: true,
@@ -32,6 +34,8 @@ app.options('*', (req, res) => {
32
34
  'http://localhost:3001',
33
35
  'http://127.0.0.1:3000',
34
36
  'http://127.0.0.1:3001',
37
+ 'https://www.studious.sh', // Production frontend
38
+ 'https://studious.sh', // Production frontend (without www)
35
39
  process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
36
40
  ];
37
41
  const origin = req.headers.origin;
@@ -82,6 +86,8 @@ const io = new Server(httpServer, {
82
86
  'http://localhost:3001', // Server port
83
87
  'http://127.0.0.1:3000', // Alternative localhost
84
88
  'http://127.0.0.1:3001', // Alternative localhost
89
+ 'https://www.studious.sh', // Production frontend
90
+ 'https://studious.sh', // Production frontend (without www)
85
91
  process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
86
92
  ],
87
93
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
@@ -137,6 +143,68 @@ app.get('/api/files/:filePath', async (req, res) => {
137
143
  res.status(500).json({ error: 'Internal server error' });
138
144
  }
139
145
  });
146
+ // File upload endpoint for secure file uploads (supports both POST and PUT)
147
+ app.post('/api/upload/:filePath', async (req, res) => {
148
+ handleFileUpload(req, res);
149
+ });
150
+ app.put('/api/upload/:filePath', async (req, res) => {
151
+ handleFileUpload(req, res);
152
+ });
153
+ function handleFileUpload(req, res) {
154
+ try {
155
+ const filePath = decodeURIComponent(req.params.filePath);
156
+ console.log('File upload request:', { filePath, originalPath: req.params.filePath, method: req.method });
157
+ // Set CORS headers for upload endpoint
158
+ const origin = req.headers.origin;
159
+ const allowedOrigins = [
160
+ 'http://localhost:3000',
161
+ 'http://localhost:3001',
162
+ 'http://127.0.0.1:3000',
163
+ 'http://127.0.0.1:3001',
164
+ 'https://www.studious.sh', // Production frontend
165
+ 'https://studious.sh', // Production frontend (without www)
166
+ process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
167
+ ];
168
+ if (origin && allowedOrigins.includes(origin)) {
169
+ res.header('Access-Control-Allow-Origin', origin);
170
+ }
171
+ else {
172
+ res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
173
+ }
174
+ res.header('Access-Control-Allow-Credentials', 'true');
175
+ // Get content type from headers
176
+ const contentType = req.headers['content-type'] || 'application/octet-stream';
177
+ // Create a new file in the bucket
178
+ const file = bucket.file(filePath);
179
+ // Create a write stream to Google Cloud Storage
180
+ const writeStream = file.createWriteStream({
181
+ metadata: {
182
+ contentType,
183
+ },
184
+ });
185
+ // Handle stream events
186
+ writeStream.on('error', (error) => {
187
+ console.error('Error uploading file:', error);
188
+ if (!res.headersSent) {
189
+ res.status(500).json({ error: 'Error uploading file' });
190
+ }
191
+ });
192
+ writeStream.on('finish', () => {
193
+ console.log('File uploaded successfully:', filePath);
194
+ res.status(200).json({
195
+ success: true,
196
+ filePath,
197
+ message: 'File uploaded successfully'
198
+ });
199
+ });
200
+ // Pipe the request body to the write stream
201
+ req.pipe(writeStream);
202
+ }
203
+ catch (error) {
204
+ console.error('Error handling file upload:', error);
205
+ res.status(500).json({ error: 'Internal server error' });
206
+ }
207
+ }
140
208
  // Create caller
141
209
  const createCaller = createCallerFactory(appRouter);
142
210
  // Setup tRPC middleware
@@ -3548,6 +3548,40 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
3548
3548
  };
3549
3549
  meta: object;
3550
3550
  }>;
3551
+ update: import("@trpc/server").TRPCMutationProcedure<{
3552
+ input: {
3553
+ content: string;
3554
+ messageId: string;
3555
+ mentionedUserIds?: string[] | undefined;
3556
+ };
3557
+ output: {
3558
+ id: string;
3559
+ content: string;
3560
+ senderId: string;
3561
+ conversationId: string;
3562
+ createdAt: Date;
3563
+ sender: {
3564
+ id: string;
3565
+ username: string;
3566
+ profile: {
3567
+ displayName: string | null;
3568
+ profilePicture: string | null;
3569
+ } | null;
3570
+ };
3571
+ mentionedUserIds: string[];
3572
+ };
3573
+ meta: object;
3574
+ }>;
3575
+ delete: import("@trpc/server").TRPCMutationProcedure<{
3576
+ input: {
3577
+ messageId: string;
3578
+ };
3579
+ output: {
3580
+ success: boolean;
3581
+ messageId: string;
3582
+ };
3583
+ meta: object;
3584
+ }>;
3551
3585
  markAsRead: import("@trpc/server").TRPCMutationProcedure<{
3552
3586
  input: {
3553
3587
  conversationId: string;
@@ -7130,6 +7164,40 @@ export declare const createCaller: import("@trpc/server").TRPCRouterCaller<{
7130
7164
  };
7131
7165
  meta: object;
7132
7166
  }>;
7167
+ update: import("@trpc/server").TRPCMutationProcedure<{
7168
+ input: {
7169
+ content: string;
7170
+ messageId: string;
7171
+ mentionedUserIds?: string[] | undefined;
7172
+ };
7173
+ output: {
7174
+ id: string;
7175
+ content: string;
7176
+ senderId: string;
7177
+ conversationId: string;
7178
+ createdAt: Date;
7179
+ sender: {
7180
+ id: string;
7181
+ username: string;
7182
+ profile: {
7183
+ displayName: string | null;
7184
+ profilePicture: string | null;
7185
+ } | null;
7186
+ };
7187
+ mentionedUserIds: string[];
7188
+ };
7189
+ meta: object;
7190
+ }>;
7191
+ delete: import("@trpc/server").TRPCMutationProcedure<{
7192
+ input: {
7193
+ messageId: string;
7194
+ };
7195
+ output: {
7196
+ success: boolean;
7197
+ messageId: string;
7198
+ };
7199
+ meta: object;
7200
+ }>;
7133
7201
  markAsRead: import("@trpc/server").TRPCMutationProcedure<{
7134
7202
  input: {
7135
7203
  conversationId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"_app.d.ts","sourceRoot":"","sources":["../../src/routers/_app.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAY1E,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAepB,CAAC;AAGH,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC;AACzC,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;AAG1D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAAiC,CAAC"}
1
+ {"version":3,"file":"_app.d.ts","sourceRoot":"","sources":["../../src/routers/_app.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAY1E,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAepB,CAAC;AAGH,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC;AACzC,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;AAG1D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAAiC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/routers/conversation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuR7B,CAAC"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/routers/conversation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8R7B,CAAC"}
@@ -70,14 +70,19 @@ export const conversationRouter = createTRPCRouter({
70
70
  },
71
71
  });
72
72
  // Count unread mentions
73
+ // Use the later of lastViewedAt or lastViewedMentionAt
74
+ // This means if user viewed conversation after mention, mention is considered read
75
+ const mentionCutoffTime = lastViewedMentionAt && lastViewedAt
76
+ ? (lastViewedMentionAt > lastViewedAt ? lastViewedMentionAt : lastViewedAt)
77
+ : (lastViewedMentionAt || lastViewedAt);
73
78
  const unreadMentionCount = await prisma.mention.count({
74
79
  where: {
75
80
  userId,
76
81
  message: {
77
82
  conversationId: conversation.id,
78
83
  senderId: { not: userId },
79
- ...(lastViewedMentionAt && {
80
- createdAt: { gt: lastViewedMentionAt }
84
+ ...(mentionCutoffTime && {
85
+ createdAt: { gt: mentionCutoffTime }
81
86
  }),
82
87
  },
83
88
  },
@@ -164,12 +169,13 @@ export const conversationRouter = createTRPCRouter({
164
169
  // Verify all members exist
165
170
  const members = await prisma.user.findMany({
166
171
  where: {
167
- id: {
172
+ username: {
168
173
  in: memberIds,
169
174
  },
170
175
  },
171
176
  select: {
172
177
  id: true,
178
+ username: true,
173
179
  },
174
180
  });
175
181
  if (members.length !== memberIds.length) {
@@ -190,7 +196,7 @@ export const conversationRouter = createTRPCRouter({
190
196
  role: type === 'GROUP' ? 'ADMIN' : 'MEMBER',
191
197
  },
192
198
  ...memberIds.map((memberId) => ({
193
- userId: memberId,
199
+ userId: members.find((member) => member.username === memberId).id,
194
200
  role: 'MEMBER',
195
201
  })),
196
202
  ],
@@ -76,6 +76,40 @@ export declare const messageRouter: import("@trpc/server").TRPCBuiltRouter<{
76
76
  };
77
77
  meta: object;
78
78
  }>;
79
+ update: import("@trpc/server").TRPCMutationProcedure<{
80
+ input: {
81
+ content: string;
82
+ messageId: string;
83
+ mentionedUserIds?: string[] | undefined;
84
+ };
85
+ output: {
86
+ id: string;
87
+ content: string;
88
+ senderId: string;
89
+ conversationId: string;
90
+ createdAt: Date;
91
+ sender: {
92
+ id: string;
93
+ username: string;
94
+ profile: {
95
+ displayName: string | null;
96
+ profilePicture: string | null;
97
+ } | null;
98
+ };
99
+ mentionedUserIds: string[];
100
+ };
101
+ meta: object;
102
+ }>;
103
+ delete: import("@trpc/server").TRPCMutationProcedure<{
104
+ input: {
105
+ messageId: string;
106
+ };
107
+ output: {
108
+ success: boolean;
109
+ messageId: string;
110
+ };
111
+ meta: object;
112
+ }>;
79
113
  markAsRead: import("@trpc/server").TRPCMutationProcedure<{
80
114
  input: {
81
115
  conversationId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/routers/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsWxB,CAAC"}
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/routers/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6kBxB,CAAC"}
@@ -192,6 +192,207 @@ export const messageRouter = createTRPCRouter({
192
192
  mentionedUserIds,
193
193
  };
194
194
  }),
195
+ update: protectedProcedure
196
+ .input(z.object({
197
+ messageId: z.string(),
198
+ content: z.string().min(1).max(4000),
199
+ mentionedUserIds: z.array(z.string()).optional(),
200
+ }))
201
+ .mutation(async ({ input, ctx }) => {
202
+ const userId = ctx.user.id;
203
+ const { messageId, content, mentionedUserIds = [] } = input;
204
+ // Get the existing message and verify user is the sender
205
+ const existingMessage = await prisma.message.findUnique({
206
+ where: { id: messageId },
207
+ include: {
208
+ sender: {
209
+ select: {
210
+ id: true,
211
+ username: true,
212
+ profile: {
213
+ select: {
214
+ displayName: true,
215
+ profilePicture: true,
216
+ },
217
+ },
218
+ },
219
+ },
220
+ },
221
+ });
222
+ if (!existingMessage) {
223
+ throw new TRPCError({
224
+ code: 'NOT_FOUND',
225
+ message: 'Message not found',
226
+ });
227
+ }
228
+ if (existingMessage.senderId !== userId) {
229
+ throw new TRPCError({
230
+ code: 'FORBIDDEN',
231
+ message: 'Not the sender of this message',
232
+ });
233
+ }
234
+ // Verify user is still a member of the conversation
235
+ const membership = await prisma.conversationMember.findFirst({
236
+ where: {
237
+ conversationId: existingMessage.conversationId,
238
+ userId,
239
+ },
240
+ });
241
+ if (!membership) {
242
+ throw new TRPCError({
243
+ code: 'FORBIDDEN',
244
+ message: 'Not a member of this conversation',
245
+ });
246
+ }
247
+ // Verify mentioned users are members of the conversation
248
+ if (mentionedUserIds.length > 0) {
249
+ const mentionedMemberships = await prisma.conversationMember.findMany({
250
+ where: {
251
+ conversationId: existingMessage.conversationId,
252
+ userId: { in: mentionedUserIds },
253
+ },
254
+ });
255
+ if (mentionedMemberships.length !== mentionedUserIds.length) {
256
+ throw new TRPCError({
257
+ code: 'BAD_REQUEST',
258
+ message: 'Some mentioned users are not members of this conversation',
259
+ });
260
+ }
261
+ }
262
+ // Update message and mentions in transaction
263
+ const updatedMessage = await prisma.$transaction(async (tx) => {
264
+ // Update the message content
265
+ const message = await tx.message.update({
266
+ where: { id: messageId },
267
+ data: { content },
268
+ include: {
269
+ sender: {
270
+ select: {
271
+ id: true,
272
+ username: true,
273
+ profile: {
274
+ select: {
275
+ displayName: true,
276
+ profilePicture: true,
277
+ },
278
+ },
279
+ },
280
+ },
281
+ },
282
+ });
283
+ // Delete existing mentions
284
+ await tx.mention.deleteMany({
285
+ where: { messageId },
286
+ });
287
+ // Create new mentions if any
288
+ if (mentionedUserIds.length > 0) {
289
+ await tx.mention.createMany({
290
+ data: mentionedUserIds.map((mentionedUserId) => ({
291
+ messageId,
292
+ userId: mentionedUserId,
293
+ })),
294
+ });
295
+ }
296
+ return message;
297
+ });
298
+ // Broadcast message update to Pusher
299
+ try {
300
+ await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-updated', {
301
+ id: updatedMessage.id,
302
+ content: updatedMessage.content,
303
+ senderId: updatedMessage.senderId,
304
+ conversationId: updatedMessage.conversationId,
305
+ createdAt: updatedMessage.createdAt,
306
+ sender: updatedMessage.sender,
307
+ mentionedUserIds,
308
+ });
309
+ }
310
+ catch (error) {
311
+ console.error('Failed to broadcast message update:', error);
312
+ // Don't fail the request if Pusher fails
313
+ }
314
+ return {
315
+ id: updatedMessage.id,
316
+ content: updatedMessage.content,
317
+ senderId: updatedMessage.senderId,
318
+ conversationId: updatedMessage.conversationId,
319
+ createdAt: updatedMessage.createdAt,
320
+ sender: updatedMessage.sender,
321
+ mentionedUserIds,
322
+ };
323
+ }),
324
+ delete: protectedProcedure
325
+ .input(z.object({
326
+ messageId: z.string(),
327
+ }))
328
+ .mutation(async ({ input, ctx }) => {
329
+ const userId = ctx.user.id;
330
+ const { messageId } = input;
331
+ // Get the message and verify user is the sender
332
+ const existingMessage = await prisma.message.findUnique({
333
+ where: { id: messageId },
334
+ include: {
335
+ sender: {
336
+ select: {
337
+ id: true,
338
+ username: true,
339
+ },
340
+ },
341
+ },
342
+ });
343
+ if (!existingMessage) {
344
+ throw new TRPCError({
345
+ code: 'NOT_FOUND',
346
+ message: 'Message not found',
347
+ });
348
+ }
349
+ if (existingMessage.senderId !== userId) {
350
+ throw new TRPCError({
351
+ code: 'FORBIDDEN',
352
+ message: 'Not the sender of this message',
353
+ });
354
+ }
355
+ // Verify user is still a member of the conversation
356
+ const membership = await prisma.conversationMember.findFirst({
357
+ where: {
358
+ conversationId: existingMessage.conversationId,
359
+ userId,
360
+ },
361
+ });
362
+ if (!membership) {
363
+ throw new TRPCError({
364
+ code: 'FORBIDDEN',
365
+ message: 'Not a member of this conversation',
366
+ });
367
+ }
368
+ // Delete message and all related mentions in transaction
369
+ await prisma.$transaction(async (tx) => {
370
+ // Delete mentions first (due to foreign key constraint)
371
+ await tx.mention.deleteMany({
372
+ where: { messageId },
373
+ });
374
+ // Delete the message
375
+ await tx.message.delete({
376
+ where: { id: messageId },
377
+ });
378
+ });
379
+ // Broadcast message deletion to Pusher
380
+ try {
381
+ await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-deleted', {
382
+ messageId,
383
+ conversationId: existingMessage.conversationId,
384
+ senderId: existingMessage.senderId,
385
+ });
386
+ }
387
+ catch (error) {
388
+ console.error('Failed to broadcast message deletion:', error);
389
+ // Don't fail the request if Pusher fails
390
+ }
391
+ return {
392
+ success: true,
393
+ messageId,
394
+ };
395
+ }),
195
396
  markAsRead: protectedProcedure
196
397
  .input(z.object({
197
398
  conversationId: z.string(),
@@ -305,14 +506,19 @@ export const messageRouter = createTRPCRouter({
305
506
  },
306
507
  });
307
508
  // Count unread mentions
509
+ // Use the later of lastViewedAt or lastViewedMentionAt
510
+ // This means if user viewed conversation after mention, mention is considered read
511
+ const mentionCutoffTime = membership.lastViewedMentionAt && membership.lastViewedAt
512
+ ? (membership.lastViewedMentionAt > membership.lastViewedAt ? membership.lastViewedMentionAt : membership.lastViewedAt)
513
+ : (membership.lastViewedMentionAt || membership.lastViewedAt);
308
514
  const unreadMentionCount = await prisma.mention.count({
309
515
  where: {
310
516
  userId,
311
517
  message: {
312
518
  conversationId,
313
519
  senderId: { not: userId },
314
- ...(membership.lastViewedMentionAt && {
315
- createdAt: { gt: membership.lastViewedMentionAt }
520
+ ...(mentionCutoffTime && {
521
+ createdAt: { gt: mentionCutoffTime }
316
522
  }),
317
523
  },
318
524
  },
@@ -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;AA2DxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqMrB,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuMrB,CAAC"}
@@ -2,7 +2,6 @@ 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 { getSignedUrl } from "../lib/googleCloudStorage.js";
6
5
  import { logger } from "../utils/logger.js";
7
6
  // Helper function to convert file path to backend proxy URL
8
7
  function getFileUrl(filePath) {
@@ -203,13 +202,15 @@ export const userRouter = createTRPCRouter({
203
202
  const fileExtension = input.fileName.split('.').pop();
204
203
  const uniqueFilename = `${ctx.user.id}-${Date.now()}.${fileExtension}`;
205
204
  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);
205
+ // Generate backend proxy upload URL instead of direct GCS signed URL
206
+ const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
207
+ const uploadUrl = `${backendUrl}/api/upload/${encodeURIComponent(filePath)}`;
208
208
  logger.info('Generated upload URL', {
209
209
  userId: ctx.user.id,
210
210
  filePath,
211
211
  fileName: uniqueFilename,
212
- fileType: input.fileType
212
+ fileType: input.fileType,
213
+ uploadUrl
213
214
  });
214
215
  return {
215
216
  uploadUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
package/src/index.ts CHANGED
@@ -22,6 +22,8 @@ app.use(cors({
22
22
  'http://localhost:3001', // Server port
23
23
  'http://127.0.0.1:3000', // Alternative localhost
24
24
  'http://127.0.0.1:3001', // Alternative localhost
25
+ 'https://www.studious.sh', // Production frontend
26
+ 'https://studious.sh', // Production frontend (without www)
25
27
  process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
26
28
  ],
27
29
  credentials: true,
@@ -37,6 +39,8 @@ app.options('*', (req, res) => {
37
39
  'http://localhost:3001',
38
40
  'http://127.0.0.1:3000',
39
41
  'http://127.0.0.1:3001',
42
+ 'https://www.studious.sh', // Production frontend
43
+ 'https://studious.sh', // Production frontend (without www)
40
44
  process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
41
45
  ];
42
46
 
@@ -92,6 +96,8 @@ const io = new Server(httpServer, {
92
96
  'http://localhost:3001', // Server port
93
97
  'http://127.0.0.1:3000', // Alternative localhost
94
98
  'http://127.0.0.1:3001', // Alternative localhost
99
+ 'https://www.studious.sh', // Production frontend
100
+ 'https://studious.sh', // Production frontend (without www)
95
101
  process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
96
102
  ],
97
103
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
@@ -158,6 +164,79 @@ app.get('/api/files/:filePath', async (req, res) => {
158
164
  }
159
165
  });
160
166
 
167
+ // File upload endpoint for secure file uploads (supports both POST and PUT)
168
+ app.post('/api/upload/:filePath', async (req, res) => {
169
+ handleFileUpload(req, res);
170
+ });
171
+
172
+ app.put('/api/upload/:filePath', async (req, res) => {
173
+ handleFileUpload(req, res);
174
+ });
175
+
176
+ function handleFileUpload(req: any, res: any) {
177
+ try {
178
+ const filePath = decodeURIComponent(req.params.filePath);
179
+ console.log('File upload request:', { filePath, originalPath: req.params.filePath, method: req.method });
180
+
181
+ // Set CORS headers for upload endpoint
182
+ const origin = req.headers.origin;
183
+ const allowedOrigins = [
184
+ 'http://localhost:3000',
185
+ 'http://localhost:3001',
186
+ 'http://127.0.0.1:3000',
187
+ 'http://127.0.0.1:3001',
188
+ 'https://www.studious.sh', // Production frontend
189
+ 'https://studious.sh', // Production frontend (without www)
190
+ process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
191
+ ];
192
+
193
+ if (origin && allowedOrigins.includes(origin)) {
194
+ res.header('Access-Control-Allow-Origin', origin);
195
+ } else {
196
+ res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
197
+ }
198
+
199
+ res.header('Access-Control-Allow-Credentials', 'true');
200
+
201
+ // Get content type from headers
202
+ const contentType = req.headers['content-type'] || 'application/octet-stream';
203
+
204
+ // Create a new file in the bucket
205
+ const file = bucket.file(filePath);
206
+
207
+ // Create a write stream to Google Cloud Storage
208
+ const writeStream = file.createWriteStream({
209
+ metadata: {
210
+ contentType,
211
+ },
212
+ });
213
+
214
+ // Handle stream events
215
+ writeStream.on('error', (error) => {
216
+ console.error('Error uploading file:', error);
217
+ if (!res.headersSent) {
218
+ res.status(500).json({ error: 'Error uploading file' });
219
+ }
220
+ });
221
+
222
+ writeStream.on('finish', () => {
223
+ console.log('File uploaded successfully:', filePath);
224
+ res.status(200).json({
225
+ success: true,
226
+ filePath,
227
+ message: 'File uploaded successfully'
228
+ });
229
+ });
230
+
231
+ // Pipe the request body to the write stream
232
+ req.pipe(writeStream);
233
+
234
+ } catch (error) {
235
+ console.error('Error handling file upload:', error);
236
+ res.status(500).json({ error: 'Internal server error' });
237
+ }
238
+ }
239
+
161
240
  // Create caller
162
241
  const createCaller = createCallerFactory(appRouter);
163
242
 
@@ -76,14 +76,20 @@ export const conversationRouter = createTRPCRouter({
76
76
  });
77
77
 
78
78
  // Count unread mentions
79
+ // Use the later of lastViewedAt or lastViewedMentionAt
80
+ // This means if user viewed conversation after mention, mention is considered read
81
+ const mentionCutoffTime = lastViewedMentionAt && lastViewedAt
82
+ ? (lastViewedMentionAt > lastViewedAt ? lastViewedMentionAt : lastViewedAt)
83
+ : (lastViewedMentionAt || lastViewedAt);
84
+
79
85
  const unreadMentionCount = await prisma.mention.count({
80
86
  where: {
81
87
  userId,
82
88
  message: {
83
89
  conversationId: conversation.id,
84
90
  senderId: { not: userId },
85
- ...(lastViewedMentionAt && {
86
- createdAt: { gt: lastViewedMentionAt }
91
+ ...(mentionCutoffTime && {
92
+ createdAt: { gt: mentionCutoffTime }
87
93
  }),
88
94
  },
89
95
  },
@@ -181,12 +187,13 @@ export const conversationRouter = createTRPCRouter({
181
187
  // Verify all members exist
182
188
  const members = await prisma.user.findMany({
183
189
  where: {
184
- id: {
190
+ username: {
185
191
  in: memberIds,
186
192
  },
187
193
  },
188
194
  select: {
189
195
  id: true,
196
+ username: true,
190
197
  },
191
198
  });
192
199
 
@@ -209,7 +216,7 @@ export const conversationRouter = createTRPCRouter({
209
216
  role: type === 'GROUP' ? 'ADMIN' : 'MEMBER',
210
217
  },
211
218
  ...memberIds.map((memberId) => ({
212
- userId: memberId,
219
+ userId: members.find((member) => member.username === memberId)!.id,
213
220
  role: 'MEMBER' as const,
214
221
  })),
215
222
  ],
@@ -212,7 +212,232 @@ export const messageRouter = createTRPCRouter({
212
212
  mentionedUserIds,
213
213
  };
214
214
  }),
215
+ update: protectedProcedure
216
+ .input(
217
+ z.object({
218
+ messageId: z.string(),
219
+ content: z.string().min(1).max(4000),
220
+ mentionedUserIds: z.array(z.string()).optional(),
221
+ })
222
+ )
223
+ .mutation(async ({ input, ctx }) => {
224
+ const userId = ctx.user!.id;
225
+ const { messageId, content, mentionedUserIds = [] } = input;
226
+
227
+ // Get the existing message and verify user is the sender
228
+ const existingMessage = await prisma.message.findUnique({
229
+ where: { id: messageId },
230
+ include: {
231
+ sender: {
232
+ select: {
233
+ id: true,
234
+ username: true,
235
+ profile: {
236
+ select: {
237
+ displayName: true,
238
+ profilePicture: true,
239
+ },
240
+ },
241
+ },
242
+ },
243
+ },
244
+ });
245
+
246
+ if (!existingMessage) {
247
+ throw new TRPCError({
248
+ code: 'NOT_FOUND',
249
+ message: 'Message not found',
250
+ });
251
+ }
252
+
253
+ if (existingMessage.senderId !== userId) {
254
+ throw new TRPCError({
255
+ code: 'FORBIDDEN',
256
+ message: 'Not the sender of this message',
257
+ });
258
+ }
259
+
260
+ // Verify user is still a member of the conversation
261
+ const membership = await prisma.conversationMember.findFirst({
262
+ where: {
263
+ conversationId: existingMessage.conversationId,
264
+ userId,
265
+ },
266
+ });
267
+
268
+ if (!membership) {
269
+ throw new TRPCError({
270
+ code: 'FORBIDDEN',
271
+ message: 'Not a member of this conversation',
272
+ });
273
+ }
274
+
275
+ // Verify mentioned users are members of the conversation
276
+ if (mentionedUserIds.length > 0) {
277
+ const mentionedMemberships = await prisma.conversationMember.findMany({
278
+ where: {
279
+ conversationId: existingMessage.conversationId,
280
+ userId: { in: mentionedUserIds },
281
+ },
282
+ });
283
+
284
+ if (mentionedMemberships.length !== mentionedUserIds.length) {
285
+ throw new TRPCError({
286
+ code: 'BAD_REQUEST',
287
+ message: 'Some mentioned users are not members of this conversation',
288
+ });
289
+ }
290
+ }
291
+
292
+ // Update message and mentions in transaction
293
+ const updatedMessage = await prisma.$transaction(async (tx) => {
294
+ // Update the message content
295
+ const message = await tx.message.update({
296
+ where: { id: messageId },
297
+ data: { content },
298
+ include: {
299
+ sender: {
300
+ select: {
301
+ id: true,
302
+ username: true,
303
+ profile: {
304
+ select: {
305
+ displayName: true,
306
+ profilePicture: true,
307
+ },
308
+ },
309
+ },
310
+ },
311
+ },
312
+ });
215
313
 
314
+ // Delete existing mentions
315
+ await tx.mention.deleteMany({
316
+ where: { messageId },
317
+ });
318
+
319
+ // Create new mentions if any
320
+ if (mentionedUserIds.length > 0) {
321
+ await tx.mention.createMany({
322
+ data: mentionedUserIds.map((mentionedUserId) => ({
323
+ messageId,
324
+ userId: mentionedUserId,
325
+ })),
326
+ });
327
+ }
328
+
329
+ return message;
330
+ });
331
+
332
+ // Broadcast message update to Pusher
333
+ try {
334
+ await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-updated', {
335
+ id: updatedMessage.id,
336
+ content: updatedMessage.content,
337
+ senderId: updatedMessage.senderId,
338
+ conversationId: updatedMessage.conversationId,
339
+ createdAt: updatedMessage.createdAt,
340
+ sender: updatedMessage.sender,
341
+ mentionedUserIds,
342
+ });
343
+ } catch (error) {
344
+ console.error('Failed to broadcast message update:', error);
345
+ // Don't fail the request if Pusher fails
346
+ }
347
+
348
+ return {
349
+ id: updatedMessage.id,
350
+ content: updatedMessage.content,
351
+ senderId: updatedMessage.senderId,
352
+ conversationId: updatedMessage.conversationId,
353
+ createdAt: updatedMessage.createdAt,
354
+ sender: updatedMessage.sender,
355
+ mentionedUserIds,
356
+ };
357
+ }),
358
+
359
+ delete: protectedProcedure
360
+ .input(
361
+ z.object({
362
+ messageId: z.string(),
363
+ })
364
+ )
365
+ .mutation(async ({ input, ctx }) => {
366
+ const userId = ctx.user!.id;
367
+ const { messageId } = input;
368
+
369
+ // Get the message and verify user is the sender
370
+ const existingMessage = await prisma.message.findUnique({
371
+ where: { id: messageId },
372
+ include: {
373
+ sender: {
374
+ select: {
375
+ id: true,
376
+ username: true,
377
+ },
378
+ },
379
+ },
380
+ });
381
+
382
+ if (!existingMessage) {
383
+ throw new TRPCError({
384
+ code: 'NOT_FOUND',
385
+ message: 'Message not found',
386
+ });
387
+ }
388
+
389
+ if (existingMessage.senderId !== userId) {
390
+ throw new TRPCError({
391
+ code: 'FORBIDDEN',
392
+ message: 'Not the sender of this message',
393
+ });
394
+ }
395
+
396
+ // Verify user is still a member of the conversation
397
+ const membership = await prisma.conversationMember.findFirst({
398
+ where: {
399
+ conversationId: existingMessage.conversationId,
400
+ userId,
401
+ },
402
+ });
403
+
404
+ if (!membership) {
405
+ throw new TRPCError({
406
+ code: 'FORBIDDEN',
407
+ message: 'Not a member of this conversation',
408
+ });
409
+ }
410
+
411
+ // Delete message and all related mentions in transaction
412
+ await prisma.$transaction(async (tx) => {
413
+ // Delete mentions first (due to foreign key constraint)
414
+ await tx.mention.deleteMany({
415
+ where: { messageId },
416
+ });
417
+
418
+ // Delete the message
419
+ await tx.message.delete({
420
+ where: { id: messageId },
421
+ });
422
+ });
423
+
424
+ // Broadcast message deletion to Pusher
425
+ try {
426
+ await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-deleted', {
427
+ messageId,
428
+ conversationId: existingMessage.conversationId,
429
+ senderId: existingMessage.senderId,
430
+ });
431
+ } catch (error) {
432
+ console.error('Failed to broadcast message deletion:', error);
433
+ // Don't fail the request if Pusher fails
434
+ }
435
+
436
+ return {
437
+ success: true,
438
+ messageId,
439
+ };
440
+ }),
216
441
  markAsRead: protectedProcedure
217
442
  .input(
218
443
  z.object({
@@ -344,14 +569,20 @@ export const messageRouter = createTRPCRouter({
344
569
  });
345
570
 
346
571
  // Count unread mentions
572
+ // Use the later of lastViewedAt or lastViewedMentionAt
573
+ // This means if user viewed conversation after mention, mention is considered read
574
+ const mentionCutoffTime = membership.lastViewedMentionAt && membership.lastViewedAt
575
+ ? (membership.lastViewedMentionAt > membership.lastViewedAt ? membership.lastViewedMentionAt : membership.lastViewedAt)
576
+ : (membership.lastViewedMentionAt || membership.lastViewedAt);
577
+
347
578
  const unreadMentionCount = await prisma.mention.count({
348
579
  where: {
349
580
  userId,
350
581
  message: {
351
582
  conversationId,
352
583
  senderId: { not: userId },
353
- ...(membership.lastViewedMentionAt && {
354
- createdAt: { gt: membership.lastViewedMentionAt }
584
+ ...(mentionCutoffTime && {
585
+ createdAt: { gt: mentionCutoffTime }
355
586
  }),
356
587
  },
357
588
  },
@@ -228,14 +228,16 @@ export const userRouter = createTRPCRouter({
228
228
  const uniqueFilename = `${ctx.user.id}-${Date.now()}.${fileExtension}`;
229
229
  const filePath = `users/${ctx.user.id}/profile/${uniqueFilename}`;
230
230
 
231
- // Generate signed URL for direct upload (write permission)
232
- const uploadUrl = await getSignedUrl(filePath, 'write', input.fileType);
231
+ // Generate backend proxy upload URL instead of direct GCS signed URL
232
+ const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
233
+ const uploadUrl = `${backendUrl}/api/upload/${encodeURIComponent(filePath)}`;
233
234
 
234
235
  logger.info('Generated upload URL', {
235
236
  userId: ctx.user.id,
236
237
  filePath,
237
238
  fileName: uniqueFilename,
238
- fileType: input.fileType
239
+ fileType: input.fileType,
240
+ uploadUrl
239
241
  });
240
242
 
241
243
  return {