@goscribe/server 1.3.0 → 1.3.1
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/context.d.ts +5 -1
- package/dist/lib/activity_human_description.d.ts +13 -0
- package/dist/lib/activity_human_description.js +221 -0
- package/dist/lib/activity_human_description.test.d.ts +1 -0
- package/dist/lib/activity_human_description.test.js +16 -0
- package/dist/lib/activity_log_service.d.ts +87 -0
- package/dist/lib/activity_log_service.js +276 -0
- package/dist/lib/activity_log_service.test.d.ts +1 -0
- package/dist/lib/activity_log_service.test.js +27 -0
- package/dist/lib/ai-session.d.ts +15 -2
- package/dist/lib/ai-session.js +147 -85
- package/dist/lib/constants.d.ts +13 -0
- package/dist/lib/constants.js +12 -0
- package/dist/lib/email.d.ts +11 -0
- package/dist/lib/email.js +193 -0
- package/dist/lib/env.d.ts +13 -0
- package/dist/lib/env.js +16 -0
- package/dist/lib/inference.d.ts +4 -1
- package/dist/lib/inference.js +3 -3
- package/dist/lib/logger.d.ts +4 -4
- package/dist/lib/logger.js +30 -8
- package/dist/lib/notification-service.d.ts +152 -0
- package/dist/lib/notification-service.js +473 -0
- package/dist/lib/notification-service.test.d.ts +1 -0
- package/dist/lib/notification-service.test.js +87 -0
- package/dist/lib/prisma.d.ts +2 -1
- package/dist/lib/prisma.js +5 -1
- package/dist/lib/pusher.d.ts +23 -0
- package/dist/lib/pusher.js +69 -5
- package/dist/lib/retry.d.ts +15 -0
- package/dist/lib/retry.js +37 -0
- package/dist/lib/storage.js +2 -2
- package/dist/lib/stripe.d.ts +9 -0
- package/dist/lib/stripe.js +36 -0
- package/dist/lib/subscription_service.d.ts +37 -0
- package/dist/lib/subscription_service.js +654 -0
- package/dist/lib/usage_service.d.ts +26 -0
- package/dist/lib/usage_service.js +59 -0
- package/dist/lib/worksheet-generation.d.ts +91 -0
- package/dist/lib/worksheet-generation.js +95 -0
- package/dist/lib/worksheet-generation.test.d.ts +1 -0
- package/dist/lib/worksheet-generation.test.js +20 -0
- package/dist/lib/workspace-access.d.ts +18 -0
- package/dist/lib/workspace-access.js +13 -0
- package/dist/routers/_app.d.ts +1349 -253
- package/dist/routers/_app.js +10 -0
- package/dist/routers/admin.d.ts +361 -0
- package/dist/routers/admin.js +633 -0
- package/dist/routers/annotations.d.ts +219 -0
- package/dist/routers/annotations.js +187 -0
- package/dist/routers/auth.d.ts +88 -7
- package/dist/routers/auth.js +339 -19
- package/dist/routers/chat.d.ts +6 -12
- package/dist/routers/copilot.d.ts +199 -0
- package/dist/routers/copilot.js +571 -0
- package/dist/routers/flashcards.d.ts +47 -81
- package/dist/routers/flashcards.js +143 -27
- package/dist/routers/members.d.ts +36 -7
- package/dist/routers/members.js +200 -19
- package/dist/routers/notifications.d.ts +99 -0
- package/dist/routers/notifications.js +127 -0
- package/dist/routers/payment.d.ts +89 -0
- package/dist/routers/payment.js +403 -0
- package/dist/routers/podcast.d.ts +8 -13
- package/dist/routers/podcast.js +54 -31
- package/dist/routers/studyguide.d.ts +1 -29
- package/dist/routers/studyguide.js +80 -71
- package/dist/routers/worksheets.d.ts +105 -38
- package/dist/routers/worksheets.js +258 -68
- package/dist/routers/workspace.d.ts +139 -60
- package/dist/routers/workspace.js +455 -315
- package/dist/scripts/purge-deleted-users.d.ts +1 -0
- package/dist/scripts/purge-deleted-users.js +149 -0
- package/dist/server.js +130 -10
- package/dist/services/flashcard-progress.service.d.ts +18 -66
- package/dist/services/flashcard-progress.service.js +51 -42
- package/dist/trpc.d.ts +20 -21
- package/dist/trpc.js +150 -1
- package/package.json +1 -1
package/dist/routers/members.js
CHANGED
|
@@ -2,6 +2,9 @@ import { z } from 'zod';
|
|
|
2
2
|
import { TRPCError } from '@trpc/server';
|
|
3
3
|
import { router, publicProcedure, authedProcedure } from '../trpc.js';
|
|
4
4
|
import { logger } from '../lib/logger.js';
|
|
5
|
+
import { sendInvitationEmail } from '../lib/email.js';
|
|
6
|
+
import PusherService from '../lib/pusher.js';
|
|
7
|
+
import { notifyInviteAccepted, notifyInviteRecipient, notifyWorkspaceMembershipRemoved, notifyWorkspaceRoleChanged, } from '../lib/notification-service.js';
|
|
5
8
|
/**
|
|
6
9
|
* Members router for workspace member management
|
|
7
10
|
*
|
|
@@ -72,8 +75,7 @@ export const members = router({
|
|
|
72
75
|
message: 'Workspace not found or access denied'
|
|
73
76
|
});
|
|
74
77
|
}
|
|
75
|
-
|
|
76
|
-
const members = [
|
|
78
|
+
const workspaceMembers = [
|
|
77
79
|
{
|
|
78
80
|
id: workspace.owner.id,
|
|
79
81
|
name: workspace.owner.name || 'Unknown',
|
|
@@ -89,7 +91,10 @@ export const members = router({
|
|
|
89
91
|
joinedAt: membership.joinedAt,
|
|
90
92
|
}))
|
|
91
93
|
];
|
|
92
|
-
|
|
94
|
+
logger.info(`👥 Fetched ${workspaceMembers.length} members for workspace ${input.workspaceId}`, 'WORKSPACE', {
|
|
95
|
+
memberEmails: workspaceMembers.map(m => m.email)
|
|
96
|
+
});
|
|
97
|
+
return workspaceMembers;
|
|
93
98
|
}),
|
|
94
99
|
/**
|
|
95
100
|
* Get current user's role in a workspace
|
|
@@ -202,6 +207,21 @@ export const members = router({
|
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
});
|
|
210
|
+
const invitedExistingUser = await ctx.db.user.findUnique({
|
|
211
|
+
where: { email: input.email },
|
|
212
|
+
select: { id: true, name: true },
|
|
213
|
+
});
|
|
214
|
+
if (invitedExistingUser) {
|
|
215
|
+
await notifyInviteRecipient(ctx.db, {
|
|
216
|
+
invitedUserId: invitedExistingUser.id,
|
|
217
|
+
inviterUserId: ctx.session.user.id,
|
|
218
|
+
workspaceId: input.workspaceId,
|
|
219
|
+
workspaceTitle: invitation.workspace.title,
|
|
220
|
+
invitationId: invitation.id,
|
|
221
|
+
invitationToken: invitation.token,
|
|
222
|
+
inviterName: invitation.workspace.owner.name || invitation.workspace.owner.email,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
205
225
|
logger.info(`🎫 Invitation created for ${input.email} to workspace ${input.workspaceId} with role ${input.role}`, 'WORKSPACE', {
|
|
206
226
|
invitationId: invitation.id,
|
|
207
227
|
workspaceId: input.workspaceId,
|
|
@@ -209,8 +229,14 @@ export const members = router({
|
|
|
209
229
|
role: input.role,
|
|
210
230
|
invitedBy: ctx.session.user.id
|
|
211
231
|
});
|
|
212
|
-
//
|
|
213
|
-
|
|
232
|
+
// Send email notification
|
|
233
|
+
await sendInvitationEmail({
|
|
234
|
+
email: invitation.email,
|
|
235
|
+
token: invitation.token,
|
|
236
|
+
role: invitation.role,
|
|
237
|
+
workspaceTitle: invitation.workspace.title,
|
|
238
|
+
invitedByName: invitation.workspace.owner.name || invitation.workspace.owner.email || 'Someone',
|
|
239
|
+
});
|
|
214
240
|
return {
|
|
215
241
|
invitationId: invitation.id,
|
|
216
242
|
token: invitation.token,
|
|
@@ -251,6 +277,7 @@ export const members = router({
|
|
|
251
277
|
select: {
|
|
252
278
|
id: true,
|
|
253
279
|
title: true,
|
|
280
|
+
ownerId: true,
|
|
254
281
|
owner: {
|
|
255
282
|
select: {
|
|
256
283
|
name: true,
|
|
@@ -277,8 +304,10 @@ export const members = router({
|
|
|
277
304
|
const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
|
|
278
305
|
if (!user || !user.email)
|
|
279
306
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
280
|
-
|
|
281
|
-
if
|
|
307
|
+
logger.info(`🔍 Verification check for ${user.email} accepting invite for ${invitation.email}`, 'WORKSPACE');
|
|
308
|
+
// Check if the email matches the user's email (case-insensitive)
|
|
309
|
+
if (user.email.toLowerCase() !== invitation.email.toLowerCase()) {
|
|
310
|
+
logger.warn(`❌ Invitation email mismatch: user ${user.email} vs invite ${invitation.email}`, 'WORKSPACE');
|
|
282
311
|
throw new TRPCError({
|
|
283
312
|
code: 'BAD_REQUEST',
|
|
284
313
|
message: 'This invitation was sent to a different email address'
|
|
@@ -295,40 +324,81 @@ export const members = router({
|
|
|
295
324
|
}
|
|
296
325
|
});
|
|
297
326
|
if (isAlreadyMember) {
|
|
327
|
+
logger.info(`ℹ️ User ${ctx.session.user.id} is already a member of workspace ${invitation.workspaceId}. Marking invite as accepted.`, 'WORKSPACE');
|
|
298
328
|
// Mark invitation as accepted even if already a member
|
|
299
329
|
await ctx.db.workspaceInvitation.update({
|
|
300
330
|
where: { id: invitation.id },
|
|
301
331
|
data: { acceptedAt: new Date() }
|
|
302
332
|
});
|
|
303
|
-
|
|
304
|
-
|
|
333
|
+
return {
|
|
334
|
+
workspaceId: invitation.workspaceId,
|
|
335
|
+
workspaceTitle: invitation.workspace.title,
|
|
336
|
+
role: invitation.role,
|
|
337
|
+
ownerName: invitation.workspace.owner.name || invitation.workspace.owner.email,
|
|
305
338
|
message: 'You are already a member of this workspace'
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// Add user to workspace with proper role.
|
|
342
|
+
// This can race if accept is triggered twice (e.g. redirects/retries),
|
|
343
|
+
// so treat duplicate membership as a successful, idempotent accept.
|
|
344
|
+
let memberAdded = false;
|
|
345
|
+
try {
|
|
346
|
+
await ctx.db.workspaceMember.create({
|
|
347
|
+
data: {
|
|
348
|
+
workspaceId: invitation.workspaceId,
|
|
349
|
+
userId: ctx.session.user.id,
|
|
350
|
+
role: invitation.role,
|
|
351
|
+
}
|
|
306
352
|
});
|
|
353
|
+
memberAdded = true;
|
|
307
354
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
workspaceId: invitation.workspaceId,
|
|
312
|
-
userId: ctx.session.user.id,
|
|
313
|
-
role: invitation.role,
|
|
355
|
+
catch (error) {
|
|
356
|
+
if (error?.code !== 'P2002') {
|
|
357
|
+
throw error;
|
|
314
358
|
}
|
|
315
|
-
|
|
359
|
+
logger.info(`ℹ️ Duplicate invite accept handled for user ${ctx.session.user.id} in workspace ${invitation.workspaceId}`, 'WORKSPACE');
|
|
360
|
+
}
|
|
316
361
|
// Mark invitation as accepted
|
|
317
362
|
await ctx.db.workspaceInvitation.update({
|
|
318
363
|
where: { id: invitation.id },
|
|
319
364
|
data: { acceptedAt: new Date() }
|
|
320
365
|
});
|
|
321
|
-
|
|
366
|
+
if (memberAdded) {
|
|
367
|
+
await notifyInviteAccepted(ctx.db, {
|
|
368
|
+
recipientUserIds: [invitation.invitedById, invitation.workspace.ownerId],
|
|
369
|
+
actorUserId: ctx.session.user.id,
|
|
370
|
+
workspaceId: invitation.workspaceId,
|
|
371
|
+
workspaceTitle: invitation.workspace.title,
|
|
372
|
+
memberName: user.name || user.email,
|
|
373
|
+
invitationId: invitation.id,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
logger.info(`✅ Invitation accepted by ${ctx.session.user.id} (${user.email}) for workspace ${invitation.workspaceId}`, 'WORKSPACE', {
|
|
322
377
|
invitationId: invitation.id,
|
|
323
378
|
workspaceId: invitation.workspaceId,
|
|
324
379
|
userId: ctx.session.user.id,
|
|
325
380
|
email: invitation.email
|
|
326
381
|
});
|
|
382
|
+
// Try to emit a Pusher event if possible
|
|
383
|
+
try {
|
|
384
|
+
if (memberAdded) {
|
|
385
|
+
await PusherService.emitMemberJoined(invitation.workspaceId, {
|
|
386
|
+
id: ctx.session.user.id,
|
|
387
|
+
name: user.name || 'Member',
|
|
388
|
+
email: user.email,
|
|
389
|
+
role: invitation.role,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
logger.error('Failed to emit member joined event', e);
|
|
395
|
+
}
|
|
327
396
|
return {
|
|
328
397
|
workspaceId: invitation.workspaceId,
|
|
329
398
|
workspaceTitle: invitation.workspace.title,
|
|
330
399
|
role: invitation.role,
|
|
331
400
|
ownerName: invitation.workspace.owner.name || invitation.workspace.owner.email,
|
|
401
|
+
...(memberAdded ? {} : { message: 'You are already a member of this workspace' }),
|
|
332
402
|
};
|
|
333
403
|
}),
|
|
334
404
|
/**
|
|
@@ -346,7 +416,8 @@ export const members = router({
|
|
|
346
416
|
where: {
|
|
347
417
|
id: input.workspaceId,
|
|
348
418
|
ownerId: ctx.session.user.id
|
|
349
|
-
}
|
|
419
|
+
},
|
|
420
|
+
select: { id: true, title: true, ownerId: true },
|
|
350
421
|
});
|
|
351
422
|
if (!workspace) {
|
|
352
423
|
throw new TRPCError({
|
|
@@ -394,6 +465,19 @@ export const members = router({
|
|
|
394
465
|
newRole: input.role,
|
|
395
466
|
changedBy: ctx.session.user.id
|
|
396
467
|
});
|
|
468
|
+
const actor = await ctx.db.user.findUnique({
|
|
469
|
+
where: { id: ctx.session.user.id },
|
|
470
|
+
select: { name: true, email: true },
|
|
471
|
+
});
|
|
472
|
+
await notifyWorkspaceRoleChanged(ctx.db, {
|
|
473
|
+
memberUserId: input.memberId,
|
|
474
|
+
workspaceId: input.workspaceId,
|
|
475
|
+
workspaceTitle: workspace.title,
|
|
476
|
+
newRole: input.role,
|
|
477
|
+
oldRole: member.role,
|
|
478
|
+
actorUserId: ctx.session.user.id,
|
|
479
|
+
actorName: actor?.name || actor?.email || 'A workspace admin',
|
|
480
|
+
}).catch(() => { });
|
|
397
481
|
return {
|
|
398
482
|
memberId: input.memberId,
|
|
399
483
|
role: input.role,
|
|
@@ -415,7 +499,8 @@ export const members = router({
|
|
|
415
499
|
where: {
|
|
416
500
|
id: input.workspaceId,
|
|
417
501
|
ownerId: ctx.session.user.id
|
|
418
|
-
}
|
|
502
|
+
},
|
|
503
|
+
select: { id: true, title: true, ownerId: true },
|
|
419
504
|
});
|
|
420
505
|
if (!workspace) {
|
|
421
506
|
throw new TRPCError({
|
|
@@ -451,6 +536,17 @@ export const members = router({
|
|
|
451
536
|
message: 'Member not found in this workspace'
|
|
452
537
|
});
|
|
453
538
|
}
|
|
539
|
+
const actor = await ctx.db.user.findUnique({
|
|
540
|
+
where: { id: ctx.session.user.id },
|
|
541
|
+
select: { name: true, email: true },
|
|
542
|
+
});
|
|
543
|
+
await notifyWorkspaceMembershipRemoved(ctx.db, {
|
|
544
|
+
memberUserId: input.memberId,
|
|
545
|
+
workspaceId: input.workspaceId,
|
|
546
|
+
workspaceTitle: workspace.title,
|
|
547
|
+
actorUserId: ctx.session.user.id,
|
|
548
|
+
actorName: actor?.name || actor?.email || 'A workspace admin',
|
|
549
|
+
}).catch(() => { });
|
|
454
550
|
// Remove member from workspace
|
|
455
551
|
await ctx.db.workspaceMember.delete({
|
|
456
552
|
where: { id: member.id }
|
|
@@ -551,4 +647,89 @@ export const members = router({
|
|
|
551
647
|
message: 'Invitation cancelled successfully'
|
|
552
648
|
};
|
|
553
649
|
}),
|
|
650
|
+
/**
|
|
651
|
+
* Resend a pending invitation (owner only)
|
|
652
|
+
*/
|
|
653
|
+
resendInvitation: authedProcedure
|
|
654
|
+
.input(z.object({
|
|
655
|
+
invitationId: z.string(),
|
|
656
|
+
}))
|
|
657
|
+
.mutation(async ({ ctx, input }) => {
|
|
658
|
+
// Check if user is owner of the workspace and invitation is pending
|
|
659
|
+
const invitation = await ctx.db.workspaceInvitation.findFirst({
|
|
660
|
+
where: {
|
|
661
|
+
id: input.invitationId,
|
|
662
|
+
acceptedAt: null,
|
|
663
|
+
workspace: {
|
|
664
|
+
ownerId: ctx.session.user.id
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
include: {
|
|
668
|
+
workspace: {
|
|
669
|
+
select: {
|
|
670
|
+
title: true,
|
|
671
|
+
owner: {
|
|
672
|
+
select: {
|
|
673
|
+
name: true,
|
|
674
|
+
email: true,
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
if (!invitation) {
|
|
682
|
+
throw new TRPCError({
|
|
683
|
+
code: 'NOT_FOUND',
|
|
684
|
+
message: 'Invitation not found or insufficient permissions'
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
// Check if expired and update expiry if needed
|
|
688
|
+
if (invitation.expiresAt < new Date()) {
|
|
689
|
+
const newExpiry = new Date();
|
|
690
|
+
newExpiry.setDate(newExpiry.getDate() + 7);
|
|
691
|
+
await ctx.db.workspaceInvitation.update({
|
|
692
|
+
where: { id: invitation.id },
|
|
693
|
+
data: { expiresAt: newExpiry }
|
|
694
|
+
});
|
|
695
|
+
invitation.expiresAt = newExpiry;
|
|
696
|
+
}
|
|
697
|
+
// Send email notification
|
|
698
|
+
await sendInvitationEmail({
|
|
699
|
+
email: invitation.email,
|
|
700
|
+
token: invitation.token,
|
|
701
|
+
role: invitation.role,
|
|
702
|
+
workspaceTitle: invitation.workspace.title,
|
|
703
|
+
invitedByName: invitation.workspace.owner.name || invitation.workspace.owner.email || 'Someone',
|
|
704
|
+
});
|
|
705
|
+
logger.info(`📧 Invitation resent to ${invitation.email} for workspace ${invitation.workspaceId}`, 'WORKSPACE', {
|
|
706
|
+
invitationId: invitation.id,
|
|
707
|
+
workspaceId: invitation.workspaceId,
|
|
708
|
+
email: invitation.email,
|
|
709
|
+
resentBy: ctx.session.user.id
|
|
710
|
+
});
|
|
711
|
+
return {
|
|
712
|
+
invitationId: invitation.id,
|
|
713
|
+
message: 'Invitation email resent successfully'
|
|
714
|
+
};
|
|
715
|
+
}),
|
|
716
|
+
/**
|
|
717
|
+
* DEBUG ONLY: Get all invitations for a workspace
|
|
718
|
+
*/
|
|
719
|
+
getAllInvitationsDebug: authedProcedure
|
|
720
|
+
.input(z.object({
|
|
721
|
+
workspaceId: z.string(),
|
|
722
|
+
}))
|
|
723
|
+
.query(async ({ ctx, input }) => {
|
|
724
|
+
// Check if user is owner
|
|
725
|
+
const workspace = await ctx.db.workspace.findFirst({
|
|
726
|
+
where: { id: input.workspaceId, ownerId: ctx.session.user.id }
|
|
727
|
+
});
|
|
728
|
+
if (!workspace)
|
|
729
|
+
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
730
|
+
return ctx.db.workspaceInvitation.findMany({
|
|
731
|
+
where: { workspaceId: input.workspaceId },
|
|
732
|
+
orderBy: { createdAt: 'desc' }
|
|
733
|
+
});
|
|
734
|
+
}),
|
|
554
735
|
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export declare const notifications: import("@trpc/server").TRPCBuiltRouter<{
|
|
2
|
+
ctx: import("../context.js").Context;
|
|
3
|
+
meta: object;
|
|
4
|
+
errorShape: {
|
|
5
|
+
data: {
|
|
6
|
+
zodError: string | null;
|
|
7
|
+
code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
|
|
8
|
+
httpStatus: number;
|
|
9
|
+
path?: string;
|
|
10
|
+
stack?: string;
|
|
11
|
+
};
|
|
12
|
+
message: string;
|
|
13
|
+
code: import("@trpc/server").TRPC_ERROR_CODE_NUMBER;
|
|
14
|
+
};
|
|
15
|
+
transformer: true;
|
|
16
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
17
|
+
list: import("@trpc/server").TRPCQueryProcedure<{
|
|
18
|
+
input: {
|
|
19
|
+
cursor?: string | undefined;
|
|
20
|
+
limit?: number | undefined;
|
|
21
|
+
unreadOnly?: boolean | undefined;
|
|
22
|
+
types?: string[] | undefined;
|
|
23
|
+
};
|
|
24
|
+
output: {
|
|
25
|
+
items: ({
|
|
26
|
+
workspace: {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
} | null;
|
|
30
|
+
actor: {
|
|
31
|
+
name: string | null;
|
|
32
|
+
id: string;
|
|
33
|
+
email: string | null;
|
|
34
|
+
} | null;
|
|
35
|
+
} & {
|
|
36
|
+
id: string;
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
updatedAt: Date;
|
|
39
|
+
workspaceId: string | null;
|
|
40
|
+
type: string;
|
|
41
|
+
title: string;
|
|
42
|
+
content: string | null;
|
|
43
|
+
userId: string;
|
|
44
|
+
actorUserId: string | null;
|
|
45
|
+
body: string;
|
|
46
|
+
actionUrl: string | null;
|
|
47
|
+
metadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
48
|
+
priority: import("@prisma/client").$Enums.NotificationPriority;
|
|
49
|
+
sourceId: string | null;
|
|
50
|
+
read: boolean;
|
|
51
|
+
readAt: Date | null;
|
|
52
|
+
deliveredAt: Date | null;
|
|
53
|
+
})[];
|
|
54
|
+
nextCursor: string | undefined;
|
|
55
|
+
};
|
|
56
|
+
meta: object;
|
|
57
|
+
}>;
|
|
58
|
+
unreadCount: import("@trpc/server").TRPCQueryProcedure<{
|
|
59
|
+
input: void;
|
|
60
|
+
output: {
|
|
61
|
+
count: number;
|
|
62
|
+
};
|
|
63
|
+
meta: object;
|
|
64
|
+
}>;
|
|
65
|
+
markRead: import("@trpc/server").TRPCMutationProcedure<{
|
|
66
|
+
input: {
|
|
67
|
+
id: string;
|
|
68
|
+
};
|
|
69
|
+
output: {
|
|
70
|
+
success: boolean;
|
|
71
|
+
};
|
|
72
|
+
meta: object;
|
|
73
|
+
}>;
|
|
74
|
+
markManyRead: import("@trpc/server").TRPCMutationProcedure<{
|
|
75
|
+
input: {
|
|
76
|
+
ids: string[];
|
|
77
|
+
};
|
|
78
|
+
output: {
|
|
79
|
+
success: boolean;
|
|
80
|
+
};
|
|
81
|
+
meta: object;
|
|
82
|
+
}>;
|
|
83
|
+
markAllRead: import("@trpc/server").TRPCMutationProcedure<{
|
|
84
|
+
input: void;
|
|
85
|
+
output: {
|
|
86
|
+
success: boolean;
|
|
87
|
+
};
|
|
88
|
+
meta: object;
|
|
89
|
+
}>;
|
|
90
|
+
delete: import("@trpc/server").TRPCMutationProcedure<{
|
|
91
|
+
input: {
|
|
92
|
+
id: string;
|
|
93
|
+
};
|
|
94
|
+
output: {
|
|
95
|
+
success: boolean;
|
|
96
|
+
};
|
|
97
|
+
meta: object;
|
|
98
|
+
}>;
|
|
99
|
+
}>>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { authedProcedure, router } from '../trpc.js';
|
|
3
|
+
import PusherService from '../lib/pusher.js';
|
|
4
|
+
const listInputSchema = z.object({
|
|
5
|
+
cursor: z.string().optional(),
|
|
6
|
+
limit: z.number().min(1).max(50).default(20),
|
|
7
|
+
unreadOnly: z.boolean().optional(),
|
|
8
|
+
types: z.array(z.string()).optional(),
|
|
9
|
+
});
|
|
10
|
+
export const notifications = router({
|
|
11
|
+
list: authedProcedure
|
|
12
|
+
.input(listInputSchema)
|
|
13
|
+
.query(async ({ ctx, input }) => {
|
|
14
|
+
const { cursor, limit, unreadOnly, types } = input;
|
|
15
|
+
const userId = ctx.userId;
|
|
16
|
+
const items = await ctx.db.notification.findMany({
|
|
17
|
+
where: {
|
|
18
|
+
userId,
|
|
19
|
+
...(unreadOnly ? { read: false } : {}),
|
|
20
|
+
...(types?.length ? { type: { in: types } } : {}),
|
|
21
|
+
},
|
|
22
|
+
include: {
|
|
23
|
+
actor: {
|
|
24
|
+
select: {
|
|
25
|
+
id: true,
|
|
26
|
+
name: true,
|
|
27
|
+
email: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
workspace: {
|
|
31
|
+
select: {
|
|
32
|
+
id: true,
|
|
33
|
+
title: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
|
|
38
|
+
take: limit + 1,
|
|
39
|
+
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
|
|
40
|
+
});
|
|
41
|
+
let nextCursor;
|
|
42
|
+
if (items.length > limit) {
|
|
43
|
+
const next = items.pop();
|
|
44
|
+
nextCursor = next?.id;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
items,
|
|
48
|
+
nextCursor,
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
unreadCount: authedProcedure.query(async ({ ctx }) => {
|
|
52
|
+
const count = await ctx.db.notification.count({
|
|
53
|
+
where: {
|
|
54
|
+
userId: ctx.userId,
|
|
55
|
+
read: false,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return { count };
|
|
59
|
+
}),
|
|
60
|
+
markRead: authedProcedure
|
|
61
|
+
.input(z.object({ id: z.string() }))
|
|
62
|
+
.mutation(async ({ ctx, input }) => {
|
|
63
|
+
const now = new Date();
|
|
64
|
+
await ctx.db.notification.updateMany({
|
|
65
|
+
where: {
|
|
66
|
+
id: input.id,
|
|
67
|
+
userId: ctx.userId,
|
|
68
|
+
},
|
|
69
|
+
data: {
|
|
70
|
+
read: true,
|
|
71
|
+
readAt: now,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const unreadCount = await ctx.db.notification.count({
|
|
75
|
+
where: { userId: ctx.userId, read: false },
|
|
76
|
+
});
|
|
77
|
+
await PusherService.emitNotificationReadState(ctx.userId, { unreadCount });
|
|
78
|
+
return { success: true };
|
|
79
|
+
}),
|
|
80
|
+
markManyRead: authedProcedure
|
|
81
|
+
.input(z.object({ ids: z.array(z.string()).min(1) }))
|
|
82
|
+
.mutation(async ({ ctx, input }) => {
|
|
83
|
+
const now = new Date();
|
|
84
|
+
await ctx.db.notification.updateMany({
|
|
85
|
+
where: {
|
|
86
|
+
id: { in: input.ids },
|
|
87
|
+
userId: ctx.userId,
|
|
88
|
+
},
|
|
89
|
+
data: {
|
|
90
|
+
read: true,
|
|
91
|
+
readAt: now,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const unreadCount = await ctx.db.notification.count({
|
|
95
|
+
where: { userId: ctx.userId, read: false },
|
|
96
|
+
});
|
|
97
|
+
await PusherService.emitNotificationReadState(ctx.userId, { unreadCount });
|
|
98
|
+
return { success: true };
|
|
99
|
+
}),
|
|
100
|
+
markAllRead: authedProcedure
|
|
101
|
+
.mutation(async ({ ctx }) => {
|
|
102
|
+
const now = new Date();
|
|
103
|
+
await ctx.db.notification.updateMany({
|
|
104
|
+
where: {
|
|
105
|
+
userId: ctx.userId,
|
|
106
|
+
read: false,
|
|
107
|
+
},
|
|
108
|
+
data: {
|
|
109
|
+
read: true,
|
|
110
|
+
readAt: now,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
await PusherService.emitNotificationReadState(ctx.userId, { unreadCount: 0 });
|
|
114
|
+
return { success: true };
|
|
115
|
+
}),
|
|
116
|
+
delete: authedProcedure
|
|
117
|
+
.input(z.object({ id: z.string() }))
|
|
118
|
+
.mutation(async ({ ctx, input }) => {
|
|
119
|
+
await ctx.db.notification.deleteMany({
|
|
120
|
+
where: {
|
|
121
|
+
id: input.id,
|
|
122
|
+
userId: ctx.userId,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
return { success: true };
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export declare const paymentRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
2
|
+
ctx: import("../context.js").Context;
|
|
3
|
+
meta: object;
|
|
4
|
+
errorShape: {
|
|
5
|
+
data: {
|
|
6
|
+
zodError: string | null;
|
|
7
|
+
code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
|
|
8
|
+
httpStatus: number;
|
|
9
|
+
path?: string;
|
|
10
|
+
stack?: string;
|
|
11
|
+
};
|
|
12
|
+
message: string;
|
|
13
|
+
code: import("@trpc/server").TRPC_ERROR_CODE_NUMBER;
|
|
14
|
+
};
|
|
15
|
+
transformer: true;
|
|
16
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
17
|
+
getPlans: import("@trpc/server").TRPCQueryProcedure<{
|
|
18
|
+
input: void;
|
|
19
|
+
output: any[];
|
|
20
|
+
meta: object;
|
|
21
|
+
}>;
|
|
22
|
+
createCheckoutSession: import("@trpc/server").TRPCMutationProcedure<{
|
|
23
|
+
input: {
|
|
24
|
+
planId: string;
|
|
25
|
+
};
|
|
26
|
+
output: {
|
|
27
|
+
url: any;
|
|
28
|
+
};
|
|
29
|
+
meta: object;
|
|
30
|
+
}>;
|
|
31
|
+
confirmCheckoutSuccess: import("@trpc/server").TRPCMutationProcedure<{
|
|
32
|
+
input: {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
};
|
|
35
|
+
output: {
|
|
36
|
+
confirmed: boolean;
|
|
37
|
+
reason: string;
|
|
38
|
+
kind?: undefined;
|
|
39
|
+
} | {
|
|
40
|
+
confirmed: boolean;
|
|
41
|
+
kind: "payment";
|
|
42
|
+
reason?: undefined;
|
|
43
|
+
} | {
|
|
44
|
+
confirmed: boolean;
|
|
45
|
+
kind: "subscription";
|
|
46
|
+
reason?: undefined;
|
|
47
|
+
};
|
|
48
|
+
meta: object;
|
|
49
|
+
}>;
|
|
50
|
+
createResourcePurchaseSession: import("@trpc/server").TRPCMutationProcedure<{
|
|
51
|
+
input: {
|
|
52
|
+
resourceType: "STUDY_GUIDE" | "FLASHCARD_SET" | "WORKSHEET" | "MEETING_SUMMARY" | "PODCAST_EPISODE" | "STORAGE";
|
|
53
|
+
quantity?: number | undefined;
|
|
54
|
+
};
|
|
55
|
+
output: {
|
|
56
|
+
url: any;
|
|
57
|
+
};
|
|
58
|
+
meta: object;
|
|
59
|
+
}>;
|
|
60
|
+
getUsageOverview: import("@trpc/server").TRPCQueryProcedure<{
|
|
61
|
+
input: void;
|
|
62
|
+
output: {
|
|
63
|
+
usage: import("../lib/usage_service.js").UserUsage;
|
|
64
|
+
limits: {
|
|
65
|
+
id: string;
|
|
66
|
+
planId: string;
|
|
67
|
+
maxStorageBytes: bigint;
|
|
68
|
+
maxWorksheets: number;
|
|
69
|
+
maxFlashcards: number;
|
|
70
|
+
maxPodcasts: number;
|
|
71
|
+
maxStudyGuides: number;
|
|
72
|
+
createdAt: Date;
|
|
73
|
+
updatedAt: Date;
|
|
74
|
+
} | null;
|
|
75
|
+
hasActivePlan: boolean;
|
|
76
|
+
};
|
|
77
|
+
meta: object;
|
|
78
|
+
}>;
|
|
79
|
+
getResourcePrices: import("@trpc/server").TRPCQueryProcedure<{
|
|
80
|
+
input: void;
|
|
81
|
+
output: {
|
|
82
|
+
id: string;
|
|
83
|
+
updatedAt: Date;
|
|
84
|
+
resourceType: import("@prisma/client").$Enums.ArtifactType;
|
|
85
|
+
priceCents: number;
|
|
86
|
+
}[];
|
|
87
|
+
meta: object;
|
|
88
|
+
}>;
|
|
89
|
+
}>>;
|