@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.
- package/dist/context.d.ts +1 -1
- package/dist/lib/auth.js +10 -6
- package/dist/routers/_app.d.ts +70 -74
- package/dist/routers/_app.js +2 -2
- package/dist/routers/auth.d.ts +12 -11
- package/dist/routers/auth.js +48 -21
- package/dist/routers/flashcards.d.ts +21 -46
- package/dist/routers/flashcards.js +25 -41
- package/dist/routers/worksheets.d.ts +35 -15
- package/dist/routers/worksheets.js +38 -33
- package/dist/routers/workspace.d.ts +1 -1
- package/dist/routers/workspace.js +13 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -0
- package/dist/trpc.d.ts +5 -5
- package/package.json +3 -1
- package/src/context.ts +1 -0
- package/src/lib/auth.ts +18 -6
- package/src/routers/_app.ts +2 -2
- package/src/routers/auth.ts +52 -23
- package/src/routers/flashcards.ts +25 -43
- package/src/routers/worksheets.ts +37 -32
- package/src/routers/workspace.ts +13 -0
- package/src/server.ts +1 -0
- package/dist/context.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/auth.d.ts.map +0 -1
- package/dist/lib/file.d.ts.map +0 -1
- package/dist/lib/prisma.d.ts.map +0 -1
- package/dist/lib/storage.d.ts.map +0 -1
- package/dist/routers/_app.d.ts.map +0 -1
- package/dist/routers/auth.d.ts.map +0 -1
- package/dist/routers/sample.js +0 -21
- package/dist/routers/workspace.d.ts.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/trpc.d.ts.map +0 -1
|
@@ -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()
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
44
|
-
.input(z.object({
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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:
|
|
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()
|
|
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()
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
get: import("@trpc/server").TRPCQueryProcedure<{
|
|
47
67
|
input: {
|
|
48
|
-
|
|
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
|
-
|
|
93
|
+
createWorksheetQuestion: import("@trpc/server").TRPCMutationProcedure<{
|
|
74
94
|
input: {
|
|
75
|
-
|
|
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
|
-
|
|
114
|
+
updateWorksheetQuestion: import("@trpc/server").TRPCMutationProcedure<{
|
|
95
115
|
input: {
|
|
96
|
-
|
|
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
|
-
|
|
135
|
+
deleteWorksheetQuestion: import("@trpc/server").TRPCMutationProcedure<{
|
|
116
136
|
input: {
|
|
117
|
-
|
|
137
|
+
worksheetQuestionId: string;
|
|
118
138
|
};
|
|
119
139
|
output: boolean;
|
|
120
140
|
meta: object;
|
|
121
141
|
}>;
|
|
122
|
-
|
|
142
|
+
deleteWorksheet: import("@trpc/server").TRPCMutationProcedure<{
|
|
123
143
|
input: {
|
|
124
|
-
|
|
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
|
-
|
|
16
|
-
.input(z.object({ workspaceId: z.string()
|
|
15
|
+
list: authedProcedure
|
|
16
|
+
.input(z.object({ workspaceId: z.string() }))
|
|
17
17
|
.query(async ({ ctx, input }) => {
|
|
18
|
-
const
|
|
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
|
-
|
|
30
|
-
.input(z.object({ workspaceId: z.string()
|
|
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
|
-
|
|
48
|
-
.input(z.object({
|
|
52
|
+
get: authedProcedure
|
|
53
|
+
.input(z.object({ worksheetId: z.string() }))
|
|
49
54
|
.query(async ({ ctx, input }) => {
|
|
50
|
-
const
|
|
55
|
+
const worksheet = await ctx.db.artifact.findFirst({
|
|
51
56
|
where: {
|
|
52
|
-
id: input.
|
|
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 (!
|
|
63
|
+
if (!worksheet)
|
|
59
64
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
60
|
-
return
|
|
65
|
+
return worksheet;
|
|
61
66
|
}),
|
|
62
67
|
// Add a question to a worksheet
|
|
63
|
-
|
|
68
|
+
createWorksheetQuestion: authedProcedure
|
|
64
69
|
.input(z.object({
|
|
65
|
-
|
|
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
|
|
74
|
-
where: { id: input.
|
|
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 (!
|
|
81
|
+
if (!worksheet)
|
|
77
82
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
78
83
|
return ctx.db.worksheetQuestion.create({
|
|
79
84
|
data: {
|
|
80
|
-
artifactId: input.
|
|
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
|
-
|
|
95
|
+
updateWorksheetQuestion: authedProcedure
|
|
91
96
|
.input(z.object({
|
|
92
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
118
|
-
.input(z.object({
|
|
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.
|
|
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.
|
|
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
|
-
|
|
130
|
-
.input(z.object({
|
|
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.
|
|
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:
|
|
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
|
-
|
|
1
|
+
import 'dotenv/config';
|
package/dist/server.js
CHANGED
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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)
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
+
|
|
35
|
+
if (!timingSafeEqualHex(signatureHex, expected)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
26
38
|
|
|
27
39
|
return { userId };
|
|
28
40
|
}
|
package/src/routers/_app.ts
CHANGED
|
@@ -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,
|
package/src/routers/auth.ts
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 {
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
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
|
-
|
|
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: "
|
|
116
|
+
sameSite: "lax",
|
|
88
117
|
path: "/",
|
|
89
|
-
maxAge:
|
|
118
|
+
maxAge: 0, // Expire immediately
|
|
90
119
|
}));
|
|
91
120
|
|
|
92
|
-
return {
|
|
121
|
+
return { success: true };
|
|
93
122
|
}),
|
|
94
123
|
});
|
|
95
124
|
|