@goscribe/server 1.0.6 → 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.6",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,6 +28,7 @@
28
28
  "compression": "^1.8.1",
29
29
  "cookie": "^1.0.2",
30
30
  "cors": "^2.8.5",
31
+ "dotenv": "^17.2.1",
31
32
  "express": "^5.1.0",
32
33
  "helmet": "^8.1.0",
33
34
  "morgan": "^1.10.1",
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,8 +2,8 @@ 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';
6
- import { worksheets } from './worksheets';
5
+ import { flashcards } from './flashcards.js';
6
+ import { worksheets } from './worksheets.js';
7
7
 
8
8
  export const appRouter = router({
9
9
  auth,
@@ -2,6 +2,21 @@ import { z } from 'zod';
2
2
  import { router, publicProcedure, authedProcedure } from '../trpc.js';
3
3
  import bcrypt from 'bcryptjs';
4
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
+ }
5
20
 
6
21
  export const auth = router({
7
22
  signup: publicProcedure
@@ -49,47 +64,61 @@ export const auth = router({
49
64
  throw new Error("Invalid credentials");
50
65
  }
51
66
 
52
- const session = await ctx.db.session.create({
53
- data: {
54
- userId: user.id,
55
- expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
56
- },
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
57
77
  });
78
+
79
+ ctx.res.setHeader("Set-Cookie", cookieValue);
58
80
 
59
- 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
+ };
60
87
  }),
61
88
  getSession: publicProcedure.query(async ({ ctx }) => {
62
- const session = await ctx.db.session.findUnique({
63
- where: {
64
- id: ctx.session?.id,
65
- },
66
- });
67
-
68
- if (!session) {
69
- throw new Error("Session not found");
70
- }
71
-
72
- if (session.expires < new Date()) {
73
- throw new Error("Session expired");
89
+ // Just return the current session from context
90
+ if (!ctx.session) {
91
+ throw new Error("No session found");
74
92
  }
75
93
 
76
94
  const user = await ctx.db.user.findUnique({
77
- where: { id: session.userId },
95
+ where: { id: (ctx.session as any).user.id },
78
96
  });
79
97
 
80
98
  if (!user) {
81
99
  throw new Error("User not found");
82
100
  }
83
101
 
84
- ctx.res.setHeader("Set-Cookie", serialize("auth_token", session.id, {
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", "", {
85
114
  httpOnly: true,
86
115
  secure: process.env.NODE_ENV === "production",
87
- sameSite: "none", // cross-origin XHR needs None
116
+ sameSite: "lax",
88
117
  path: "/",
89
- maxAge: 60 * 60 * 24 * 7,
118
+ maxAge: 0, // Expire immediately
90
119
  }));
91
120
 
92
- return { id: session.id, userId: session.userId, user: { id: user.id, email: user.email, name: user.name, image: user.image } };
121
+ return { success: true };
93
122
  }),
94
123
  });
95
124
 
@@ -20,6 +20,12 @@ export const flashcards = router({
20
20
  if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
21
21
  return ctx.db.artifact.findMany({
22
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
+ },
23
29
  orderBy: { updatedAt: 'desc' },
24
30
  });
25
31
  }),
File without changes
@@ -24,12 +24,18 @@ export const worksheets = router({
24
24
  if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
25
25
  return ctx.db.artifact.findMany({
26
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
+ },
27
33
  orderBy: { updatedAt: 'desc' },
28
34
  });
29
35
  }),
30
36
 
31
37
  // Create a worksheet set
32
- createSet: authedProcedure
38
+ createWorksheet: authedProcedure
33
39
  .input(z.object({ workspaceId: z.string().uuid(), title: z.string().min(1).max(120) }))
34
40
  .mutation(async ({ ctx, input }) => {
35
41
  const workspace = await ctx.db.workspace.findFirst({
@@ -47,25 +53,25 @@ export const worksheets = router({
47
53
  }),
48
54
 
49
55
  // Get a worksheet with its questions
50
- getSet: authedProcedure
51
- .input(z.object({ setId: z.string().uuid() }))
56
+ getWorksheet: authedProcedure
57
+ .input(z.object({ worksheetId: z.string().uuid() }))
52
58
  .query(async ({ ctx, input }) => {
53
- const set = await ctx.db.artifact.findFirst({
59
+ const worksheet = await ctx.db.artifact.findFirst({
54
60
  where: {
55
- id: input.setId,
61
+ id: input.worksheetId,
56
62
  type: ArtifactType.WORKSHEET,
57
63
  workspace: { ownerId: ctx.session.user.id },
58
64
  },
59
65
  include: { questions: true },
60
66
  });
61
- if (!set) throw new TRPCError({ code: 'NOT_FOUND' });
62
- return set;
67
+ if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
68
+ return worksheet;
63
69
  }),
64
70
 
65
71
  // Add a question to a worksheet
66
- createQuestion: authedProcedure
72
+ createWorksheetQuestion: authedProcedure
67
73
  .input(z.object({
68
- setId: z.string().uuid(),
74
+ worksheetId: z.string().uuid(),
69
75
  prompt: z.string().min(1),
70
76
  answer: z.string().optional(),
71
77
  difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
@@ -73,13 +79,13 @@ export const worksheets = router({
73
79
  meta: z.record(z.string(), z.unknown()).optional(),
74
80
  }))
75
81
  .mutation(async ({ ctx, input }) => {
76
- const set = await ctx.db.artifact.findFirst({
77
- where: { id: input.setId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
82
+ const worksheet = await ctx.db.artifact.findFirst({
83
+ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
78
84
  });
79
- if (!set) throw new TRPCError({ code: 'NOT_FOUND' });
85
+ if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
80
86
  return ctx.db.worksheetQuestion.create({
81
87
  data: {
82
- artifactId: input.setId,
88
+ artifactId: input.worksheetId,
83
89
  prompt: input.prompt,
84
90
  answer: input.answer,
85
91
  difficulty: (input.difficulty ?? Difficulty.MEDIUM) as any,
@@ -90,9 +96,9 @@ export const worksheets = router({
90
96
  }),
91
97
 
92
98
  // Update a question
93
- updateQuestion: authedProcedure
99
+ updateWorksheetQuestion: authedProcedure
94
100
  .input(z.object({
95
- questionId: z.string().uuid(),
101
+ worksheetQuestionId: z.string().uuid(),
96
102
  prompt: z.string().optional(),
97
103
  answer: z.string().optional(),
98
104
  difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
@@ -101,11 +107,11 @@ export const worksheets = router({
101
107
  }))
102
108
  .mutation(async ({ ctx, input }) => {
103
109
  const q = await ctx.db.worksheetQuestion.findFirst({
104
- where: { id: input.questionId, artifact: { type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } } },
110
+ where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } } },
105
111
  });
106
112
  if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
107
113
  return ctx.db.worksheetQuestion.update({
108
- where: { id: input.questionId },
114
+ where: { id: input.worksheetQuestionId },
109
115
  data: {
110
116
  prompt: input.prompt ?? q.prompt,
111
117
  answer: input.answer ?? q.answer,
@@ -117,23 +123,23 @@ export const worksheets = router({
117
123
  }),
118
124
 
119
125
  // Delete a question
120
- deleteQuestion: authedProcedure
121
- .input(z.object({ questionId: z.string().uuid() }))
126
+ deleteWorksheetQuestion: authedProcedure
127
+ .input(z.object({ worksheetQuestionId: z.string().uuid() }))
122
128
  .mutation(async ({ ctx, input }) => {
123
129
  const q = await ctx.db.worksheetQuestion.findFirst({
124
- where: { id: input.questionId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
130
+ where: { id: input.worksheetQuestionId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
125
131
  });
126
132
  if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
127
- await ctx.db.worksheetQuestion.delete({ where: { id: input.questionId } });
133
+ await ctx.db.worksheetQuestion.delete({ where: { id: input.worksheetQuestionId } });
128
134
  return true;
129
135
  }),
130
136
 
131
137
  // Delete a worksheet set and its questions
132
- deleteSet: authedProcedure
133
- .input(z.object({ setId: z.string().uuid() }))
138
+ deleteWorksheet: authedProcedure
139
+ .input(z.object({ worksheetId: z.string().uuid() }))
134
140
  .mutation(async ({ ctx, input }) => {
135
141
  const deleted = await ctx.db.artifact.deleteMany({
136
- where: { id: input.setId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
142
+ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
137
143
  });
138
144
  if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
139
145
  return true;
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';