@goscribe/server 1.1.7 → 1.2.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.
- package/.env.example +43 -0
- package/dist/routers/auth.js +1 -1
- package/package.json +2 -1
- package/prisma/schema.prisma +46 -0
- package/src/lib/ai-session.ts +117 -63
- package/src/lib/constants.ts +14 -0
- package/src/lib/email.ts +46 -0
- package/src/lib/inference.ts +1 -1
- package/src/lib/logger.ts +26 -9
- package/src/lib/pusher.ts +4 -4
- package/src/lib/retry.ts +61 -0
- package/src/lib/storage.ts +2 -2
- package/src/lib/workspace-access.ts +13 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/annotations.ts +186 -0
- package/src/routers/auth.ts +98 -9
- package/src/routers/flashcards.ts +7 -13
- package/src/routers/podcast.ts +24 -28
- package/src/routers/studyguide.ts +68 -74
- package/src/routers/worksheets.ts +11 -14
- package/src/routers/workspace.ts +275 -272
- package/src/server.ts +4 -2
- package/src/services/flashcard-progress.service.ts +3 -6
- package/src/routers/meetingsummary.ts +0 -416
package/src/lib/retry.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
|
|
3
|
+
export interface RetryOptions {
|
|
4
|
+
/** Maximum number of attempts (default: 3) */
|
|
5
|
+
maxRetries?: number;
|
|
6
|
+
/** Base delay in ms for exponential backoff (default: 2000) */
|
|
7
|
+
baseDelayMs?: number;
|
|
8
|
+
/** Timeout per attempt in ms (default: 300000 = 5 minutes) */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
/** Label for logging (optional) */
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Wraps an async function with retry logic and exponential backoff.
|
|
16
|
+
* Throws the last error if all attempts fail.
|
|
17
|
+
*/
|
|
18
|
+
export async function withRetry<T>(
|
|
19
|
+
fn: () => Promise<T>,
|
|
20
|
+
options: RetryOptions = {}
|
|
21
|
+
): Promise<T> {
|
|
22
|
+
const {
|
|
23
|
+
maxRetries = 3,
|
|
24
|
+
baseDelayMs = 2000,
|
|
25
|
+
timeoutMs = 300000,
|
|
26
|
+
label = 'operation',
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
let lastError: Error | null = null;
|
|
30
|
+
|
|
31
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
32
|
+
try {
|
|
33
|
+
logger.info(`[retry] ${label} attempt ${attempt}/${maxRetries}`);
|
|
34
|
+
|
|
35
|
+
// Create an AbortController for per-attempt timeout
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = await fn();
|
|
41
|
+
clearTimeout(timeoutId);
|
|
42
|
+
return result;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
clearTimeout(timeoutId);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
49
|
+
logger.error(`[retry] ${label} attempt ${attempt} failed: ${lastError.message}`);
|
|
50
|
+
|
|
51
|
+
if (attempt < maxRetries) {
|
|
52
|
+
const delay = Math.pow(2, attempt) * baseDelayMs;
|
|
53
|
+
logger.info(`[retry] ${label} retrying in ${delay}ms...`);
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
logger.error(`[retry] ${label} all ${maxRetries} attempts failed`);
|
|
60
|
+
throw lastError!;
|
|
61
|
+
}
|
package/src/lib/storage.ts
CHANGED
|
@@ -88,13 +88,13 @@ export async function makeFilePublic(objectKey: string): Promise<void> {
|
|
|
88
88
|
// In Supabase, files are public by default when uploaded to public buckets
|
|
89
89
|
// For private buckets, you would need to update the bucket policy
|
|
90
90
|
// This function is kept for compatibility but may not be needed
|
|
91
|
-
|
|
91
|
+
// File is already public in Supabase Storage
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export async function makeFilePrivate(objectKey: string): Promise<void> {
|
|
95
95
|
// In Supabase, you would need to update the bucket policy to make files private
|
|
96
96
|
// This function is kept for compatibility but may not be needed
|
|
97
|
-
|
|
97
|
+
// File privacy is controlled by bucket policy in Supabase Storage
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Export supabase client for direct access if needed
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma `where` filter for workspace access checks.
|
|
3
|
+
* Allows access if the user is the owner OR a member of the workspace.
|
|
4
|
+
* Use this instead of `{ ownerId: userId }` to support collaborative workspaces.
|
|
5
|
+
*/
|
|
6
|
+
export function workspaceAccessFilter(userId: string) {
|
|
7
|
+
return {
|
|
8
|
+
OR: [
|
|
9
|
+
{ ownerId: userId },
|
|
10
|
+
{ members: { some: { userId } } },
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
}
|
package/src/routers/_app.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { studyguide } from './studyguide.js';
|
|
|
8
8
|
import { podcast } from './podcast.js';
|
|
9
9
|
import { chat } from './chat.js';
|
|
10
10
|
import { members } from './members.js';
|
|
11
|
+
import { annotations } from './annotations.js';
|
|
11
12
|
|
|
12
13
|
export const appRouter = router({
|
|
13
14
|
auth,
|
|
@@ -17,6 +18,7 @@ export const appRouter = router({
|
|
|
17
18
|
studyguide,
|
|
18
19
|
podcast,
|
|
19
20
|
chat,
|
|
21
|
+
annotations,
|
|
20
22
|
// Public member endpoints (for invitation acceptance)
|
|
21
23
|
member: router({
|
|
22
24
|
acceptInvite: members.acceptInvite,
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { TRPCError } from '@trpc/server';
|
|
3
|
+
import { router, authedProcedure } from '../trpc.js';
|
|
4
|
+
|
|
5
|
+
export const annotations = router({
|
|
6
|
+
// List all highlights (with nested comments) for an artifact version
|
|
7
|
+
listHighlights: authedProcedure
|
|
8
|
+
.input(
|
|
9
|
+
z.object({
|
|
10
|
+
artifactVersionId: z.string(),
|
|
11
|
+
})
|
|
12
|
+
)
|
|
13
|
+
.query(async ({ ctx, input }) => {
|
|
14
|
+
const highlights = await ctx.db.studyGuideHighlight.findMany({
|
|
15
|
+
where: { artifactVersionId: input.artifactVersionId },
|
|
16
|
+
include: {
|
|
17
|
+
comments: {
|
|
18
|
+
include: {
|
|
19
|
+
user: { select: { id: true, name: true, profilePicture: true } },
|
|
20
|
+
},
|
|
21
|
+
orderBy: { createdAt: 'asc' },
|
|
22
|
+
},
|
|
23
|
+
user: { select: { id: true, name: true, profilePicture: true } },
|
|
24
|
+
},
|
|
25
|
+
orderBy: { startOffset: 'asc' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return highlights;
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
// Create a new highlight
|
|
32
|
+
createHighlight: authedProcedure
|
|
33
|
+
.input(
|
|
34
|
+
z.object({
|
|
35
|
+
artifactVersionId: z.string(),
|
|
36
|
+
startOffset: z.number().int().min(0),
|
|
37
|
+
endOffset: z.number().int().min(0),
|
|
38
|
+
selectedText: z.string().min(1),
|
|
39
|
+
color: z.string().optional(),
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
.mutation(async ({ ctx, input }) => {
|
|
43
|
+
// Verify the artifact version exists
|
|
44
|
+
const version = await ctx.db.artifactVersion.findUnique({
|
|
45
|
+
where: { id: input.artifactVersionId },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!version) {
|
|
49
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Artifact version not found' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const highlight = await ctx.db.studyGuideHighlight.create({
|
|
53
|
+
data: {
|
|
54
|
+
artifactVersionId: input.artifactVersionId,
|
|
55
|
+
userId: ctx.session.user.id,
|
|
56
|
+
startOffset: input.startOffset,
|
|
57
|
+
endOffset: input.endOffset,
|
|
58
|
+
selectedText: input.selectedText,
|
|
59
|
+
...(input.color && { color: input.color }),
|
|
60
|
+
},
|
|
61
|
+
include: {
|
|
62
|
+
comments: true,
|
|
63
|
+
user: { select: { id: true, name: true, profilePicture: true } },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return highlight;
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
// Delete a highlight (and its comments via cascade)
|
|
71
|
+
deleteHighlight: authedProcedure
|
|
72
|
+
.input(
|
|
73
|
+
z.object({
|
|
74
|
+
highlightId: z.string(),
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
.mutation(async ({ ctx, input }) => {
|
|
78
|
+
const highlight = await ctx.db.studyGuideHighlight.findUnique({
|
|
79
|
+
where: { id: input.highlightId },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!highlight) {
|
|
83
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Highlight not found' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (highlight.userId !== ctx.session.user.id) {
|
|
87
|
+
throw new TRPCError({ code: 'FORBIDDEN', message: 'You can only delete your own highlights' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await ctx.db.studyGuideHighlight.delete({
|
|
91
|
+
where: { id: input.highlightId },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return { success: true };
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
// Add a comment to a highlight
|
|
98
|
+
addComment: authedProcedure
|
|
99
|
+
.input(
|
|
100
|
+
z.object({
|
|
101
|
+
highlightId: z.string(),
|
|
102
|
+
content: z.string().min(1),
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
.mutation(async ({ ctx, input }) => {
|
|
106
|
+
const highlight = await ctx.db.studyGuideHighlight.findUnique({
|
|
107
|
+
where: { id: input.highlightId },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!highlight) {
|
|
111
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Highlight not found' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const comment = await ctx.db.studyGuideComment.create({
|
|
115
|
+
data: {
|
|
116
|
+
highlightId: input.highlightId,
|
|
117
|
+
userId: ctx.session.user.id,
|
|
118
|
+
content: input.content,
|
|
119
|
+
},
|
|
120
|
+
include: {
|
|
121
|
+
user: { select: { id: true, name: true, profilePicture: true } },
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return comment;
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
// Update a comment
|
|
129
|
+
updateComment: authedProcedure
|
|
130
|
+
.input(
|
|
131
|
+
z.object({
|
|
132
|
+
commentId: z.string(),
|
|
133
|
+
content: z.string().min(1),
|
|
134
|
+
})
|
|
135
|
+
)
|
|
136
|
+
.mutation(async ({ ctx, input }) => {
|
|
137
|
+
const comment = await ctx.db.studyGuideComment.findUnique({
|
|
138
|
+
where: { id: input.commentId },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!comment) {
|
|
142
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Comment not found' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (comment.userId !== ctx.session.user.id) {
|
|
146
|
+
throw new TRPCError({ code: 'FORBIDDEN', message: 'You can only edit your own comments' });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const updated = await ctx.db.studyGuideComment.update({
|
|
150
|
+
where: { id: input.commentId },
|
|
151
|
+
data: { content: input.content },
|
|
152
|
+
include: {
|
|
153
|
+
user: { select: { id: true, name: true, profilePicture: true } },
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return updated;
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
// Delete a comment
|
|
161
|
+
deleteComment: authedProcedure
|
|
162
|
+
.input(
|
|
163
|
+
z.object({
|
|
164
|
+
commentId: z.string(),
|
|
165
|
+
})
|
|
166
|
+
)
|
|
167
|
+
.mutation(async ({ ctx, input }) => {
|
|
168
|
+
const comment = await ctx.db.studyGuideComment.findUnique({
|
|
169
|
+
where: { id: input.commentId },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!comment) {
|
|
173
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Comment not found' });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (comment.userId !== ctx.session.user.id) {
|
|
177
|
+
throw new TRPCError({ code: 'FORBIDDEN', message: 'You can only delete your own comments' });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await ctx.db.studyGuideComment.delete({
|
|
181
|
+
where: { id: input.commentId },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return { success: true };
|
|
185
|
+
}),
|
|
186
|
+
});
|
package/src/routers/auth.ts
CHANGED
|
@@ -4,13 +4,14 @@ import bcrypt from 'bcryptjs';
|
|
|
4
4
|
import { serialize } from 'cookie';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { TRPCError } from '@trpc/server';
|
|
7
|
-
import { supabaseClient } from '
|
|
7
|
+
import { supabaseClient } from '../lib/storage.js';
|
|
8
|
+
import { sendVerificationEmail } from '../lib/email.js';
|
|
8
9
|
|
|
9
10
|
// Helper to create custom auth token
|
|
10
11
|
function createCustomAuthToken(userId: string): string {
|
|
11
12
|
const secret = process.env.AUTH_SECRET;
|
|
12
13
|
if (!secret) {
|
|
13
|
-
throw new
|
|
14
|
+
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: "AUTH_SECRET is not set" });
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const base64UserId = Buffer.from(userId, 'utf8').toString('base64url');
|
|
@@ -80,7 +81,7 @@ export const auth = router({
|
|
|
80
81
|
where: { email: input.email },
|
|
81
82
|
});
|
|
82
83
|
if (existing) {
|
|
83
|
-
throw new
|
|
84
|
+
throw new TRPCError({ code: 'CONFLICT', message: "Email already registered" });
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const hash = await bcrypt.hash(input.password, 10);
|
|
@@ -90,12 +91,99 @@ export const auth = router({
|
|
|
90
91
|
name: input.name,
|
|
91
92
|
email: input.email,
|
|
92
93
|
passwordHash: hash,
|
|
93
|
-
emailVerified
|
|
94
|
+
// emailVerified is null -- user must verify
|
|
94
95
|
},
|
|
95
96
|
});
|
|
96
97
|
|
|
98
|
+
// Create verification token (24h expiry)
|
|
99
|
+
const token = crypto.randomUUID();
|
|
100
|
+
await ctx.db.verificationToken.create({
|
|
101
|
+
data: {
|
|
102
|
+
identifier: input.email,
|
|
103
|
+
token,
|
|
104
|
+
expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Send verification email (non-blocking)
|
|
109
|
+
sendVerificationEmail(input.email, token, input.name).catch(() => {});
|
|
110
|
+
|
|
97
111
|
return { id: user.id, email: user.email, name: user.name };
|
|
98
112
|
}),
|
|
113
|
+
|
|
114
|
+
// Verify email with token from the email link
|
|
115
|
+
verifyEmail: publicProcedure
|
|
116
|
+
.input(z.object({
|
|
117
|
+
token: z.string(),
|
|
118
|
+
}))
|
|
119
|
+
.mutation(async ({ ctx, input }) => {
|
|
120
|
+
const record = await ctx.db.verificationToken.findUnique({
|
|
121
|
+
where: { token: input.token },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!record) {
|
|
125
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Invalid or expired verification link' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (record.expires < new Date()) {
|
|
129
|
+
// Clean up expired token
|
|
130
|
+
await ctx.db.verificationToken.delete({ where: { token: input.token } });
|
|
131
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Verification link has expired. Please request a new one.' });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Mark user as verified
|
|
135
|
+
await ctx.db.user.update({
|
|
136
|
+
where: { email: record.identifier },
|
|
137
|
+
data: { emailVerified: new Date() },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Delete used token
|
|
141
|
+
await ctx.db.verificationToken.delete({ where: { token: input.token } });
|
|
142
|
+
|
|
143
|
+
return { success: true, message: 'Email verified successfully' };
|
|
144
|
+
}),
|
|
145
|
+
|
|
146
|
+
// Resend verification email (for logged-in users who haven't verified)
|
|
147
|
+
resendVerification: publicProcedure
|
|
148
|
+
.mutation(async ({ ctx }) => {
|
|
149
|
+
if (!ctx.session?.user?.id) {
|
|
150
|
+
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not logged in' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const user = await ctx.db.user.findUnique({
|
|
154
|
+
where: { id: ctx.session.user.id },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!user || !user.email) {
|
|
158
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (user.emailVerified) {
|
|
162
|
+
return { success: true, message: 'Email is already verified' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Delete any existing tokens for this email
|
|
166
|
+
await ctx.db.verificationToken.deleteMany({
|
|
167
|
+
where: { identifier: user.email },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Create new token
|
|
171
|
+
const token = crypto.randomUUID();
|
|
172
|
+
await ctx.db.verificationToken.create({
|
|
173
|
+
data: {
|
|
174
|
+
identifier: user.email,
|
|
175
|
+
token,
|
|
176
|
+
expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const sent = await sendVerificationEmail(user.email, token, user.name);
|
|
181
|
+
if (!sent) {
|
|
182
|
+
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to send email. Please try again.' });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { success: true, message: 'Verification email sent' };
|
|
186
|
+
}),
|
|
99
187
|
login: publicProcedure
|
|
100
188
|
.input(z.object({
|
|
101
189
|
email: z.string().email(),
|
|
@@ -106,12 +194,12 @@ export const auth = router({
|
|
|
106
194
|
where: { email: input.email },
|
|
107
195
|
});
|
|
108
196
|
if (!user) {
|
|
109
|
-
throw new
|
|
197
|
+
throw new TRPCError({ code: 'UNAUTHORIZED', message: "Invalid credentials" });
|
|
110
198
|
}
|
|
111
199
|
|
|
112
200
|
const valid = await bcrypt.compare(input.password, user.passwordHash!);
|
|
113
201
|
if (!valid) {
|
|
114
|
-
throw new
|
|
202
|
+
throw new TRPCError({ code: 'UNAUTHORIZED', message: "Invalid credentials" });
|
|
115
203
|
}
|
|
116
204
|
|
|
117
205
|
// Create custom auth token
|
|
@@ -141,7 +229,7 @@ export const auth = router({
|
|
|
141
229
|
getSession: publicProcedure.query(async ({ ctx }) => {
|
|
142
230
|
// Just return the current session from context
|
|
143
231
|
if (!ctx.session) {
|
|
144
|
-
throw new
|
|
232
|
+
throw new TRPCError({ code: 'UNAUTHORIZED', message: "No session found" });
|
|
145
233
|
}
|
|
146
234
|
|
|
147
235
|
const user = await ctx.db.user.findUnique({
|
|
@@ -149,7 +237,7 @@ export const auth = router({
|
|
|
149
237
|
});
|
|
150
238
|
|
|
151
239
|
if (!user) {
|
|
152
|
-
throw new
|
|
240
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: "User not found" });
|
|
153
241
|
}
|
|
154
242
|
|
|
155
243
|
return {
|
|
@@ -157,6 +245,7 @@ export const auth = router({
|
|
|
157
245
|
id: user.id,
|
|
158
246
|
email: user.email,
|
|
159
247
|
name: user.name,
|
|
248
|
+
emailVerified: !!user.emailVerified,
|
|
160
249
|
}
|
|
161
250
|
};
|
|
162
251
|
}),
|
|
@@ -164,7 +253,7 @@ export const auth = router({
|
|
|
164
253
|
const token = ctx.cookies["auth_token"];
|
|
165
254
|
|
|
166
255
|
if (!token) {
|
|
167
|
-
throw new
|
|
256
|
+
throw new TRPCError({ code: 'UNAUTHORIZED', message: "No token found" });
|
|
168
257
|
}
|
|
169
258
|
|
|
170
259
|
await ctx.db.session.delete({
|
|
@@ -5,14 +5,8 @@ import createInferenceService from '../lib/inference.js';
|
|
|
5
5
|
import { aiSessionService } from '../lib/ai-session.js';
|
|
6
6
|
import PusherService from '../lib/pusher.js';
|
|
7
7
|
import { createFlashcardProgressService } from '../services/flashcard-progress.service.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
STUDY_GUIDE: 'STUDY_GUIDE',
|
|
11
|
-
FLASHCARD_SET: 'FLASHCARD_SET',
|
|
12
|
-
WORKSHEET: 'WORKSHEET',
|
|
13
|
-
MEETING_SUMMARY: 'MEETING_SUMMARY',
|
|
14
|
-
PODCAST_EPISODE: 'PODCAST_EPISODE',
|
|
15
|
-
} as const;
|
|
8
|
+
import { ArtifactType } from '../lib/constants.js';
|
|
9
|
+
import { workspaceAccessFilter } from '../lib/workspace-access.js';
|
|
16
10
|
|
|
17
11
|
export const flashcards = router({
|
|
18
12
|
listSets: authedProcedure
|
|
@@ -37,7 +31,7 @@ export const flashcards = router({
|
|
|
37
31
|
.input(z.object({ workspaceId: z.string() }))
|
|
38
32
|
.query(async ({ ctx, input }) => {
|
|
39
33
|
const set = await ctx.db.artifact.findFirst({
|
|
40
|
-
where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace:
|
|
34
|
+
where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace: workspaceAccessFilter(ctx.session.user.id) },
|
|
41
35
|
include: {
|
|
42
36
|
flashcards: {
|
|
43
37
|
include: {
|
|
@@ -59,7 +53,7 @@ export const flashcards = router({
|
|
|
59
53
|
.input(z.object({ workspaceId: z.string() }))
|
|
60
54
|
.query(async ({ ctx, input }) => {
|
|
61
55
|
const artifact = await ctx.db.artifact.findFirst({
|
|
62
|
-
where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace:
|
|
56
|
+
where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace: workspaceAccessFilter(ctx.session.user.id) }, orderBy: { updatedAt: 'desc' },
|
|
63
57
|
});
|
|
64
58
|
return artifact?.generating;
|
|
65
59
|
}),
|
|
@@ -103,7 +97,7 @@ export const flashcards = router({
|
|
|
103
97
|
}))
|
|
104
98
|
.mutation(async ({ ctx, input }) => {
|
|
105
99
|
const card = await ctx.db.flashcard.findFirst({
|
|
106
|
-
where: { id: input.cardId, artifact: { type: ArtifactType.FLASHCARD_SET, workspace:
|
|
100
|
+
where: { id: input.cardId, artifact: { type: ArtifactType.FLASHCARD_SET, workspace: workspaceAccessFilter(ctx.session.user.id) } },
|
|
107
101
|
});
|
|
108
102
|
if (!card) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
109
103
|
return ctx.db.flashcard.update({
|
|
@@ -121,7 +115,7 @@ export const flashcards = router({
|
|
|
121
115
|
.input(z.object({ cardId: z.string() }))
|
|
122
116
|
.mutation(async ({ ctx, input }) => {
|
|
123
117
|
const card = await ctx.db.flashcard.findFirst({
|
|
124
|
-
where: { id: input.cardId, artifact: { workspace:
|
|
118
|
+
where: { id: input.cardId, artifact: { workspace: workspaceAccessFilter(ctx.session.user.id) } },
|
|
125
119
|
});
|
|
126
120
|
if (!card) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
127
121
|
await ctx.db.flashcard.delete({ where: { id: input.cardId } });
|
|
@@ -132,7 +126,7 @@ export const flashcards = router({
|
|
|
132
126
|
.input(z.object({ setId: z.string().uuid() }))
|
|
133
127
|
.mutation(async ({ ctx, input }) => {
|
|
134
128
|
const deleted = await ctx.db.artifact.deleteMany({
|
|
135
|
-
where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace:
|
|
129
|
+
where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace: workspaceAccessFilter(ctx.session.user.id) },
|
|
136
130
|
});
|
|
137
131
|
if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
138
132
|
return true;
|