@studious-lms/server 1.1.9 → 1.1.11
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 +68 -0
- package/dist/routers/_app.d.ts +138 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +10 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +21 -0
- package/dist/routers/attendance.d.ts +15 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +32 -0
- package/dist/routers/class.d.ts +10 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +14 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +10 -4
- package/dist/routers/message.d.ts +34 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +208 -2
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +5 -4
- package/package.json +1 -1
- package/src/index.ts +79 -0
- package/src/routers/assignment.ts +21 -0
- package/src/routers/attendance.ts +32 -0
- package/src/routers/class.ts +14 -0
- package/src/routers/conversation.ts +11 -4
- package/src/routers/message.ts +233 -2
- package/src/routers/user.ts +5 -3
package/src/routers/message.ts
CHANGED
|
@@ -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
|
-
...(
|
|
354
|
-
createdAt: { gt:
|
|
584
|
+
...(mentionCutoffTime && {
|
|
585
|
+
createdAt: { gt: mentionCutoffTime }
|
|
355
586
|
}),
|
|
356
587
|
},
|
|
357
588
|
},
|
package/src/routers/user.ts
CHANGED
|
@@ -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
|
|
232
|
-
const
|
|
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 {
|