@studious-lms/server 1.4.2 → 1.4.4
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/.coderabbit.yaml +4 -1
- package/.vscode/settings.json +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -29
- package/dist/index.js.map +1 -1
- package/dist/lib/fileUpload.d.ts +10 -2
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +38 -6
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +18 -5
- package/dist/middleware/auth.js.map +1 -1
- package/dist/models/auth.d.ts +19 -14
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +53 -30
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +3 -1
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +16 -4
- package/dist/models/class.js.map +1 -1
- package/dist/models/notification.d.ts +11 -10
- package/dist/models/notification.d.ts.map +1 -1
- package/dist/models/notification.js +13 -9
- package/dist/models/notification.js.map +1 -1
- package/dist/models/worksheet.d.ts +379 -7
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +168 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +11 -3
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +3 -3
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +25 -14
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +10 -3
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +12 -0
- package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -0
- package/dist/pipelines/pdfToWorksheet.js +155 -0
- package/dist/pipelines/pdfToWorksheet.js.map +1 -0
- package/dist/routers/_app.d.ts +264 -16
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/class.d.ts +2 -1
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +3 -3
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/labChat.d.ts +6 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +5 -3
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/message.d.ts +31 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +27 -4
- package/dist/routers/message.js.map +1 -1
- package/dist/routers/newtonChat.d.ts +6 -0
- package/dist/routers/newtonChat.d.ts.map +1 -1
- package/dist/routers/newtonChat.js +5 -4
- package/dist/routers/newtonChat.js.map +1 -1
- package/dist/routers/notifications.d.ts +9 -2
- package/dist/routers/notifications.d.ts.map +1 -1
- package/dist/routers/notifications.js +4 -4
- package/dist/routers/notifications.js.map +1 -1
- package/dist/routers/worksheet.d.ts +78 -5
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +126 -17
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/auth.d.ts +1 -1
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +19 -12
- package/dist/services/auth.js.map +1 -1
- package/dist/services/class.d.ts +1 -1
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +6 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +7 -2
- package/dist/services/file.js.map +1 -1
- package/dist/services/labChat.d.ts +6 -0
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +30 -18
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +30 -0
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +97 -21
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +6 -1
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +36 -18
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/notification.d.ts +12 -5
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/notification.js +24 -9
- package/dist/services/notification.js.map +1 -1
- package/dist/services/studentProgress.d.ts.map +1 -1
- package/dist/services/studentProgress.js +6 -3
- package/dist/services/studentProgress.js.map +1 -1
- package/dist/services/worksheet.d.ts +36 -15
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +186 -54
- package/dist/services/worksheet.js.map +1 -1
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +11 -17
- package/dist/trpc.js.map +1 -1
- package/dist/utils/inference.d.ts +10 -1
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +186 -50
- package/dist/utils/inference.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260415103500_add_session_type/migration.sql +33 -0
- package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +19 -0
- package/prisma/schema.prisma +27 -1
- package/scripts/test-pre-push.ts +6 -2
- package/src/index.ts +83 -30
- package/src/lib/fileUpload.ts +55 -5
- package/src/middleware/auth.ts +19 -4
- package/src/models/auth.ts +59 -29
- package/src/models/class.ts +15 -4
- package/src/models/notification.ts +13 -7
- package/src/models/worksheet.ts +203 -0
- package/src/pipelines/aiLabChat.ts +9 -1
- package/src/pipelines/aiNewtonChat.ts +24 -14
- package/src/pipelines/gradeWorksheet.ts +8 -0
- package/src/pipelines/pdfToWorksheet.ts +177 -0
- package/src/routers/class.ts +2 -2
- package/src/routers/labChat.ts +3 -1
- package/src/routers/message.ts +38 -1
- package/src/routers/newtonChat.ts +4 -2
- package/src/routers/notifications.ts +2 -2
- package/src/routers/worksheet.ts +174 -19
- package/src/services/auth.ts +26 -12
- package/src/services/class.ts +4 -1
- package/src/services/file.ts +6 -0
- package/src/services/labChat.ts +42 -18
- package/src/services/message.ts +163 -18
- package/src/services/newtonChat.ts +49 -18
- package/src/services/notification.ts +27 -8
- package/src/services/studentProgress.ts +4 -1
- package/src/services/worksheet.ts +252 -51
- package/src/trpc.ts +12 -18
- package/src/utils/inference.ts +280 -56
- package/tests/globalSetup.ts +13 -4
- package/tests/pipelines/pdfToWorksheet.test.ts +176 -0
- package/tests/routers/auth.test.ts +120 -17
- package/tests/routers/class.test.ts +7 -1
- package/tests/routers/message.test.ts +236 -9
- package/tests/routers/newtonChat.test.ts +81 -0
- package/tests/routers/notification.test.ts +12 -0
- package/tests/routers/worksheet.test.ts +194 -0
- package/tests/setup.ts +22 -5
- package/tests/utils/inference.test.ts +255 -0
package/src/routers/worksheet.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { TRPCError } from "@trpc/server";
|
|
2
|
+
import { prisma } from "../lib/prisma.js";
|
|
3
|
+
import {
|
|
4
|
+
confirmDirectUpload,
|
|
5
|
+
createDirectUploadFiles,
|
|
6
|
+
updateUploadProgress,
|
|
7
|
+
} from "../lib/fileUpload.js";
|
|
2
8
|
import {
|
|
3
9
|
createTRPCRouter,
|
|
4
10
|
protectedClassMemberProcedure,
|
|
5
11
|
protectedProcedure,
|
|
12
|
+
protectedTeacherProcedure,
|
|
6
13
|
} from "../trpc.js";
|
|
7
14
|
import { z } from "zod";
|
|
8
15
|
import {
|
|
@@ -12,6 +19,8 @@ import {
|
|
|
12
19
|
updateWorksheetRecord,
|
|
13
20
|
deleteWorksheetRecord,
|
|
14
21
|
createWorksheetRecord,
|
|
22
|
+
createWorksheetFromPdf,
|
|
23
|
+
createWorksheetFromUploadedPdf,
|
|
15
24
|
addQuestionToWorksheet,
|
|
16
25
|
reorderWorksheetQuestions,
|
|
17
26
|
updateWorksheetQuestionRecord,
|
|
@@ -24,10 +33,44 @@ import {
|
|
|
24
33
|
addCommentToResponse,
|
|
25
34
|
} from "../services/worksheet.js";
|
|
26
35
|
|
|
36
|
+
const directFileSchema = z.object({
|
|
37
|
+
name: z.string(),
|
|
38
|
+
type: z.string(),
|
|
39
|
+
size: z.number(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const pdfFileSchema = directFileSchema.superRefine((file, ctx) => {
|
|
43
|
+
const isPdf =
|
|
44
|
+
file.type === "application/pdf" && file.name.toLowerCase().endsWith(".pdf");
|
|
45
|
+
if (!isPdf) {
|
|
46
|
+
ctx.addIssue({
|
|
47
|
+
code: z.ZodIssueCode.custom,
|
|
48
|
+
message: "Only PDF files are supported",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const getWorksheetUploadUrlsSchema = z.object({
|
|
54
|
+
classId: z.string(),
|
|
55
|
+
files: z.array(pdfFileSchema),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const confirmWorksheetUploadSchema = z.object({
|
|
59
|
+
classId: z.string(),
|
|
60
|
+
fileId: z.string(),
|
|
61
|
+
uploadSuccess: z.boolean(),
|
|
62
|
+
errorMessage: z.string().optional(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const updateUploadProgressSchema = z.object({
|
|
66
|
+
fileId: z.string(),
|
|
67
|
+
progress: z.number().min(0).max(100),
|
|
68
|
+
});
|
|
69
|
+
|
|
27
70
|
export const worksheetRouter = createTRPCRouter({
|
|
28
71
|
getWorksheet: protectedProcedure
|
|
29
72
|
.input(z.object({ worksheetId: z.string() }))
|
|
30
|
-
.query(({ input }) => getWorksheet(input.worksheetId)),
|
|
73
|
+
.query(({ ctx, input }) => getWorksheet(input.worksheetId, ctx.user!.id)),
|
|
31
74
|
|
|
32
75
|
exists: protectedClassMemberProcedure
|
|
33
76
|
.input(z.object({ id: z.string() }))
|
|
@@ -41,26 +84,99 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
41
84
|
return worksheetExists(input.id);
|
|
42
85
|
}),
|
|
43
86
|
|
|
44
|
-
listWorksheets:
|
|
87
|
+
listWorksheets: protectedClassMemberProcedure
|
|
45
88
|
.input(z.object({ classId: z.string() }))
|
|
46
89
|
.query(({ input }) => listWorksheets(input.classId)),
|
|
47
90
|
|
|
48
91
|
updateWorksheet: protectedProcedure
|
|
49
92
|
.input(z.object({ worksheetId: z.string(), name: z.string().optional() }))
|
|
50
|
-
.mutation(({ input }) =>
|
|
51
|
-
updateWorksheetRecord(input.worksheetId,
|
|
93
|
+
.mutation(({ ctx, input }) =>
|
|
94
|
+
updateWorksheetRecord(input.worksheetId, ctx.user!.id, {
|
|
95
|
+
name: input.name,
|
|
96
|
+
})
|
|
52
97
|
),
|
|
53
98
|
|
|
54
99
|
deleteWorksheet: protectedProcedure
|
|
55
100
|
.input(z.object({ worksheetId: z.string() }))
|
|
56
|
-
.mutation(({ input }) =>
|
|
101
|
+
.mutation(({ ctx, input }) =>
|
|
102
|
+
deleteWorksheetRecord(input.worksheetId, ctx.user!.id)
|
|
103
|
+
),
|
|
57
104
|
|
|
58
|
-
create:
|
|
105
|
+
create: protectedTeacherProcedure
|
|
59
106
|
.input(z.object({ classId: z.string(), name: z.string() }))
|
|
60
107
|
.mutation(({ input }) =>
|
|
61
108
|
createWorksheetRecord(input.classId, input.name)
|
|
62
109
|
),
|
|
63
110
|
|
|
111
|
+
createFromPdf: protectedTeacherProcedure
|
|
112
|
+
.input(z.object({ classId: z.string(), url: z.string().url() }))
|
|
113
|
+
.mutation(({ input }) => createWorksheetFromPdf(input.classId, input.url)),
|
|
114
|
+
|
|
115
|
+
getWorksheetUploadUrls: protectedTeacherProcedure
|
|
116
|
+
.input(getWorksheetUploadUrlsSchema)
|
|
117
|
+
.mutation(async ({ ctx, input }) => {
|
|
118
|
+
if (!ctx.user) {
|
|
119
|
+
throw new TRPCError({
|
|
120
|
+
code: "UNAUTHORIZED",
|
|
121
|
+
message: "You must be logged in to upload files",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const uploadFiles = await createDirectUploadFiles(
|
|
126
|
+
input.files,
|
|
127
|
+
ctx.user.id
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
success: true,
|
|
132
|
+
uploadFiles,
|
|
133
|
+
};
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
confirmWorksheetUpload: protectedTeacherProcedure
|
|
137
|
+
.input(confirmWorksheetUploadSchema)
|
|
138
|
+
.mutation(async ({ ctx, input }) => {
|
|
139
|
+
if (!ctx.user) {
|
|
140
|
+
throw new TRPCError({
|
|
141
|
+
code: "UNAUTHORIZED",
|
|
142
|
+
message: "You must be logged in",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const file = await prisma.file.findFirst({
|
|
147
|
+
where: {
|
|
148
|
+
id: input.fileId,
|
|
149
|
+
userId: ctx.user.id,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!file) {
|
|
154
|
+
throw new TRPCError({
|
|
155
|
+
code: "NOT_FOUND",
|
|
156
|
+
message: "File not found or you don't have permission",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await confirmDirectUpload(
|
|
161
|
+
input.fileId,
|
|
162
|
+
input.uploadSuccess,
|
|
163
|
+
input.errorMessage
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
success: true,
|
|
168
|
+
message: input.uploadSuccess
|
|
169
|
+
? "Upload confirmed successfully"
|
|
170
|
+
: "Upload failed",
|
|
171
|
+
};
|
|
172
|
+
}),
|
|
173
|
+
|
|
174
|
+
createFromUploadedPdf: protectedTeacherProcedure
|
|
175
|
+
.input(z.object({ classId: z.string(), fileId: z.string() }))
|
|
176
|
+
.mutation(({ ctx, input }) =>
|
|
177
|
+
createWorksheetFromUploadedPdf(input.classId, input.fileId, ctx.user!.id)
|
|
178
|
+
),
|
|
179
|
+
|
|
64
180
|
addQuestion: protectedProcedure
|
|
65
181
|
.input(
|
|
66
182
|
z.object({
|
|
@@ -80,8 +196,8 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
80
196
|
]),
|
|
81
197
|
})
|
|
82
198
|
)
|
|
83
|
-
.mutation(({ input }) =>
|
|
84
|
-
addQuestionToWorksheet(input.worksheetId, {
|
|
199
|
+
.mutation(({ ctx, input }) =>
|
|
200
|
+
addQuestionToWorksheet(input.worksheetId, ctx.user!.id, {
|
|
85
201
|
question: input.question,
|
|
86
202
|
answer: input.answer,
|
|
87
203
|
points: input.points,
|
|
@@ -100,9 +216,10 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
100
216
|
targetId: z.string(),
|
|
101
217
|
})
|
|
102
218
|
)
|
|
103
|
-
.mutation(({ input }) =>
|
|
219
|
+
.mutation(({ ctx, input }) =>
|
|
104
220
|
reorderWorksheetQuestions(
|
|
105
221
|
input.worksheetId,
|
|
222
|
+
ctx.user!.id,
|
|
106
223
|
input.movedId,
|
|
107
224
|
input.position,
|
|
108
225
|
input.targetId
|
|
@@ -131,10 +248,11 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
131
248
|
.optional(),
|
|
132
249
|
})
|
|
133
250
|
)
|
|
134
|
-
.mutation(({ input }) =>
|
|
251
|
+
.mutation(({ ctx, input }) =>
|
|
135
252
|
updateWorksheetQuestionRecord(
|
|
136
253
|
input.worksheetId,
|
|
137
254
|
input.questionId,
|
|
255
|
+
ctx.user!.id,
|
|
138
256
|
{
|
|
139
257
|
question: input.question,
|
|
140
258
|
answer: input.answer,
|
|
@@ -153,8 +271,12 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
153
271
|
questionId: z.string(),
|
|
154
272
|
})
|
|
155
273
|
)
|
|
156
|
-
.mutation(({ input }) =>
|
|
157
|
-
deleteWorksheetQuestionRecord(
|
|
274
|
+
.mutation(({ ctx, input }) =>
|
|
275
|
+
deleteWorksheetQuestionRecord(
|
|
276
|
+
input.worksheetId,
|
|
277
|
+
input.questionId,
|
|
278
|
+
ctx.user!.id
|
|
279
|
+
)
|
|
158
280
|
),
|
|
159
281
|
|
|
160
282
|
getWorksheetSubmission: protectedProcedure
|
|
@@ -164,8 +286,8 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
164
286
|
submissionId: z.string(),
|
|
165
287
|
})
|
|
166
288
|
)
|
|
167
|
-
.query(({ input }) =>
|
|
168
|
-
getWorksheetSubmission(input.worksheetId, input.submissionId)
|
|
289
|
+
.query(({ ctx, input }) =>
|
|
290
|
+
getWorksheetSubmission(input.worksheetId, input.submissionId, ctx.user!.id)
|
|
169
291
|
),
|
|
170
292
|
|
|
171
293
|
answerQuestion: protectedProcedure
|
|
@@ -192,8 +314,8 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
192
314
|
progressId: z.string(),
|
|
193
315
|
})
|
|
194
316
|
)
|
|
195
|
-
.mutation(({ input }) =>
|
|
196
|
-
cancelGrading(input.worksheetResponseId, input.progressId)
|
|
317
|
+
.mutation(({ ctx, input }) =>
|
|
318
|
+
cancelGrading(input.worksheetResponseId, input.progressId, ctx.user!.id)
|
|
197
319
|
),
|
|
198
320
|
|
|
199
321
|
regradeQuestion: protectedProcedure
|
|
@@ -203,8 +325,8 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
203
325
|
progressId: z.string(),
|
|
204
326
|
})
|
|
205
327
|
)
|
|
206
|
-
.mutation(({ input }) =>
|
|
207
|
-
regradeQuestion(input.worksheetResponseId, input.progressId)
|
|
328
|
+
.mutation(({ ctx, input }) =>
|
|
329
|
+
regradeQuestion(input.worksheetResponseId, input.progressId, ctx.user!.id)
|
|
208
330
|
),
|
|
209
331
|
|
|
210
332
|
gradeAnswer: protectedProcedure
|
|
@@ -220,10 +342,11 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
220
342
|
points: z.number().optional(),
|
|
221
343
|
})
|
|
222
344
|
)
|
|
223
|
-
.mutation(({ input }) =>
|
|
345
|
+
.mutation(({ ctx, input }) =>
|
|
224
346
|
gradeAnswer(
|
|
225
347
|
input.questionId,
|
|
226
348
|
input.studentWorksheetResponseId,
|
|
349
|
+
ctx.user!.id,
|
|
227
350
|
{
|
|
228
351
|
responseId: input.responseId,
|
|
229
352
|
response: input.response,
|
|
@@ -249,4 +372,36 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
249
372
|
ctx.user!.id
|
|
250
373
|
)
|
|
251
374
|
),
|
|
375
|
+
|
|
376
|
+
updateUploadProgress: protectedProcedure
|
|
377
|
+
.input(updateUploadProgressSchema)
|
|
378
|
+
.mutation(async ({ ctx, input }) => {
|
|
379
|
+
if (!ctx.user) {
|
|
380
|
+
throw new TRPCError({
|
|
381
|
+
code: "UNAUTHORIZED",
|
|
382
|
+
message: "You must be logged in",
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const file = await prisma.file.findFirst({
|
|
387
|
+
where: {
|
|
388
|
+
id: input.fileId,
|
|
389
|
+
userId: ctx.user.id,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (!file) {
|
|
394
|
+
throw new TRPCError({
|
|
395
|
+
code: "NOT_FOUND",
|
|
396
|
+
message: "File not found or you don't have permission",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
await updateUploadProgress(input.fileId, input.progress);
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
success: true,
|
|
404
|
+
progress: input.progress,
|
|
405
|
+
};
|
|
406
|
+
}),
|
|
252
407
|
});
|
package/src/services/auth.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Auth service – user registration, login, logout, verification, and password reset.
|
|
3
3
|
* Handles session creation, Redis caching, and email notifications.
|
|
4
4
|
*/
|
|
5
|
+
import { SessionType } from "@prisma/client";
|
|
5
6
|
import { TRPCError } from "@trpc/server";
|
|
6
7
|
import { v4 as uuidv4 } from "uuid";
|
|
7
8
|
import { compare, hash } from "bcryptjs";
|
|
@@ -19,13 +20,12 @@ import {
|
|
|
19
20
|
deleteSessionsByUserId,
|
|
20
21
|
deleteShortLivedSessionsByUserId,
|
|
21
22
|
deleteUserById,
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
findSessionByIdAndType,
|
|
24
|
+
findSessionByIdWithUserAndType,
|
|
24
25
|
updateUserVerified,
|
|
25
26
|
updateUserPassword,
|
|
26
27
|
deleteSessionById,
|
|
27
28
|
} from "../models/auth.js";
|
|
28
|
-
import { getRedis } from "../lib/redis.js";
|
|
29
29
|
|
|
30
30
|
/** Register a new user, create verification session, and send verification email. */
|
|
31
31
|
export async function register(input: {
|
|
@@ -79,6 +79,7 @@ export async function register(input: {
|
|
|
79
79
|
() =>
|
|
80
80
|
createSession({
|
|
81
81
|
id: uuidv4(),
|
|
82
|
+
type: SessionType.EMAIL_VERIFICATION,
|
|
82
83
|
userId: user.id,
|
|
83
84
|
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
|
|
84
85
|
}),
|
|
@@ -102,7 +103,7 @@ export async function register(input: {
|
|
|
102
103
|
return { user: { id: user.id, username: user.username } };
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
/** Authenticate user by username/password, create session
|
|
106
|
+
/** Authenticate user by username/password, create session. */
|
|
106
107
|
export async function login(input: { username: string; password: string }) {
|
|
107
108
|
const { username, password } = input;
|
|
108
109
|
|
|
@@ -130,15 +131,11 @@ export async function login(input: { username: string; password: string }) {
|
|
|
130
131
|
|
|
131
132
|
const session = await createSession({
|
|
132
133
|
id: uuidv4(),
|
|
134
|
+
type: SessionType.AUTH,
|
|
133
135
|
userId: user.id,
|
|
134
136
|
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
|
|
135
137
|
});
|
|
136
138
|
|
|
137
|
-
const cachePayload = JSON.stringify({ id: user.id, username: user.username });
|
|
138
|
-
const ttlSeconds = 30 * 24 * 60 * 60; // 30 days, match session expiry
|
|
139
|
-
const r = getRedis();
|
|
140
|
-
if (r) await r.setex(`session:${session.id}`, ttlSeconds, cachePayload);
|
|
141
|
-
|
|
142
139
|
return {
|
|
143
140
|
token: session.id,
|
|
144
141
|
user: { id: user.id, username: user.username },
|
|
@@ -177,6 +174,7 @@ export async function resendVerificationEmail(email: string) {
|
|
|
177
174
|
|
|
178
175
|
const verificationToken = await createSession({
|
|
179
176
|
id: uuidv4(),
|
|
177
|
+
type: SessionType.EMAIL_VERIFICATION,
|
|
180
178
|
userId: user.id,
|
|
181
179
|
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
|
|
182
180
|
});
|
|
@@ -200,7 +198,10 @@ export async function resendVerificationEmail(email: string) {
|
|
|
200
198
|
|
|
201
199
|
/** Verify email via token. Marks user as verified and deletes the session. */
|
|
202
200
|
export async function verify(token: string) {
|
|
203
|
-
const session = await
|
|
201
|
+
const session = await findSessionByIdAndType(
|
|
202
|
+
token,
|
|
203
|
+
SessionType.EMAIL_VERIFICATION
|
|
204
|
+
);
|
|
204
205
|
if (!session) {
|
|
205
206
|
throw new TRPCError({
|
|
206
207
|
code: "NOT_FOUND",
|
|
@@ -209,13 +210,22 @@ export async function verify(token: string) {
|
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
if (session.expiresAt && session.expiresAt < new Date()) {
|
|
213
|
+
await deleteSessionById(token);
|
|
212
214
|
throw new TRPCError({
|
|
213
215
|
code: "UNAUTHORIZED",
|
|
214
216
|
message: "Session expired",
|
|
215
217
|
});
|
|
216
218
|
}
|
|
217
219
|
|
|
218
|
-
|
|
220
|
+
if (!session.userId) {
|
|
221
|
+
await deleteSessionById(token);
|
|
222
|
+
throw new TRPCError({
|
|
223
|
+
code: "BAD_REQUEST",
|
|
224
|
+
message: "Invalid session state",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await updateUserVerified(session.userId, true);
|
|
219
229
|
await deleteSessionById(token);
|
|
220
230
|
return { success: true };
|
|
221
231
|
}
|
|
@@ -232,6 +242,7 @@ export async function requestPasswordReset(email: string) {
|
|
|
232
242
|
|
|
233
243
|
const resetToken = await createSession({
|
|
234
244
|
id: uuidv4(),
|
|
245
|
+
type: SessionType.PASSWORD_RESET,
|
|
235
246
|
userId: user.id,
|
|
236
247
|
expiresAt: new Date(Date.now() + 1000 * 60 * 60),
|
|
237
248
|
});
|
|
@@ -255,7 +266,10 @@ export async function requestPasswordReset(email: string) {
|
|
|
255
266
|
|
|
256
267
|
/** Reset password using token from email. Validates token and updates password. */
|
|
257
268
|
export async function resetPassword(token: string, password: string) {
|
|
258
|
-
const session = await
|
|
269
|
+
const session = await findSessionByIdWithUserAndType(
|
|
270
|
+
token,
|
|
271
|
+
SessionType.PASSWORD_RESET
|
|
272
|
+
);
|
|
259
273
|
if (!session || !session.userId) {
|
|
260
274
|
throw new TRPCError({
|
|
261
275
|
code: "NOT_FOUND",
|
package/src/services/class.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Class service – CRUD for classes, members, grades, mark schemes, grading boundaries,
|
|
3
3
|
* syllabus, lab drafts, and invite codes.
|
|
4
4
|
*/
|
|
5
|
+
import { SessionType } from "@prisma/client";
|
|
5
6
|
import { TRPCError } from "@trpc/server";
|
|
6
7
|
import { prisma } from "../lib/prisma.js";
|
|
7
8
|
import { generateInviteCode } from "../utils/generateInviteCode.js";
|
|
@@ -286,6 +287,7 @@ export async function getInviteCode(classId: string) {
|
|
|
286
287
|
data: {
|
|
287
288
|
id: generateInviteCode(),
|
|
288
289
|
classId,
|
|
290
|
+
type: SessionType.CLASS_INVITE,
|
|
289
291
|
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
290
292
|
},
|
|
291
293
|
});
|
|
@@ -297,13 +299,14 @@ export async function getInviteCode(classId: string) {
|
|
|
297
299
|
|
|
298
300
|
export async function createInviteCode(classId: string) {
|
|
299
301
|
await prisma.session.deleteMany({
|
|
300
|
-
where: { classId },
|
|
302
|
+
where: { classId, type: SessionType.CLASS_INVITE },
|
|
301
303
|
});
|
|
302
304
|
|
|
303
305
|
const session = await prisma.session.create({
|
|
304
306
|
data: {
|
|
305
307
|
id: generateInviteCode(),
|
|
306
308
|
classId,
|
|
309
|
+
type: SessionType.CLASS_INVITE,
|
|
307
310
|
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
308
311
|
},
|
|
309
312
|
});
|
package/src/services/file.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
updateFileName,
|
|
15
15
|
deleteFileById,
|
|
16
16
|
} from "../models/file.js";
|
|
17
|
+
import { findConversationMembership } from "../models/message.js";
|
|
17
18
|
|
|
18
19
|
/** Generate a signed URL for file download. Validates user has access. */
|
|
19
20
|
export async function getFileSignedUrl(userId: string, fileId: string) {
|
|
@@ -65,6 +66,11 @@ export async function getFileSignedUrl(userId: string, fileId: string) {
|
|
|
65
66
|
hasAccess = true;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
if (file.conversationId) {
|
|
70
|
+
const membership = await findConversationMembership(file.conversationId, userId);
|
|
71
|
+
hasAccess = hasAccess || Boolean(membership);
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
if (file.folder?.class) {
|
|
69
75
|
const isTeacher = file.folder.class.teachers.some((t) => t.id === userId);
|
|
70
76
|
const isStudent = file.folder.class.students.some((s) => s.id === userId);
|
package/src/services/labChat.ts
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
import { generateAndSendLabIntroduction, generateAndSendLabResponse } from "../pipelines/aiLabChat.js";
|
|
21
21
|
import { isAIUser } from "../utils/aiUser.js";
|
|
22
22
|
import { logger } from "../utils/logger.js";
|
|
23
|
+
import {
|
|
24
|
+
claimMessageAttachments,
|
|
25
|
+
ensureMessageHasContentOrAttachments,
|
|
26
|
+
} from "./message.js";
|
|
23
27
|
|
|
24
28
|
/** Create a lab chat with conversation and teacher members. Broadcasts via Pusher. */
|
|
25
29
|
export async function createLabChat(
|
|
@@ -213,9 +217,10 @@ export async function postToLabChat(
|
|
|
213
217
|
labChatId: string;
|
|
214
218
|
content: string;
|
|
215
219
|
mentionedUserIds?: string[];
|
|
220
|
+
fileIds?: string[];
|
|
216
221
|
}
|
|
217
222
|
) {
|
|
218
|
-
const { labChatId, content, mentionedUserIds = [] } = input;
|
|
223
|
+
const { labChatId, content, mentionedUserIds = [], fileIds = [] } = input;
|
|
219
224
|
|
|
220
225
|
const labChat = await findLabChatForPost(labChatId, userId);
|
|
221
226
|
if (!labChat) {
|
|
@@ -238,30 +243,27 @@ export async function postToLabChat(
|
|
|
238
243
|
}
|
|
239
244
|
}
|
|
240
245
|
|
|
246
|
+
ensureMessageHasContentOrAttachments(content, fileIds);
|
|
247
|
+
const normalizedContent = content.trim();
|
|
248
|
+
|
|
241
249
|
const result = await prisma.$transaction(async (tx) => {
|
|
242
250
|
const message = await tx.message.create({
|
|
243
251
|
data: {
|
|
244
|
-
content,
|
|
252
|
+
content: normalizedContent,
|
|
245
253
|
senderId: userId,
|
|
246
254
|
conversationId: labChat.conversationId,
|
|
247
255
|
...(isAIUser(userId) ? {} : { status: GenerationStatus.PENDING }),
|
|
248
256
|
},
|
|
249
|
-
include: {
|
|
250
|
-
sender: {
|
|
251
|
-
select: {
|
|
252
|
-
id: true,
|
|
253
|
-
username: true,
|
|
254
|
-
profile: {
|
|
255
|
-
select: {
|
|
256
|
-
displayName: true,
|
|
257
|
-
profilePicture: true,
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
257
|
});
|
|
264
258
|
|
|
259
|
+
await claimMessageAttachments(
|
|
260
|
+
tx,
|
|
261
|
+
userId,
|
|
262
|
+
labChat.conversationId,
|
|
263
|
+
message.id,
|
|
264
|
+
fileIds
|
|
265
|
+
);
|
|
266
|
+
|
|
265
267
|
if (mentionedUserIds.length > 0) {
|
|
266
268
|
await tx.mention.createMany({
|
|
267
269
|
data: mentionedUserIds.map((mentionedUserId) => ({
|
|
@@ -276,7 +278,26 @@ export async function postToLabChat(
|
|
|
276
278
|
data: { updatedAt: new Date() },
|
|
277
279
|
});
|
|
278
280
|
|
|
279
|
-
return message
|
|
281
|
+
return tx.message.findUniqueOrThrow({
|
|
282
|
+
where: { id: message.id },
|
|
283
|
+
include: {
|
|
284
|
+
sender: {
|
|
285
|
+
select: {
|
|
286
|
+
id: true,
|
|
287
|
+
username: true,
|
|
288
|
+
profile: {
|
|
289
|
+
select: {
|
|
290
|
+
displayName: true,
|
|
291
|
+
profilePicture: true,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
attachments: {
|
|
297
|
+
select: { id: true, name: true, type: true },
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
});
|
|
280
301
|
});
|
|
281
302
|
|
|
282
303
|
try {
|
|
@@ -290,7 +311,9 @@ export async function postToLabChat(
|
|
|
290
311
|
conversationId: result.conversationId,
|
|
291
312
|
createdAt: result.createdAt,
|
|
292
313
|
sender: result.sender,
|
|
314
|
+
attachments: result.attachments ?? [],
|
|
293
315
|
mentionedUserIds,
|
|
316
|
+
meta: {},
|
|
294
317
|
}
|
|
295
318
|
);
|
|
296
319
|
} catch (error) {
|
|
@@ -307,7 +330,7 @@ export async function postToLabChat(
|
|
|
307
330
|
} catch (error) {
|
|
308
331
|
console.error("Failed to broadcast lab response pending:", error);
|
|
309
332
|
}
|
|
310
|
-
generateAndSendLabResponse(labChatId,
|
|
333
|
+
generateAndSendLabResponse(labChatId, normalizedContent, {
|
|
311
334
|
classId: labChat.classId,
|
|
312
335
|
messageId: result.id,
|
|
313
336
|
}).catch((error) =>
|
|
@@ -327,6 +350,7 @@ export async function postToLabChat(
|
|
|
327
350
|
conversationId: result.conversationId,
|
|
328
351
|
createdAt: result.createdAt,
|
|
329
352
|
sender: result.sender,
|
|
353
|
+
attachments: result.attachments ?? [],
|
|
330
354
|
mentionedUserIds,
|
|
331
355
|
};
|
|
332
356
|
}
|