@goscribe/server 1.0.5 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goscribe/server",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,11 +24,11 @@
24
24
  "@google-cloud/storage": "^7.17.0",
25
25
  "@prisma/client": "^6.14.0",
26
26
  "@trpc/server": "^11.5.0",
27
- "@types/cookie": "^1.0.0",
28
27
  "bcryptjs": "^3.0.2",
29
28
  "compression": "^1.8.1",
30
29
  "cookie": "^1.0.2",
31
30
  "cors": "^2.8.5",
31
+ "dotenv": "^17.2.1",
32
32
  "express": "^5.1.0",
33
33
  "helmet": "^8.1.0",
34
34
  "morgan": "^1.10.1",
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/compression": "^1.8.1",
41
+ "@types/cookie": "^1.0.0",
41
42
  "@types/cors": "^2.8.19",
42
43
  "@types/express": "^5.0.3",
43
44
  "@types/morgan": "^1.9.10",
package/src/context.ts CHANGED
@@ -9,6 +9,7 @@ export async function createContext({ req, res }: CreateExpressContextOptions) {
9
9
 
10
10
  // Only use custom auth cookie
11
11
  const custom = verifyCustomAuthCookie(cookies["auth_token"]);
12
+
12
13
  if (custom) {
13
14
  return { db: prisma, session: { user: { id: custom.userId } } as any, req, res, cookies };
14
15
  }
package/src/lib/auth.ts CHANGED
@@ -3,26 +3,38 @@ import crypto from "node:crypto";
3
3
 
4
4
  // Custom HMAC cookie: auth_token = base64(userId).hex(hmacSHA256(base64(userId), secret))
5
5
  export function verifyCustomAuthCookie(cookieValue: string | undefined): { userId: string } | null {
6
- if (!cookieValue) return null;
6
+ if (!cookieValue) {
7
+ return null;
8
+ }
9
+
7
10
  const secret = process.env.CUSTOM_AUTH_SECRET;
8
- if (!secret) return null;
11
+
12
+ if (!secret) {
13
+ return null;
14
+ }
9
15
 
10
16
  const parts = cookieValue.split(".");
11
- if (parts.length !== 2) return null;
17
+
18
+ if (parts.length !== 2) {
19
+ return null;
20
+ }
12
21
  const [base64UserId, signatureHex] = parts;
13
22
 
14
23
  let userId: string;
15
24
  try {
16
25
  const buf = Buffer.from(base64UserId, "base64url");
17
26
  userId = buf.toString("utf8");
18
- } catch {
27
+ } catch (error) {
19
28
  return null;
20
29
  }
21
30
 
22
31
  const hmac = crypto.createHmac("sha256", secret);
23
32
  hmac.update(base64UserId);
24
33
  const expected = hmac.digest("hex");
25
- if (!timingSafeEqualHex(signatureHex, expected)) return null;
34
+
35
+ if (!timingSafeEqualHex(signatureHex, expected)) {
36
+ return null;
37
+ }
26
38
 
27
39
  return { userId };
28
40
  }
@@ -2,10 +2,14 @@ import { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
2
2
  import { router } from '../trpc.js';
3
3
  import { auth } from './auth.js';
4
4
  import { workspace } from './workspace.js';
5
+ import { flashcards } from './flashcards.js';
6
+ import { worksheets } from './worksheets.js';
5
7
 
6
8
  export const appRouter = router({
7
9
  auth,
8
- workspace
10
+ workspace,
11
+ flashcards,
12
+ worksheets,
9
13
  });
10
14
 
11
15
  // Export type for client inference
@@ -1,6 +1,22 @@
1
1
  import { z } from 'zod';
2
2
  import { router, publicProcedure, authedProcedure } from '../trpc.js';
3
3
  import bcrypt from 'bcryptjs';
4
+ import { serialize } from 'cookie';
5
+ import crypto from 'node:crypto';
6
+
7
+ // Helper to create custom auth token
8
+ function createCustomAuthToken(userId: string): string {
9
+ const secret = process.env.AUTH_SECRET;
10
+ if (!secret) {
11
+ throw new Error("AUTH_SECRET is not set");
12
+ }
13
+
14
+ const base64UserId = Buffer.from(userId, 'utf8').toString('base64url');
15
+ const hmac = crypto.createHmac('sha256', secret);
16
+ hmac.update(base64UserId);
17
+ const signature = hmac.digest('hex');
18
+ return `${base64UserId}.${signature}`;
19
+ }
4
20
 
5
21
  export const auth = router({
6
22
  signup: publicProcedure
@@ -48,39 +64,61 @@ export const auth = router({
48
64
  throw new Error("Invalid credentials");
49
65
  }
50
66
 
51
- const session = await ctx.db.session.create({
52
- data: {
53
- userId: user.id,
54
- expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
55
- },
67
+ // Create custom auth token
68
+ const authToken = createCustomAuthToken(user.id);
69
+
70
+ // Set the cookie immediately after successful login
71
+ const cookieValue = serialize("auth_token", authToken, {
72
+ httpOnly: true,
73
+ secure: process.env.NODE_ENV === "production",
74
+ sameSite: "lax",
75
+ path: "/",
76
+ maxAge: 60 * 60 * 24 * 30, // 30 days
56
77
  });
78
+
79
+ ctx.res.setHeader("Set-Cookie", cookieValue);
57
80
 
58
- return { id: session.id, session: session.id, user: { id: user.id, email: user.email, name: user.name, image: user.image } };
81
+ return {
82
+ id: user.id,
83
+ email: user.email,
84
+ name: user.name,
85
+ image: user.image
86
+ };
59
87
  }),
60
88
  getSession: publicProcedure.query(async ({ ctx }) => {
61
- const session = await ctx.db.session.findUnique({
62
- where: {
63
- id: ctx.session?.id,
64
- },
65
- });
66
-
67
- if (!session) {
68
- throw new Error("Session not found");
69
- }
70
-
71
- if (session.expires < new Date()) {
72
- throw new Error("Session expired");
89
+ // Just return the current session from context
90
+ if (!ctx.session) {
91
+ throw new Error("No session found");
73
92
  }
74
93
 
75
94
  const user = await ctx.db.user.findUnique({
76
- where: { id: session.userId },
95
+ where: { id: (ctx.session as any).user.id },
77
96
  });
78
97
 
79
98
  if (!user) {
80
99
  throw new Error("User not found");
81
100
  }
82
101
 
83
- return { id: session.id, userId: session.userId, user: { id: user.id, email: user.email, name: user.name, image: user.image } };
102
+ return {
103
+ user: {
104
+ id: user.id,
105
+ email: user.email,
106
+ name: user.name,
107
+ image: user.image
108
+ }
109
+ };
110
+ }),
111
+ logout: publicProcedure.mutation(async ({ ctx }) => {
112
+ // Clear the auth cookie
113
+ ctx.res.setHeader("Set-Cookie", serialize("auth_token", "", {
114
+ httpOnly: true,
115
+ secure: process.env.NODE_ENV === "production",
116
+ sameSite: "lax",
117
+ path: "/",
118
+ maxAge: 0, // Expire immediately
119
+ }));
120
+
121
+ return { success: true };
84
122
  }),
85
123
  });
86
124
 
@@ -0,0 +1,135 @@
1
+ import { z } from 'zod';
2
+ import { TRPCError } from '@trpc/server';
3
+ import { router, authedProcedure } from '../trpc.js';
4
+ // Prisma enum values mapped manually to avoid type import issues in ESM
5
+ const ArtifactType = {
6
+ STUDY_GUIDE: 'STUDY_GUIDE',
7
+ FLASHCARD_SET: 'FLASHCARD_SET',
8
+ WORKSHEET: 'WORKSHEET',
9
+ MEETING_SUMMARY: 'MEETING_SUMMARY',
10
+ PODCAST_EPISODE: 'PODCAST_EPISODE',
11
+ } as const;
12
+
13
+ export const flashcards = router({
14
+ listSets: authedProcedure
15
+ .input(z.object({ workspaceId: z.string().uuid() }))
16
+ .query(async ({ ctx, input }) => {
17
+ const workspace = await ctx.db.workspace.findFirst({
18
+ where: { id: input.workspaceId, ownerId: ctx.session.user.id },
19
+ });
20
+ if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
21
+ return ctx.db.artifact.findMany({
22
+ where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET },
23
+ include: {
24
+ versions: {
25
+ orderBy: { version: 'desc' },
26
+ take: 1, // Get only the latest version
27
+ },
28
+ },
29
+ orderBy: { updatedAt: 'desc' },
30
+ });
31
+ }),
32
+
33
+ createSet: authedProcedure
34
+ .input(z.object({ workspaceId: z.string().uuid(), title: z.string().min(1).max(120) }))
35
+ .mutation(async ({ ctx, input }) => {
36
+ const workspace = await ctx.db.workspace.findFirst({
37
+ where: { id: input.workspaceId, ownerId: ctx.session.user.id },
38
+ });
39
+ if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
40
+ return ctx.db.artifact.create({
41
+ data: {
42
+ workspaceId: input.workspaceId,
43
+ type: ArtifactType.FLASHCARD_SET,
44
+ title: input.title,
45
+ createdById: ctx.session.user.id,
46
+ },
47
+ });
48
+ }),
49
+
50
+ getSet: authedProcedure
51
+ .input(z.object({ setId: z.string().uuid() }))
52
+ .query(async ({ ctx, input }) => {
53
+ const set = await ctx.db.artifact.findFirst({
54
+ where: {
55
+ id: input.setId,
56
+ type: ArtifactType.FLASHCARD_SET,
57
+ workspace: { ownerId: ctx.session.user.id },
58
+ },
59
+ include: { flashcards: true },
60
+ });
61
+ if (!set) throw new TRPCError({ code: 'NOT_FOUND' });
62
+ return set;
63
+ }),
64
+
65
+ createCard: authedProcedure
66
+ .input(z.object({
67
+ setId: z.string().uuid(),
68
+ front: z.string().min(1),
69
+ back: z.string().min(1),
70
+ tags: z.array(z.string()).optional(),
71
+ order: z.number().int().optional(),
72
+ }))
73
+ .mutation(async ({ ctx, input }) => {
74
+ const set = await ctx.db.artifact.findFirst({
75
+ where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
76
+ });
77
+ if (!set) throw new TRPCError({ code: 'NOT_FOUND' });
78
+ return ctx.db.flashcard.create({
79
+ data: {
80
+ artifactId: input.setId,
81
+ front: input.front,
82
+ back: input.back,
83
+ tags: input.tags ?? [],
84
+ order: input.order ?? 0,
85
+ },
86
+ });
87
+ }),
88
+
89
+ updateCard: authedProcedure
90
+ .input(z.object({
91
+ cardId: z.string().uuid(),
92
+ front: z.string().optional(),
93
+ back: z.string().optional(),
94
+ tags: z.array(z.string()).optional(),
95
+ order: z.number().int().optional(),
96
+ }))
97
+ .mutation(async ({ ctx, input }) => {
98
+ const card = await ctx.db.flashcard.findFirst({
99
+ where: { id: input.cardId, artifact: { type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } } },
100
+ });
101
+ if (!card) throw new TRPCError({ code: 'NOT_FOUND' });
102
+ return ctx.db.flashcard.update({
103
+ where: { id: input.cardId },
104
+ data: {
105
+ front: input.front ?? card.front,
106
+ back: input.back ?? card.back,
107
+ tags: input.tags ?? card.tags,
108
+ order: input.order ?? card.order,
109
+ },
110
+ });
111
+ }),
112
+
113
+ deleteCard: authedProcedure
114
+ .input(z.object({ cardId: z.string().uuid() }))
115
+ .mutation(async ({ ctx, input }) => {
116
+ const card = await ctx.db.flashcard.findFirst({
117
+ where: { id: input.cardId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
118
+ });
119
+ if (!card) throw new TRPCError({ code: 'NOT_FOUND' });
120
+ await ctx.db.flashcard.delete({ where: { id: input.cardId } });
121
+ return true;
122
+ }),
123
+
124
+ deleteSet: authedProcedure
125
+ .input(z.object({ setId: z.string().uuid() }))
126
+ .mutation(async ({ ctx, input }) => {
127
+ const deleted = await ctx.db.artifact.deleteMany({
128
+ where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
129
+ });
130
+ if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
131
+ return true;
132
+ }),
133
+ });
134
+
135
+
File without changes
@@ -0,0 +1,149 @@
1
+ import { z } from 'zod';
2
+ import { TRPCError } from '@trpc/server';
3
+ import { router, authedProcedure } from '../trpc.js';
4
+
5
+ // Avoid importing Prisma enums directly; mirror values as string literals
6
+ const ArtifactType = {
7
+ WORKSHEET: 'WORKSHEET',
8
+ } as const;
9
+
10
+ const Difficulty = {
11
+ EASY: 'EASY',
12
+ MEDIUM: 'MEDIUM',
13
+ HARD: 'HARD',
14
+ } as const;
15
+
16
+ export const worksheets = router({
17
+ // List all worksheet artifacts for a workspace
18
+ listSets: authedProcedure
19
+ .input(z.object({ workspaceId: z.string().uuid() }))
20
+ .query(async ({ ctx, input }) => {
21
+ const workspace = await ctx.db.workspace.findFirst({
22
+ where: { id: input.workspaceId, ownerId: ctx.session.user.id },
23
+ });
24
+ if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
25
+ return ctx.db.artifact.findMany({
26
+ where: { workspaceId: input.workspaceId, type: ArtifactType.WORKSHEET },
27
+ include: {
28
+ versions: {
29
+ orderBy: { version: 'desc' },
30
+ take: 1, // Get only the latest version
31
+ },
32
+ },
33
+ orderBy: { updatedAt: 'desc' },
34
+ });
35
+ }),
36
+
37
+ // Create a worksheet set
38
+ createWorksheet: authedProcedure
39
+ .input(z.object({ workspaceId: z.string().uuid(), title: z.string().min(1).max(120) }))
40
+ .mutation(async ({ ctx, input }) => {
41
+ const workspace = await ctx.db.workspace.findFirst({
42
+ where: { id: input.workspaceId, ownerId: ctx.session.user.id },
43
+ });
44
+ if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
45
+ return ctx.db.artifact.create({
46
+ data: {
47
+ workspaceId: input.workspaceId,
48
+ type: ArtifactType.WORKSHEET,
49
+ title: input.title,
50
+ createdById: ctx.session.user.id,
51
+ },
52
+ });
53
+ }),
54
+
55
+ // Get a worksheet with its questions
56
+ getWorksheet: authedProcedure
57
+ .input(z.object({ worksheetId: z.string().uuid() }))
58
+ .query(async ({ ctx, input }) => {
59
+ const worksheet = await ctx.db.artifact.findFirst({
60
+ where: {
61
+ id: input.worksheetId,
62
+ type: ArtifactType.WORKSHEET,
63
+ workspace: { ownerId: ctx.session.user.id },
64
+ },
65
+ include: { questions: true },
66
+ });
67
+ if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
68
+ return worksheet;
69
+ }),
70
+
71
+ // Add a question to a worksheet
72
+ createWorksheetQuestion: authedProcedure
73
+ .input(z.object({
74
+ worksheetId: z.string().uuid(),
75
+ prompt: z.string().min(1),
76
+ answer: z.string().optional(),
77
+ difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
78
+ order: z.number().int().optional(),
79
+ meta: z.record(z.string(), z.unknown()).optional(),
80
+ }))
81
+ .mutation(async ({ ctx, input }) => {
82
+ const worksheet = await ctx.db.artifact.findFirst({
83
+ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
84
+ });
85
+ if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
86
+ return ctx.db.worksheetQuestion.create({
87
+ data: {
88
+ artifactId: input.worksheetId,
89
+ prompt: input.prompt,
90
+ answer: input.answer,
91
+ difficulty: (input.difficulty ?? Difficulty.MEDIUM) as any,
92
+ order: input.order ?? 0,
93
+ meta: input.meta as any,
94
+ },
95
+ });
96
+ }),
97
+
98
+ // Update a question
99
+ updateWorksheetQuestion: authedProcedure
100
+ .input(z.object({
101
+ worksheetQuestionId: z.string().uuid(),
102
+ prompt: z.string().optional(),
103
+ answer: z.string().optional(),
104
+ difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
105
+ order: z.number().int().optional(),
106
+ meta: z.record(z.string(), z.unknown()).optional(),
107
+ }))
108
+ .mutation(async ({ ctx, input }) => {
109
+ const q = await ctx.db.worksheetQuestion.findFirst({
110
+ where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } } },
111
+ });
112
+ if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
113
+ return ctx.db.worksheetQuestion.update({
114
+ where: { id: input.worksheetQuestionId },
115
+ data: {
116
+ prompt: input.prompt ?? q.prompt,
117
+ answer: input.answer ?? q.answer,
118
+ difficulty: (input.difficulty ?? q.difficulty) as any,
119
+ order: input.order ?? q.order,
120
+ meta: (input.meta ?? q.meta) as any,
121
+ },
122
+ });
123
+ }),
124
+
125
+ // Delete a question
126
+ deleteWorksheetQuestion: authedProcedure
127
+ .input(z.object({ worksheetQuestionId: z.string().uuid() }))
128
+ .mutation(async ({ ctx, input }) => {
129
+ const q = await ctx.db.worksheetQuestion.findFirst({
130
+ where: { id: input.worksheetQuestionId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
131
+ });
132
+ if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
133
+ await ctx.db.worksheetQuestion.delete({ where: { id: input.worksheetQuestionId } });
134
+ return true;
135
+ }),
136
+
137
+ // Delete a worksheet set and its questions
138
+ deleteWorksheet: authedProcedure
139
+ .input(z.object({ worksheetId: z.string().uuid() }))
140
+ .mutation(async ({ ctx, input }) => {
141
+ const deleted = await ctx.db.artifact.deleteMany({
142
+ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
143
+ });
144
+ if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
145
+ return true;
146
+ }),
147
+ });
148
+
149
+
@@ -1,16 +1,17 @@
1
1
  import { z } from 'zod';
2
+ import { TRPCError } from '@trpc/server';
2
3
  import { router, publicProcedure, authedProcedure } from '../trpc.js';
3
4
  import { bucket } from '../lib/storage.js';
4
- import { FileAsset } from '@prisma/client';
5
5
 
6
6
  export const workspace = router({
7
- // Mutation with Zod input
7
+ // List current user's workspaces
8
8
  list: authedProcedure
9
- .query(async ({ ctx, input }) => {
9
+ .query(async ({ ctx }) => {
10
10
  const workspaces = await ctx.db.workspace.findMany({
11
11
  where: {
12
- ownerId: ctx.session?.user.id,
12
+ ownerId: ctx.session.user.id,
13
13
  },
14
+ orderBy: { updatedAt: 'desc' },
14
15
  });
15
16
  return workspaces;
16
17
  }),
@@ -20,25 +21,26 @@ export const workspace = router({
20
21
  name: z.string().min(1).max(100),
21
22
  description: z.string().max(500).optional(),
22
23
  }))
23
- .mutation(({ ctx, input}) => {
24
- return ctx.db.workspace.create({
24
+ .mutation(async ({ ctx, input}) => {
25
+ const ws = await ctx.db.workspace.create({
25
26
  data: {
26
27
  title: input.name,
27
28
  description: input.description,
28
- ownerId: ctx.session?.user.id,
29
+ ownerId: ctx.session.user.id,
29
30
  },
30
31
  });
32
+ return ws;
31
33
  }),
32
34
  get: authedProcedure
33
35
  .input(z.object({
34
36
  id: z.string().uuid(),
35
37
  }))
36
- .query(({ ctx, input }) => {
37
- return ctx.db.workspace.findUnique({
38
- where: {
39
- id: input.id,
40
- },
38
+ .query(async ({ ctx, input }) => {
39
+ const ws = await ctx.db.workspace.findFirst({
40
+ where: { id: input.id, ownerId: ctx.session.user.id },
41
41
  });
42
+ if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
43
+ return ws;
42
44
  }),
43
45
  update: authedProcedure
44
46
  .input(z.object({
@@ -46,27 +48,29 @@ export const workspace = router({
46
48
  name: z.string().min(1).max(100).optional(),
47
49
  description: z.string().max(500).optional(),
48
50
  }))
49
- .mutation(({ ctx, input }) => {
50
- return ctx.db.workspace.update({
51
- where: {
52
- id: input.id,
53
- },
51
+ .mutation(async ({ ctx, input }) => {
52
+ const existed = await ctx.db.workspace.findFirst({
53
+ where: { id: input.id, ownerId: ctx.session.user.id },
54
+ });
55
+ if (!existed) throw new TRPCError({ code: 'NOT_FOUND' });
56
+ const updated = await ctx.db.workspace.update({
57
+ where: { id: input.id },
54
58
  data: {
55
- title: input.name,
59
+ title: input.name ?? existed.title,
56
60
  description: input.description,
57
61
  },
58
- });
62
+ });
63
+ return updated;
59
64
  }),
60
65
  delete: authedProcedure
61
66
  .input(z.object({
62
67
  id: z.string().uuid(),
63
68
  }))
64
- .mutation(({ ctx, input }) => {
65
- ctx.db.workspace.delete({
66
- where: {
67
- id: input.id,
68
- },
69
+ .mutation(async ({ ctx, input }) => {
70
+ const deleted = await ctx.db.workspace.deleteMany({
71
+ where: { id: input.id, ownerId: ctx.session.user.id },
69
72
  });
73
+ if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
70
74
  return true;
71
75
  }),
72
76
  uploadFiles: authedProcedure
@@ -81,6 +85,9 @@ export const workspace = router({
81
85
  ),
82
86
  }))
83
87
  .mutation(async ({ ctx, input }) => {
88
+ // ensure workspace belongs to user
89
+ const ws = await ctx.db.workspace.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
90
+ if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
84
91
  const results = [];
85
92
 
86
93
  for (const file of input.files) {
@@ -127,31 +134,32 @@ export const workspace = router({
127
134
  fileId: z.array(z.string().uuid()),
128
135
  id: z.string().uuid(),
129
136
  }))
130
- .mutation(({ ctx, input }) => {
131
- const files = ctx.db.fileAsset.findMany({
137
+ .mutation(async ({ ctx, input }) => {
138
+ // ensure files are in the user's workspace
139
+ const files = await ctx.db.fileAsset.findMany({
132
140
  where: {
133
141
  id: { in: input.fileId },
134
142
  workspaceId: input.id,
143
+ userId: ctx.session.user.id,
135
144
  },
136
145
  });
146
+ // Delete from GCS (best-effort)
147
+ for (const file of files) {
148
+ if (file.bucket && file.objectKey) {
149
+ const gcsFile: import('@google-cloud/storage').File = bucket.file(file.objectKey);
150
+ gcsFile.delete({ ignoreNotFound: true }).catch((err: unknown) => {
151
+ console.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
152
+ });
153
+ }
154
+ }
137
155
 
138
- // Delete from GCS
139
- files.then((fileRecords: FileAsset[]) => {
140
- fileRecords.forEach((file: FileAsset) => {
141
- if (file.bucket && file.objectKey) {
142
- const gcsFile: import('@google-cloud/storage').File = bucket.file(file.objectKey);
143
- gcsFile.delete({ ignoreNotFound: true }).catch((err: unknown) => {
144
- console.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
145
- });
146
- }
147
- });
148
- });
149
-
150
- return ctx.db.fileAsset.deleteMany({
156
+ await ctx.db.fileAsset.deleteMany({
151
157
  where: {
152
158
  id: { in: input.fileId },
153
159
  workspaceId: input.id,
160
+ userId: ctx.session.user.id,
154
161
  },
155
162
  });
163
+ return true;
156
164
  }),
157
165
  });
package/src/server.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import 'dotenv/config';
1
2
  import express from 'express';
2
3
  import cors from 'cors';
3
4
  import helmet from 'helmet';