@goscribe/server 1.1.7 → 1.3.0

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.
Files changed (56) hide show
  1. package/.env.example +43 -0
  2. package/check-difficulty.cjs +14 -0
  3. package/check-questions.cjs +14 -0
  4. package/db-summary.cjs +22 -0
  5. package/dist/routers/auth.js +1 -1
  6. package/mcq-test.cjs +36 -0
  7. package/package.json +10 -2
  8. package/prisma/migrations/20260413143206_init/migration.sql +873 -0
  9. package/prisma/schema.prisma +485 -292
  10. package/src/context.ts +4 -1
  11. package/src/lib/activity_human_description.test.ts +28 -0
  12. package/src/lib/activity_human_description.ts +239 -0
  13. package/src/lib/activity_log_service.test.ts +37 -0
  14. package/src/lib/activity_log_service.ts +353 -0
  15. package/src/lib/ai-session.ts +194 -112
  16. package/src/lib/constants.ts +14 -0
  17. package/src/lib/email.ts +230 -0
  18. package/src/lib/env.ts +23 -6
  19. package/src/lib/inference.ts +3 -3
  20. package/src/lib/logger.ts +26 -9
  21. package/src/lib/notification-service.test.ts +106 -0
  22. package/src/lib/notification-service.ts +677 -0
  23. package/src/lib/prisma.ts +6 -1
  24. package/src/lib/pusher.ts +90 -6
  25. package/src/lib/retry.ts +61 -0
  26. package/src/lib/storage.ts +2 -2
  27. package/src/lib/stripe.ts +39 -0
  28. package/src/lib/subscription_service.ts +722 -0
  29. package/src/lib/usage_service.ts +74 -0
  30. package/src/lib/worksheet-generation.test.ts +31 -0
  31. package/src/lib/worksheet-generation.ts +139 -0
  32. package/src/lib/workspace-access.ts +13 -0
  33. package/src/routers/_app.ts +11 -0
  34. package/src/routers/admin.ts +710 -0
  35. package/src/routers/annotations.ts +227 -0
  36. package/src/routers/auth.ts +432 -33
  37. package/src/routers/copilot.ts +719 -0
  38. package/src/routers/flashcards.ts +207 -80
  39. package/src/routers/members.ts +280 -80
  40. package/src/routers/notifications.ts +142 -0
  41. package/src/routers/payment.ts +448 -0
  42. package/src/routers/podcast.ts +133 -108
  43. package/src/routers/studyguide.ts +80 -74
  44. package/src/routers/worksheets.ts +300 -80
  45. package/src/routers/workspace.ts +538 -328
  46. package/src/scripts/purge-deleted-users.ts +167 -0
  47. package/src/server.ts +140 -12
  48. package/src/services/flashcard-progress.service.ts +52 -43
  49. package/src/trpc.ts +184 -5
  50. package/test-generate.js +30 -0
  51. package/test-ratio.cjs +9 -0
  52. package/zod-test.cjs +22 -0
  53. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
  54. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
  55. package/prisma/seed.mjs +0 -135
  56. package/src/routers/meetingsummary.ts +0 -416
@@ -2,6 +2,14 @@ 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 {
8
+ notifyInviteAccepted,
9
+ notifyInviteRecipient,
10
+ notifyWorkspaceMembershipRemoved,
11
+ notifyWorkspaceRoleChanged,
12
+ } from '../lib/notification-service.js';
5
13
 
6
14
  /**
7
15
  * Members router for workspace member management
@@ -69,14 +77,13 @@ export const members = router({
69
77
  });
70
78
 
71
79
  if (!workspace) {
72
- throw new TRPCError({
73
- code: 'NOT_FOUND',
74
- message: 'Workspace not found or access denied'
80
+ throw new TRPCError({
81
+ code: 'NOT_FOUND',
82
+ message: 'Workspace not found or access denied'
75
83
  });
76
84
  }
77
85
 
78
- // Format members with roles
79
- const members = [
86
+ const workspaceMembers = [
80
87
  {
81
88
  id: workspace.owner.id,
82
89
  name: workspace.owner.name || 'Unknown',
@@ -93,7 +100,11 @@ export const members = router({
93
100
  }))
94
101
  ];
95
102
 
96
- return members;
103
+ logger.info(`👥 Fetched ${workspaceMembers.length} members for workspace ${input.workspaceId}`, 'WORKSPACE', {
104
+ memberEmails: workspaceMembers.map(m => m.email)
105
+ });
106
+
107
+ return workspaceMembers;
97
108
  }),
98
109
 
99
110
  /**
@@ -116,9 +127,9 @@ export const members = router({
116
127
  });
117
128
 
118
129
  if (!workspace) {
119
- throw new TRPCError({
120
- code: 'NOT_FOUND',
121
- message: 'Workspace not found'
130
+ throw new TRPCError({
131
+ code: 'NOT_FOUND',
132
+ message: 'Workspace not found'
122
133
  });
123
134
  }
124
135
 
@@ -130,9 +141,9 @@ export const members = router({
130
141
  return workspace.members[0].role as 'admin' | 'member';
131
142
  }
132
143
 
133
- throw new TRPCError({
134
- code: 'FORBIDDEN',
135
- message: 'Access denied to this workspace'
144
+ throw new TRPCError({
145
+ code: 'FORBIDDEN',
146
+ message: 'Access denied to this workspace'
136
147
  });
137
148
  }),
138
149
 
@@ -148,22 +159,22 @@ export const members = router({
148
159
  .mutation(async ({ ctx, input }) => {
149
160
  // Check if user is owner or admin of the workspace
150
161
  const workspace = await ctx.db.workspace.findFirst({
151
- where: {
162
+ where: {
152
163
  id: input.workspaceId,
153
164
  ownerId: ctx.session.user.id // Only owners can invite for now
154
165
  }
155
166
  });
156
167
 
157
168
  if (!workspace) {
158
- throw new TRPCError({
159
- code: 'NOT_FOUND',
160
- message: 'Workspace not found or insufficient permissions'
169
+ throw new TRPCError({
170
+ code: 'NOT_FOUND',
171
+ message: 'Workspace not found or insufficient permissions'
161
172
  });
162
173
  }
163
174
 
164
175
  // Check if user is already a member
165
176
  const existingMember = await ctx.db.user.findFirst({
166
- where: {
177
+ where: {
167
178
  email: input.email,
168
179
  OR: [
169
180
  { id: workspace.ownerId },
@@ -173,9 +184,9 @@ export const members = router({
173
184
  });
174
185
 
175
186
  if (existingMember) {
176
- throw new TRPCError({
177
- code: 'BAD_REQUEST',
178
- message: 'User is already a member of this workspace'
187
+ throw new TRPCError({
188
+ code: 'BAD_REQUEST',
189
+ message: 'User is already a member of this workspace'
179
190
  });
180
191
  }
181
192
 
@@ -190,9 +201,9 @@ export const members = router({
190
201
  });
191
202
 
192
203
  if (existingInvitation) {
193
- throw new TRPCError({
194
- code: 'BAD_REQUEST',
195
- message: 'Invitation already sent to this email'
204
+ throw new TRPCError({
205
+ code: 'BAD_REQUEST',
206
+ message: 'Invitation already sent to this email'
196
207
  });
197
208
  }
198
209
 
@@ -219,6 +230,23 @@ export const members = router({
219
230
  }
220
231
  });
221
232
 
233
+ const invitedExistingUser = await ctx.db.user.findUnique({
234
+ where: { email: input.email },
235
+ select: { id: true, name: true },
236
+ });
237
+
238
+ if (invitedExistingUser) {
239
+ await notifyInviteRecipient(ctx.db, {
240
+ invitedUserId: invitedExistingUser.id,
241
+ inviterUserId: ctx.session.user.id,
242
+ workspaceId: input.workspaceId,
243
+ workspaceTitle: invitation.workspace.title,
244
+ invitationId: invitation.id,
245
+ invitationToken: invitation.token,
246
+ inviterName: invitation.workspace.owner.name || invitation.workspace.owner.email,
247
+ });
248
+ }
249
+
222
250
  logger.info(`🎫 Invitation created for ${input.email} to workspace ${input.workspaceId} with role ${input.role}`, 'WORKSPACE', {
223
251
  invitationId: invitation.id,
224
252
  workspaceId: input.workspaceId,
@@ -227,8 +255,14 @@ export const members = router({
227
255
  invitedBy: ctx.session.user.id
228
256
  });
229
257
 
230
- // TODO: Send email notification here
231
- // await sendInvitationEmail(invitation);
258
+ // Send email notification
259
+ await sendInvitationEmail({
260
+ email: invitation.email,
261
+ token: invitation.token,
262
+ role: invitation.role,
263
+ workspaceTitle: invitation.workspace.title,
264
+ invitedByName: invitation.workspace.owner.name || invitation.workspace.owner.email || 'Someone',
265
+ });
232
266
 
233
267
  return {
234
268
  invitationId: invitation.id,
@@ -270,6 +304,7 @@ export const members = router({
270
304
  select: {
271
305
  id: true,
272
306
  title: true,
307
+ ownerId: true,
273
308
  owner: {
274
309
  select: {
275
310
  name: true,
@@ -282,27 +317,31 @@ export const members = router({
282
317
  });
283
318
 
284
319
  if (!invitation) {
285
- throw new TRPCError({
286
- code: 'NOT_FOUND',
287
- message: 'Invalid or expired invitation'
320
+ throw new TRPCError({
321
+ code: 'NOT_FOUND',
322
+ message: 'Invalid or expired invitation'
288
323
  });
289
324
  }
290
325
 
291
326
  // Check if user is authenticated
292
327
  if (!ctx.session?.user) {
293
- throw new TRPCError({
294
- code: 'UNAUTHORIZED',
295
- message: 'Please log in to accept this invitation'
328
+ throw new TRPCError({
329
+ code: 'UNAUTHORIZED',
330
+ message: 'Please log in to accept this invitation'
296
331
  });
297
332
  }
298
333
 
299
334
  const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
300
335
  if (!user || !user.email) throw new TRPCError({ code: 'NOT_FOUND' });
301
- // Check if the email matches the user's email
302
- if (user.email !== invitation.email) {
303
- throw new TRPCError({
304
- code: 'BAD_REQUEST',
305
- message: 'This invitation was sent to a different email address'
336
+
337
+ logger.info(`🔍 Verification check for ${user.email} accepting invite for ${invitation.email}`, 'WORKSPACE');
338
+
339
+ // Check if the email matches the user's email (case-insensitive)
340
+ if (user.email.toLowerCase() !== invitation.email.toLowerCase()) {
341
+ logger.warn(`❌ Invitation email mismatch: user ${user.email} vs invite ${invitation.email}`, 'WORKSPACE');
342
+ throw new TRPCError({
343
+ code: 'BAD_REQUEST',
344
+ message: 'This invitation was sent to a different email address'
306
345
  });
307
346
  }
308
347
 
@@ -318,26 +357,41 @@ export const members = router({
318
357
  });
319
358
 
320
359
  if (isAlreadyMember) {
360
+ logger.info(`ℹ️ User ${ctx.session.user.id} is already a member of workspace ${invitation.workspaceId}. Marking invite as accepted.`, 'WORKSPACE');
321
361
  // Mark invitation as accepted even if already a member
322
362
  await ctx.db.workspaceInvitation.update({
323
363
  where: { id: invitation.id },
324
364
  data: { acceptedAt: new Date() }
325
365
  });
326
366
 
327
- throw new TRPCError({
328
- code: 'BAD_REQUEST',
329
- message: 'You are already a member of this workspace'
330
- });
331
- }
332
-
333
- // Add user to workspace with proper role
334
- await ctx.db.workspaceMember.create({
335
- data: {
367
+ return {
336
368
  workspaceId: invitation.workspaceId,
337
- userId: ctx.session.user.id,
369
+ workspaceTitle: invitation.workspace.title,
338
370
  role: invitation.role,
371
+ ownerName: invitation.workspace.owner.name || invitation.workspace.owner.email,
372
+ message: 'You are already a member of this workspace'
373
+ };
374
+ }
375
+
376
+ // Add user to workspace with proper role.
377
+ // This can race if accept is triggered twice (e.g. redirects/retries),
378
+ // so treat duplicate membership as a successful, idempotent accept.
379
+ let memberAdded = false;
380
+ try {
381
+ await ctx.db.workspaceMember.create({
382
+ data: {
383
+ workspaceId: invitation.workspaceId,
384
+ userId: ctx.session.user.id,
385
+ role: invitation.role,
386
+ }
387
+ });
388
+ memberAdded = true;
389
+ } catch (error: any) {
390
+ if (error?.code !== 'P2002') {
391
+ throw error;
339
392
  }
340
- });
393
+ logger.info(`ℹ️ Duplicate invite accept handled for user ${ctx.session.user.id} in workspace ${invitation.workspaceId}`, 'WORKSPACE');
394
+ }
341
395
 
342
396
  // Mark invitation as accepted
343
397
  await ctx.db.workspaceInvitation.update({
@@ -345,18 +399,44 @@ export const members = router({
345
399
  data: { acceptedAt: new Date() }
346
400
  });
347
401
 
348
- logger.info(`✅ Invitation accepted by ${ctx.session.user.id} for workspace ${invitation.workspaceId}`, 'WORKSPACE', {
402
+ if (memberAdded) {
403
+ await notifyInviteAccepted(ctx.db, {
404
+ recipientUserIds: [invitation.invitedById, invitation.workspace.ownerId],
405
+ actorUserId: ctx.session.user.id,
406
+ workspaceId: invitation.workspaceId,
407
+ workspaceTitle: invitation.workspace.title,
408
+ memberName: user.name || user.email,
409
+ invitationId: invitation.id,
410
+ });
411
+ }
412
+
413
+ logger.info(`✅ Invitation accepted by ${ctx.session.user.id} (${user.email}) for workspace ${invitation.workspaceId}`, 'WORKSPACE', {
349
414
  invitationId: invitation.id,
350
415
  workspaceId: invitation.workspaceId,
351
416
  userId: ctx.session.user.id,
352
417
  email: invitation.email
353
418
  });
354
419
 
420
+ // Try to emit a Pusher event if possible
421
+ try {
422
+ if (memberAdded) {
423
+ await PusherService.emitMemberJoined(invitation.workspaceId, {
424
+ id: ctx.session.user.id,
425
+ name: user.name || 'Member',
426
+ email: user.email,
427
+ role: invitation.role,
428
+ });
429
+ }
430
+ } catch (e) {
431
+ logger.error('Failed to emit member joined event', e);
432
+ }
433
+
355
434
  return {
356
435
  workspaceId: invitation.workspaceId,
357
436
  workspaceTitle: invitation.workspace.title,
358
437
  role: invitation.role,
359
438
  ownerName: invitation.workspace.owner.name || invitation.workspace.owner.email,
439
+ ...(memberAdded ? {} : { message: 'You are already a member of this workspace' }),
360
440
  };
361
441
  }),
362
442
 
@@ -372,38 +452,39 @@ export const members = router({
372
452
  .mutation(async ({ ctx, input }) => {
373
453
  // Check if user is owner of the workspace
374
454
  const workspace = await ctx.db.workspace.findFirst({
375
- where: {
455
+ where: {
376
456
  id: input.workspaceId,
377
457
  ownerId: ctx.session.user.id
378
- }
458
+ },
459
+ select: { id: true, title: true, ownerId: true },
379
460
  });
380
461
 
381
462
  if (!workspace) {
382
- throw new TRPCError({
383
- code: 'NOT_FOUND',
384
- message: 'Workspace not found or insufficient permissions'
463
+ throw new TRPCError({
464
+ code: 'NOT_FOUND',
465
+ message: 'Workspace not found or insufficient permissions'
385
466
  });
386
467
  }
387
468
 
388
469
  // Check if member exists and is not the owner
389
470
  if (input.memberId === workspace.ownerId) {
390
- throw new TRPCError({
391
- code: 'BAD_REQUEST',
392
- message: 'Cannot change owner role'
471
+ throw new TRPCError({
472
+ code: 'BAD_REQUEST',
473
+ message: 'Cannot change owner role'
393
474
  });
394
475
  }
395
476
 
396
477
  const member = await ctx.db.workspaceMember.findFirst({
397
- where: {
478
+ where: {
398
479
  workspaceId: input.workspaceId,
399
480
  userId: input.memberId
400
481
  }
401
482
  });
402
483
 
403
484
  if (!member) {
404
- throw new TRPCError({
405
- code: 'NOT_FOUND',
406
- message: 'Member not found in this workspace'
485
+ throw new TRPCError({
486
+ code: 'NOT_FOUND',
487
+ message: 'Member not found in this workspace'
407
488
  });
408
489
  }
409
490
 
@@ -430,6 +511,20 @@ export const members = router({
430
511
  changedBy: ctx.session.user.id
431
512
  });
432
513
 
514
+ const actor = await ctx.db.user.findUnique({
515
+ where: { id: ctx.session.user.id },
516
+ select: { name: true, email: true },
517
+ });
518
+ await notifyWorkspaceRoleChanged(ctx.db, {
519
+ memberUserId: input.memberId,
520
+ workspaceId: input.workspaceId,
521
+ workspaceTitle: workspace.title,
522
+ newRole: input.role,
523
+ oldRole: member.role,
524
+ actorUserId: ctx.session.user.id,
525
+ actorName: actor?.name || actor?.email || 'A workspace admin',
526
+ }).catch(() => {});
527
+
433
528
  return {
434
529
  memberId: input.memberId,
435
530
  role: input.role,
@@ -449,30 +544,31 @@ export const members = router({
449
544
  .mutation(async ({ ctx, input }) => {
450
545
  // Check if user is owner of the workspace
451
546
  const workspace = await ctx.db.workspace.findFirst({
452
- where: {
547
+ where: {
453
548
  id: input.workspaceId,
454
549
  ownerId: ctx.session.user.id
455
- }
550
+ },
551
+ select: { id: true, title: true, ownerId: true },
456
552
  });
457
553
 
458
554
  if (!workspace) {
459
- throw new TRPCError({
460
- code: 'NOT_FOUND',
461
- message: 'Workspace not found or insufficient permissions'
555
+ throw new TRPCError({
556
+ code: 'NOT_FOUND',
557
+ message: 'Workspace not found or insufficient permissions'
462
558
  });
463
559
  }
464
560
 
465
561
  // Check if trying to remove the owner
466
562
  if (input.memberId === workspace.ownerId) {
467
- throw new TRPCError({
468
- code: 'BAD_REQUEST',
469
- message: 'Cannot remove workspace owner'
563
+ throw new TRPCError({
564
+ code: 'BAD_REQUEST',
565
+ message: 'Cannot remove workspace owner'
470
566
  });
471
567
  }
472
568
 
473
569
  // Check if member exists
474
570
  const member = await ctx.db.workspaceMember.findFirst({
475
- where: {
571
+ where: {
476
572
  workspaceId: input.workspaceId,
477
573
  userId: input.memberId
478
574
  },
@@ -487,12 +583,24 @@ export const members = router({
487
583
  });
488
584
 
489
585
  if (!member) {
490
- throw new TRPCError({
491
- code: 'NOT_FOUND',
492
- message: 'Member not found in this workspace'
586
+ throw new TRPCError({
587
+ code: 'NOT_FOUND',
588
+ message: 'Member not found in this workspace'
493
589
  });
494
590
  }
495
591
 
592
+ const actor = await ctx.db.user.findUnique({
593
+ where: { id: ctx.session.user.id },
594
+ select: { name: true, email: true },
595
+ });
596
+ await notifyWorkspaceMembershipRemoved(ctx.db, {
597
+ memberUserId: input.memberId,
598
+ workspaceId: input.workspaceId,
599
+ workspaceTitle: workspace.title,
600
+ actorUserId: ctx.session.user.id,
601
+ actorName: actor?.name || actor?.email || 'A workspace admin',
602
+ }).catch(() => {});
603
+
496
604
  // Remove member from workspace
497
605
  await ctx.db.workspaceMember.delete({
498
606
  where: { id: member.id }
@@ -520,16 +628,16 @@ export const members = router({
520
628
  .query(async ({ ctx, input }) => {
521
629
  // Check if user is owner of the workspace
522
630
  const workspace = await ctx.db.workspace.findFirst({
523
- where: {
631
+ where: {
524
632
  id: input.workspaceId,
525
633
  ownerId: ctx.session.user.id
526
634
  }
527
635
  });
528
636
 
529
637
  if (!workspace) {
530
- throw new TRPCError({
531
- code: 'NOT_FOUND',
532
- message: 'Workspace not found or insufficient permissions'
638
+ throw new TRPCError({
639
+ code: 'NOT_FOUND',
640
+ message: 'Workspace not found or insufficient permissions'
533
641
  });
534
642
  }
535
643
 
@@ -571,7 +679,7 @@ export const members = router({
571
679
  .mutation(async ({ ctx, input }) => {
572
680
  // Check if user is owner of the workspace
573
681
  const invitation = await ctx.db.workspaceInvitation.findFirst({
574
- where: {
682
+ where: {
575
683
  id: input.invitationId,
576
684
  acceptedAt: null,
577
685
  workspace: {
@@ -581,9 +689,9 @@ export const members = router({
581
689
  });
582
690
 
583
691
  if (!invitation) {
584
- throw new TRPCError({
585
- code: 'NOT_FOUND',
586
- message: 'Invitation not found or insufficient permissions'
692
+ throw new TRPCError({
693
+ code: 'NOT_FOUND',
694
+ message: 'Invitation not found or insufficient permissions'
587
695
  });
588
696
  }
589
697
 
@@ -604,4 +712,96 @@ export const members = router({
604
712
  message: 'Invitation cancelled successfully'
605
713
  };
606
714
  }),
715
+
716
+ /**
717
+ * Resend a pending invitation (owner only)
718
+ */
719
+ resendInvitation: authedProcedure
720
+ .input(z.object({
721
+ invitationId: z.string(),
722
+ }))
723
+ .mutation(async ({ ctx, input }) => {
724
+ // Check if user is owner of the workspace and invitation is pending
725
+ const invitation = await ctx.db.workspaceInvitation.findFirst({
726
+ where: {
727
+ id: input.invitationId,
728
+ acceptedAt: null,
729
+ workspace: {
730
+ ownerId: ctx.session.user.id
731
+ }
732
+ },
733
+ include: {
734
+ workspace: {
735
+ select: {
736
+ title: true,
737
+ owner: {
738
+ select: {
739
+ name: true,
740
+ email: true,
741
+ }
742
+ }
743
+ }
744
+ }
745
+ }
746
+ });
747
+
748
+ if (!invitation) {
749
+ throw new TRPCError({
750
+ code: 'NOT_FOUND',
751
+ message: 'Invitation not found or insufficient permissions'
752
+ });
753
+ }
754
+
755
+ // Check if expired and update expiry if needed
756
+ if (invitation.expiresAt < new Date()) {
757
+ const newExpiry = new Date();
758
+ newExpiry.setDate(newExpiry.getDate() + 7);
759
+ await ctx.db.workspaceInvitation.update({
760
+ where: { id: invitation.id },
761
+ data: { expiresAt: newExpiry }
762
+ });
763
+ invitation.expiresAt = newExpiry;
764
+ }
765
+
766
+ // Send email notification
767
+ await sendInvitationEmail({
768
+ email: invitation.email,
769
+ token: invitation.token,
770
+ role: invitation.role,
771
+ workspaceTitle: invitation.workspace.title,
772
+ invitedByName: invitation.workspace.owner.name || invitation.workspace.owner.email || 'Someone',
773
+ });
774
+
775
+ logger.info(`📧 Invitation resent to ${invitation.email} for workspace ${invitation.workspaceId}`, 'WORKSPACE', {
776
+ invitationId: invitation.id,
777
+ workspaceId: invitation.workspaceId,
778
+ email: invitation.email,
779
+ resentBy: ctx.session.user.id
780
+ });
781
+
782
+ return {
783
+ invitationId: invitation.id,
784
+ message: 'Invitation email resent successfully'
785
+ };
786
+ }),
787
+
788
+ /**
789
+ * DEBUG ONLY: Get all invitations for a workspace
790
+ */
791
+ getAllInvitationsDebug: authedProcedure
792
+ .input(z.object({
793
+ workspaceId: z.string(),
794
+ }))
795
+ .query(async ({ ctx, input }) => {
796
+ // Check if user is owner
797
+ const workspace = await ctx.db.workspace.findFirst({
798
+ where: { id: input.workspaceId, ownerId: ctx.session.user.id }
799
+ });
800
+ if (!workspace) throw new TRPCError({ code: 'UNAUTHORIZED' });
801
+
802
+ return ctx.db.workspaceInvitation.findMany({
803
+ where: { workspaceId: input.workspaceId },
804
+ orderBy: { createdAt: 'desc' }
805
+ });
806
+ }),
607
807
  });