@goscribe/server 1.0.6 → 1.0.8

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.
@@ -11,7 +11,7 @@ const ArtifactType = {
11
11
  };
12
12
  export const flashcards = router({
13
13
  listSets: authedProcedure
14
- .input(z.object({ workspaceId: z.string().uuid() }))
14
+ .input(z.object({ workspaceId: z.string() }))
15
15
  .query(async ({ ctx, input }) => {
16
16
  const workspace = await ctx.db.workspace.findFirst({
17
17
  where: { id: input.workspaceId, ownerId: ctx.session.user.id },
@@ -20,44 +20,32 @@ export const flashcards = router({
20
20
  throw new TRPCError({ code: 'NOT_FOUND' });
21
21
  return ctx.db.artifact.findMany({
22
22
  where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET },
23
- orderBy: { updatedAt: 'desc' },
24
- });
25
- }),
26
- createSet: authedProcedure
27
- .input(z.object({ workspaceId: z.string().uuid(), title: z.string().min(1).max(120) }))
28
- .mutation(async ({ ctx, input }) => {
29
- const workspace = await ctx.db.workspace.findFirst({
30
- where: { id: input.workspaceId, ownerId: ctx.session.user.id },
31
- });
32
- if (!workspace)
33
- throw new TRPCError({ code: 'NOT_FOUND' });
34
- return ctx.db.artifact.create({
35
- data: {
36
- workspaceId: input.workspaceId,
37
- type: ArtifactType.FLASHCARD_SET,
38
- title: input.title,
39
- createdById: ctx.session.user.id,
23
+ include: {
24
+ versions: {
25
+ orderBy: { version: 'desc' },
26
+ take: 1, // Get only the latest version
27
+ },
40
28
  },
29
+ orderBy: { updatedAt: 'desc' },
41
30
  });
42
31
  }),
43
- getSet: authedProcedure
44
- .input(z.object({ setId: z.string().uuid() }))
32
+ listCards: authedProcedure
33
+ .input(z.object({ workspaceId: z.string() }))
45
34
  .query(async ({ ctx, input }) => {
46
35
  const set = await ctx.db.artifact.findFirst({
47
- where: {
48
- id: input.setId,
49
- type: ArtifactType.FLASHCARD_SET,
50
- workspace: { ownerId: ctx.session.user.id },
36
+ where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
37
+ include: {
38
+ flashcards: true,
51
39
  },
52
- include: { flashcards: true },
40
+ orderBy: { updatedAt: 'desc' },
53
41
  });
54
42
  if (!set)
55
43
  throw new TRPCError({ code: 'NOT_FOUND' });
56
- return set;
44
+ return set.flashcards;
57
45
  }),
58
46
  createCard: authedProcedure
59
47
  .input(z.object({
60
- setId: z.string().uuid(),
48
+ workspaceId: z.string(),
61
49
  front: z.string().min(1),
62
50
  back: z.string().min(1),
63
51
  tags: z.array(z.string()).optional(),
@@ -65,13 +53,19 @@ export const flashcards = router({
65
53
  }))
66
54
  .mutation(async ({ ctx, input }) => {
67
55
  const set = await ctx.db.artifact.findFirst({
68
- where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
56
+ where: { type: ArtifactType.FLASHCARD_SET, workspace: {
57
+ id: input.workspaceId,
58
+ } },
59
+ include: {
60
+ flashcards: true,
61
+ },
62
+ orderBy: { updatedAt: 'desc' },
69
63
  });
70
64
  if (!set)
71
65
  throw new TRPCError({ code: 'NOT_FOUND' });
72
66
  return ctx.db.flashcard.create({
73
67
  data: {
74
- artifactId: input.setId,
68
+ artifactId: set.id,
75
69
  front: input.front,
76
70
  back: input.back,
77
71
  tags: input.tags ?? [],
@@ -81,7 +75,7 @@ export const flashcards = router({
81
75
  }),
82
76
  updateCard: authedProcedure
83
77
  .input(z.object({
84
- cardId: z.string().uuid(),
78
+ cardId: z.string(),
85
79
  front: z.string().optional(),
86
80
  back: z.string().optional(),
87
81
  tags: z.array(z.string()).optional(),
@@ -104,7 +98,7 @@ export const flashcards = router({
104
98
  });
105
99
  }),
106
100
  deleteCard: authedProcedure
107
- .input(z.object({ cardId: z.string().uuid() }))
101
+ .input(z.object({ cardId: z.string() }))
108
102
  .mutation(async ({ ctx, input }) => {
109
103
  const card = await ctx.db.flashcard.findFirst({
110
104
  where: { id: input.cardId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
@@ -114,14 +108,4 @@ export const flashcards = router({
114
108
  await ctx.db.flashcard.delete({ where: { id: input.cardId } });
115
109
  return true;
116
110
  }),
117
- deleteSet: authedProcedure
118
- .input(z.object({ setId: z.string().uuid() }))
119
- .mutation(async ({ ctx, input }) => {
120
- const deleted = await ctx.db.artifact.deleteMany({
121
- where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
122
- });
123
- if (deleted.count === 0)
124
- throw new TRPCError({ code: 'NOT_FOUND' });
125
- return true;
126
- }),
127
111
  });
@@ -4,17 +4,37 @@ export declare const worksheets: import("@trpc/server").TRPCBuiltRouter<{
4
4
  session: any;
5
5
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
6
6
  res: import("express").Response<any, Record<string, any>>;
7
- cookies: any;
7
+ cookies: Record<string, string | undefined>;
8
8
  };
9
9
  meta: object;
10
10
  errorShape: import("@trpc/server").TRPCDefaultErrorShape;
11
11
  transformer: true;
12
12
  }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
13
- listSets: import("@trpc/server").TRPCQueryProcedure<{
13
+ list: import("@trpc/server").TRPCQueryProcedure<{
14
14
  input: {
15
15
  workspaceId: string;
16
16
  };
17
- output: {
17
+ output: ({
18
+ versions: {
19
+ id: string;
20
+ createdAt: Date;
21
+ createdById: string | null;
22
+ artifactId: string;
23
+ content: string;
24
+ data: import("@prisma/client/runtime/library").JsonValue | null;
25
+ version: number;
26
+ }[];
27
+ questions: {
28
+ meta: import("@prisma/client/runtime/library").JsonValue | null;
29
+ id: string;
30
+ createdAt: Date;
31
+ artifactId: string;
32
+ order: number;
33
+ prompt: string;
34
+ answer: string | null;
35
+ difficulty: import("@prisma/client").$Enums.Difficulty;
36
+ }[];
37
+ } & {
18
38
  id: string;
19
39
  createdAt: Date;
20
40
  updatedAt: Date;
@@ -23,10 +43,10 @@ export declare const worksheets: import("@trpc/server").TRPCBuiltRouter<{
23
43
  type: import("@prisma/client").$Enums.ArtifactType;
24
44
  isArchived: boolean;
25
45
  createdById: string | null;
26
- }[];
46
+ })[];
27
47
  meta: object;
28
48
  }>;
29
- createSet: import("@trpc/server").TRPCMutationProcedure<{
49
+ createWorksheet: import("@trpc/server").TRPCMutationProcedure<{
30
50
  input: {
31
51
  workspaceId: string;
32
52
  title: string;
@@ -43,9 +63,9 @@ export declare const worksheets: import("@trpc/server").TRPCBuiltRouter<{
43
63
  };
44
64
  meta: object;
45
65
  }>;
46
- getSet: import("@trpc/server").TRPCQueryProcedure<{
66
+ get: import("@trpc/server").TRPCQueryProcedure<{
47
67
  input: {
48
- setId: string;
68
+ worksheetId: string;
49
69
  };
50
70
  output: {
51
71
  questions: {
@@ -70,9 +90,9 @@ export declare const worksheets: import("@trpc/server").TRPCBuiltRouter<{
70
90
  };
71
91
  meta: object;
72
92
  }>;
73
- createQuestion: import("@trpc/server").TRPCMutationProcedure<{
93
+ createWorksheetQuestion: import("@trpc/server").TRPCMutationProcedure<{
74
94
  input: {
75
- setId: string;
95
+ worksheetId: string;
76
96
  prompt: string;
77
97
  answer?: string | undefined;
78
98
  difficulty?: "EASY" | "MEDIUM" | "HARD" | undefined;
@@ -91,9 +111,9 @@ export declare const worksheets: import("@trpc/server").TRPCBuiltRouter<{
91
111
  };
92
112
  meta: object;
93
113
  }>;
94
- updateQuestion: import("@trpc/server").TRPCMutationProcedure<{
114
+ updateWorksheetQuestion: import("@trpc/server").TRPCMutationProcedure<{
95
115
  input: {
96
- questionId: string;
116
+ worksheetQuestionId: string;
97
117
  prompt?: string | undefined;
98
118
  answer?: string | undefined;
99
119
  difficulty?: "EASY" | "MEDIUM" | "HARD" | undefined;
@@ -112,16 +132,16 @@ export declare const worksheets: import("@trpc/server").TRPCBuiltRouter<{
112
132
  };
113
133
  meta: object;
114
134
  }>;
115
- deleteQuestion: import("@trpc/server").TRPCMutationProcedure<{
135
+ deleteWorksheetQuestion: import("@trpc/server").TRPCMutationProcedure<{
116
136
  input: {
117
- questionId: string;
137
+ worksheetQuestionId: string;
118
138
  };
119
139
  output: boolean;
120
140
  meta: object;
121
141
  }>;
122
- deleteSet: import("@trpc/server").TRPCMutationProcedure<{
142
+ deleteWorksheet: import("@trpc/server").TRPCMutationProcedure<{
123
143
  input: {
124
- setId: string;
144
+ worksheetId: string;
125
145
  };
126
146
  output: boolean;
127
147
  meta: object;
@@ -12,22 +12,27 @@ const Difficulty = {
12
12
  };
13
13
  export const worksheets = router({
14
14
  // List all worksheet artifacts for a workspace
15
- listSets: authedProcedure
16
- .input(z.object({ workspaceId: z.string().uuid() }))
15
+ list: authedProcedure
16
+ .input(z.object({ workspaceId: z.string() }))
17
17
  .query(async ({ ctx, input }) => {
18
- const workspace = await ctx.db.workspace.findFirst({
19
- where: { id: input.workspaceId, ownerId: ctx.session.user.id },
20
- });
21
- if (!workspace)
22
- throw new TRPCError({ code: 'NOT_FOUND' });
23
- return ctx.db.artifact.findMany({
18
+ const worksheets = await ctx.db.artifact.findMany({
24
19
  where: { workspaceId: input.workspaceId, type: ArtifactType.WORKSHEET },
20
+ include: {
21
+ versions: {
22
+ orderBy: { version: 'desc' },
23
+ take: 1, // Get only the latest version
24
+ },
25
+ questions: true,
26
+ },
25
27
  orderBy: { updatedAt: 'desc' },
26
28
  });
29
+ if (!worksheets)
30
+ throw new TRPCError({ code: 'NOT_FOUND' });
31
+ return worksheets;
27
32
  }),
28
33
  // Create a worksheet set
29
- createSet: authedProcedure
30
- .input(z.object({ workspaceId: z.string().uuid(), title: z.string().min(1).max(120) }))
34
+ createWorksheet: authedProcedure
35
+ .input(z.object({ workspaceId: z.string(), title: z.string().min(1).max(120) }))
31
36
  .mutation(async ({ ctx, input }) => {
32
37
  const workspace = await ctx.db.workspace.findFirst({
33
38
  where: { id: input.workspaceId, ownerId: ctx.session.user.id },
@@ -44,25 +49,25 @@ export const worksheets = router({
44
49
  });
45
50
  }),
46
51
  // Get a worksheet with its questions
47
- getSet: authedProcedure
48
- .input(z.object({ setId: z.string().uuid() }))
52
+ get: authedProcedure
53
+ .input(z.object({ worksheetId: z.string() }))
49
54
  .query(async ({ ctx, input }) => {
50
- const set = await ctx.db.artifact.findFirst({
55
+ const worksheet = await ctx.db.artifact.findFirst({
51
56
  where: {
52
- id: input.setId,
57
+ id: input.worksheetId,
53
58
  type: ArtifactType.WORKSHEET,
54
59
  workspace: { ownerId: ctx.session.user.id },
55
60
  },
56
61
  include: { questions: true },
57
62
  });
58
- if (!set)
63
+ if (!worksheet)
59
64
  throw new TRPCError({ code: 'NOT_FOUND' });
60
- return set;
65
+ return worksheet;
61
66
  }),
62
67
  // Add a question to a worksheet
63
- createQuestion: authedProcedure
68
+ createWorksheetQuestion: authedProcedure
64
69
  .input(z.object({
65
- setId: z.string().uuid(),
70
+ worksheetId: z.string(),
66
71
  prompt: z.string().min(1),
67
72
  answer: z.string().optional(),
68
73
  difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
@@ -70,14 +75,14 @@ export const worksheets = router({
70
75
  meta: z.record(z.string(), z.unknown()).optional(),
71
76
  }))
72
77
  .mutation(async ({ ctx, input }) => {
73
- const set = await ctx.db.artifact.findFirst({
74
- where: { id: input.setId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
78
+ const worksheet = await ctx.db.artifact.findFirst({
79
+ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
75
80
  });
76
- if (!set)
81
+ if (!worksheet)
77
82
  throw new TRPCError({ code: 'NOT_FOUND' });
78
83
  return ctx.db.worksheetQuestion.create({
79
84
  data: {
80
- artifactId: input.setId,
85
+ artifactId: input.worksheetId,
81
86
  prompt: input.prompt,
82
87
  answer: input.answer,
83
88
  difficulty: (input.difficulty ?? Difficulty.MEDIUM),
@@ -87,9 +92,9 @@ export const worksheets = router({
87
92
  });
88
93
  }),
89
94
  // Update a question
90
- updateQuestion: authedProcedure
95
+ updateWorksheetQuestion: authedProcedure
91
96
  .input(z.object({
92
- questionId: z.string().uuid(),
97
+ worksheetQuestionId: z.string(),
93
98
  prompt: z.string().optional(),
94
99
  answer: z.string().optional(),
95
100
  difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
@@ -98,12 +103,12 @@ export const worksheets = router({
98
103
  }))
99
104
  .mutation(async ({ ctx, input }) => {
100
105
  const q = await ctx.db.worksheetQuestion.findFirst({
101
- where: { id: input.questionId, artifact: { type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } } },
106
+ where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } } },
102
107
  });
103
108
  if (!q)
104
109
  throw new TRPCError({ code: 'NOT_FOUND' });
105
110
  return ctx.db.worksheetQuestion.update({
106
- where: { id: input.questionId },
111
+ where: { id: input.worksheetQuestionId },
107
112
  data: {
108
113
  prompt: input.prompt ?? q.prompt,
109
114
  answer: input.answer ?? q.answer,
@@ -114,23 +119,23 @@ export const worksheets = router({
114
119
  });
115
120
  }),
116
121
  // Delete a question
117
- deleteQuestion: authedProcedure
118
- .input(z.object({ questionId: z.string().uuid() }))
122
+ deleteWorksheetQuestion: authedProcedure
123
+ .input(z.object({ worksheetQuestionId: z.string() }))
119
124
  .mutation(async ({ ctx, input }) => {
120
125
  const q = await ctx.db.worksheetQuestion.findFirst({
121
- where: { id: input.questionId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
126
+ where: { id: input.worksheetQuestionId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
122
127
  });
123
128
  if (!q)
124
129
  throw new TRPCError({ code: 'NOT_FOUND' });
125
- await ctx.db.worksheetQuestion.delete({ where: { id: input.questionId } });
130
+ await ctx.db.worksheetQuestion.delete({ where: { id: input.worksheetQuestionId } });
126
131
  return true;
127
132
  }),
128
133
  // Delete a worksheet set and its questions
129
- deleteSet: authedProcedure
130
- .input(z.object({ setId: z.string().uuid() }))
134
+ deleteWorksheet: authedProcedure
135
+ .input(z.object({ worksheetId: z.string() }))
131
136
  .mutation(async ({ ctx, input }) => {
132
137
  const deleted = await ctx.db.artifact.deleteMany({
133
- where: { id: input.setId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
138
+ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
134
139
  });
135
140
  if (deleted.count === 0)
136
141
  throw new TRPCError({ code: 'NOT_FOUND' });
@@ -4,7 +4,7 @@ export declare const workspace: import("@trpc/server").TRPCBuiltRouter<{
4
4
  session: any;
5
5
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
6
6
  res: import("express").Response<any, Record<string, any>>;
7
- cookies: any;
7
+ cookies: Record<string, string | undefined>;
8
8
  };
9
9
  meta: object;
10
10
  errorShape: import("@trpc/server").TRPCDefaultErrorShape;
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import { router, authedProcedure } from '../trpc.js';
4
4
  import { bucket } from '../lib/storage.js';
5
+ import { ArtifactType } from '@prisma/client';
5
6
  export const workspace = router({
6
7
  // List current user's workspaces
7
8
  list: authedProcedure
@@ -25,6 +26,18 @@ export const workspace = router({
25
26
  title: input.name,
26
27
  description: input.description,
27
28
  ownerId: ctx.session.user.id,
29
+ artifacts: {
30
+ create: {
31
+ type: ArtifactType.FLASHCARD_SET,
32
+ title: "New Flashcard Set",
33
+ },
34
+ createMany: {
35
+ data: [
36
+ { type: ArtifactType.WORKSHEET, title: "Worksheet 1" },
37
+ { type: ArtifactType.WORKSHEET, title: "Worksheet 2" },
38
+ ],
39
+ },
40
+ },
28
41
  },
29
42
  });
30
43
  return ws;
package/dist/server.d.ts CHANGED
@@ -1 +1 @@
1
- export {};
1
+ import 'dotenv/config';
package/dist/server.js 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';
package/dist/trpc.d.ts CHANGED
@@ -4,7 +4,7 @@ export declare const router: import("@trpc/server").TRPCRouterBuilder<{
4
4
  session: any;
5
5
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
6
6
  res: import("express").Response<any, Record<string, any>>;
7
- cookies: any;
7
+ cookies: Record<string, string | undefined>;
8
8
  };
9
9
  meta: object;
10
10
  errorShape: import("@trpc/server").TRPCDefaultErrorShape;
@@ -15,20 +15,20 @@ export declare const middleware: <$ContextOverrides>(fn: import("@trpc/server").
15
15
  session: any;
16
16
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
17
17
  res: import("express").Response<any, Record<string, any>>;
18
- cookies: any;
18
+ cookies: Record<string, string | undefined>;
19
19
  }, object, object, $ContextOverrides, unknown>) => import("@trpc/server").TRPCMiddlewareBuilder<{
20
20
  db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
21
21
  session: any;
22
22
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
23
23
  res: import("express").Response<any, Record<string, any>>;
24
- cookies: any;
24
+ cookies: Record<string, string | undefined>;
25
25
  }, object, $ContextOverrides, unknown>;
26
26
  export declare const publicProcedure: import("@trpc/server").TRPCProcedureBuilder<{
27
27
  db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
28
28
  session: any;
29
29
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
30
30
  res: import("express").Response<any, Record<string, any>>;
31
- cookies: any;
31
+ cookies: Record<string, string | undefined>;
32
32
  }, object, object, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
33
33
  /** Exported authed procedure */
34
34
  export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilder<{
@@ -36,7 +36,7 @@ export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilde
36
36
  session: any;
37
37
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
38
38
  res: import("express").Response<any, Record<string, any>>;
39
- cookies: any;
39
+ cookies: Record<string, string | undefined>;
40
40
  }, object, {
41
41
  session: any;
42
42
  }, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goscribe/server",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,12 +22,14 @@
22
22
  "@auth/express": "^0.11.0",
23
23
  "@auth/prisma-adapter": "^2.10.0",
24
24
  "@google-cloud/storage": "^7.17.0",
25
+ "@goscribe/server": "^1.0.7",
25
26
  "@prisma/client": "^6.14.0",
26
27
  "@trpc/server": "^11.5.0",
27
28
  "bcryptjs": "^3.0.2",
28
29
  "compression": "^1.8.1",
29
30
  "cookie": "^1.0.2",
30
31
  "cors": "^2.8.5",
32
+ "dotenv": "^17.2.1",
31
33
  "express": "^5.1.0",
32
34
  "helmet": "^8.1.0",
33
35
  "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;
7
- const secret = process.env.CUSTOM_AUTH_SECRET;
8
- if (!secret) return null;
6
+ if (!cookieValue) {
7
+ return null;
8
+ }
9
+
10
+ const secret = process.env.AUTH_SECRET;
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