@studious-lms/server 1.4.1 → 1.4.3
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/.env.example +6 -0
- package/.env.test.example +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -76
- package/dist/index.js.map +1 -1
- package/dist/lib/config/cors.d.ts +16 -0
- package/dist/lib/config/cors.d.ts.map +1 -0
- package/dist/lib/config/cors.js +75 -0
- package/dist/lib/config/cors.js.map +1 -0
- package/dist/lib/config/env.d.ts +14 -0
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +9 -2
- package/dist/lib/config/env.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/lib/prisma.d.ts +14 -2
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +27 -8
- package/dist/lib/prisma.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/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +3 -3
- package/dist/middleware/security.js.map +1 -1
- package/dist/models/agenda.d.ts +16 -16
- package/dist/models/announcement.d.ts +59 -23
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +363 -276
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/attendance.d.ts +63 -21
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/auth.d.ts +115 -26
- 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 +115 -65
- 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/comment.d.ts +52 -16
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/conversation.d.ts +46 -16
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/event.d.ts +107 -53
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +213 -165
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +161 -77
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +73 -31
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketing.d.ts +25 -7
- package/dist/models/marketing.d.ts.map +1 -1
- package/dist/models/message.d.ts +31 -13
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +34 -10
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/notification.d.ts +29 -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/section.d.ts +71 -23
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/user.d.ts +27 -9
- package/dist/models/user.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +606 -106
- 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 +22 -2
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +133 -95
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +25 -25
- 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 +8 -8
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +12 -5
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/labChatPrompt.d.ts +27 -0
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +143 -69
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/routers/_app.d.ts +1579 -1257
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/agenda.d.ts +16 -16
- package/dist/routers/announcement.d.ts +19 -19
- package/dist/routers/assignment.d.ts +307 -291
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +3 -2
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +7 -7
- package/dist/routers/auth.d.ts +1 -1
- package/dist/routers/class.d.ts +79 -72
- 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/comment.d.ts +6 -6
- package/dist/routers/conversation.d.ts +11 -11
- package/dist/routers/event.d.ts +35 -35
- package/dist/routers/file.d.ts +12 -12
- package/dist/routers/folder.d.ts +54 -54
- package/dist/routers/labChat.d.ts +18 -12
- 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/marketing.d.ts +2 -2
- package/dist/routers/message.d.ts +33 -2
- 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 +7 -1
- 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 +13 -6
- 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/section.d.ts +7 -7
- package/dist/routers/studentProgress.d.ts +86 -0
- package/dist/routers/studentProgress.d.ts.map +1 -1
- package/dist/routers/studentProgress.js +14 -4
- package/dist/routers/studentProgress.js.map +1 -1
- package/dist/routers/user.d.ts +1 -1
- package/dist/routers/worksheet.d.ts +64 -62
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +18 -16
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/seedDatabase.d.ts +1 -1
- package/dist/services/agenda.d.ts +16 -16
- package/dist/services/announcement.d.ts +8 -8
- package/dist/services/assignment.d.ts +299 -283
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +24 -5
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +7 -7
- package/dist/services/auth.d.ts +2 -2
- 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 +74 -68
- 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/comment.d.ts +6 -6
- package/dist/services/conversation.d.ts +11 -11
- package/dist/services/event.d.ts +31 -31
- package/dist/services/file.d.ts +12 -12
- 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/folder.d.ts +52 -52
- package/dist/services/labChat.d.ts +18 -12
- 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/marketing.d.ts +2 -2
- 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 +16 -9
- 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/section.d.ts +6 -6
- package/dist/services/studentProgress.d.ts +75 -0
- package/dist/services/studentProgress.d.ts.map +1 -1
- package/dist/services/studentProgress.js +300 -107
- package/dist/services/studentProgress.js.map +1 -1
- package/dist/services/worksheet.d.ts +64 -64
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +138 -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 +9 -12
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +135 -95
- package/dist/utils/inference.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +14 -0
- 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 +41 -1
- package/scripts/test-pre-push.ts +6 -2
- package/src/index.ts +119 -79
- package/src/lib/config/cors.ts +96 -0
- package/src/lib/config/env.ts +12 -1
- package/src/lib/fileUpload.ts +55 -5
- package/src/lib/prisma.ts +25 -6
- package/src/middleware/auth.ts +19 -4
- package/src/middleware/security.ts +1 -1
- 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 +202 -0
- package/src/pipelines/aiLabChat.ts +183 -104
- package/src/pipelines/aiNewtonChat.ts +24 -14
- package/src/pipelines/gradeWorksheet.ts +10 -2
- package/src/pipelines/labChatPrompt.ts +196 -68
- package/src/routers/assignment.ts +1 -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/studentProgress.ts +25 -1
- package/src/routers/worksheet.ts +31 -19
- package/src/services/assignment.ts +30 -2
- 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 +425 -121
- package/src/services/worksheet.ts +191 -51
- package/src/trpc.ts +12 -18
- package/src/utils/inference.ts +205 -112
- package/tests/globalSetup.ts +13 -4
- package/tests/lib/cors.test.ts +103 -0
- package/tests/pipelines/aiLabChat.test.ts +64 -84
- 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/studentProgress.test.ts +2 -31
- package/tests/routers/worksheet.test.ts +194 -0
- package/tests/setup.ts +22 -5
- package/tests/utils/aiLabChatPrompt.test.ts +114 -6
- package/tests/utils/inference.test.ts +218 -0
- package/tests/utils/studentProgress.test.ts +361 -0
- package/vitest.unit.config.ts +1 -0
package/src/lib/fileUpload.ts
CHANGED
|
@@ -39,6 +39,43 @@ export interface DirectUploadFile {
|
|
|
39
39
|
uploadSessionId: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export async function validateDirectUploadIntent(input: {
|
|
43
|
+
filePath: string;
|
|
44
|
+
uploadSessionId: string;
|
|
45
|
+
userId: string;
|
|
46
|
+
}) {
|
|
47
|
+
const fileRecord = await prisma.file.findFirst({
|
|
48
|
+
where: {
|
|
49
|
+
path: input.filePath,
|
|
50
|
+
uploadSessionId: input.uploadSessionId,
|
|
51
|
+
userId: input.userId,
|
|
52
|
+
uploadExpiresAt: { gt: new Date() },
|
|
53
|
+
isOrphaned: false,
|
|
54
|
+
messageId: null,
|
|
55
|
+
},
|
|
56
|
+
select: {
|
|
57
|
+
id: true,
|
|
58
|
+
uploadStatus: true,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!fileRecord) {
|
|
63
|
+
throw new TRPCError({
|
|
64
|
+
code: 'FORBIDDEN',
|
|
65
|
+
message: 'Invalid or expired upload intent',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!['PENDING', 'UPLOADING'].includes(fileRecord.uploadStatus)) {
|
|
70
|
+
throw new TRPCError({
|
|
71
|
+
code: 'BAD_REQUEST',
|
|
72
|
+
message: 'Upload is no longer accepting data',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return fileRecord;
|
|
77
|
+
}
|
|
78
|
+
|
|
42
79
|
// DEPRECATED: These functions are no longer used - files are uploaded directly to GCS
|
|
43
80
|
// Use createDirectUploadFile() and createDirectUploadFiles() instead
|
|
44
81
|
|
|
@@ -103,7 +140,8 @@ export async function createDirectUploadFile(
|
|
|
103
140
|
directory?: string,
|
|
104
141
|
assignmentId?: string,
|
|
105
142
|
submissionId?: string,
|
|
106
|
-
announcementId?: string
|
|
143
|
+
announcementId?: string,
|
|
144
|
+
conversationId?: string
|
|
107
145
|
): Promise<DirectUploadFile> {
|
|
108
146
|
try {
|
|
109
147
|
// Validate file extension matches MIME type
|
|
@@ -137,7 +175,7 @@ export async function createDirectUploadFile(
|
|
|
137
175
|
|
|
138
176
|
// Generate backend proxy upload URL (not direct GCS)
|
|
139
177
|
const baseUrl = env.BACKEND_URL || 'http://localhost:3001';
|
|
140
|
-
const uploadUrl = `${baseUrl}/api/upload/${encodeURIComponent(filePath)}`;
|
|
178
|
+
const uploadUrl = `${baseUrl}/api/upload/${encodeURIComponent(filePath)}?uploadSessionId=${encodeURIComponent(uploadSessionId)}`;
|
|
141
179
|
const uploadExpiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes from now
|
|
142
180
|
|
|
143
181
|
// Create file record in database with PENDING status
|
|
@@ -173,6 +211,9 @@ export async function createDirectUploadFile(
|
|
|
173
211
|
announcement: {
|
|
174
212
|
connect: { id: announcementId }
|
|
175
213
|
}
|
|
214
|
+
}),
|
|
215
|
+
...(conversationId && {
|
|
216
|
+
conversationId,
|
|
176
217
|
})
|
|
177
218
|
},
|
|
178
219
|
});
|
|
@@ -330,11 +371,20 @@ export async function createDirectUploadFiles(
|
|
|
330
371
|
directory?: string,
|
|
331
372
|
assignmentId?: string,
|
|
332
373
|
submissionId?: string,
|
|
333
|
-
announcementId?: string
|
|
374
|
+
announcementId?: string,
|
|
375
|
+
conversationId?: string
|
|
334
376
|
): Promise<DirectUploadFile[]> {
|
|
335
377
|
try {
|
|
336
378
|
const uploadPromises = files.map(file =>
|
|
337
|
-
createDirectUploadFile(
|
|
379
|
+
createDirectUploadFile(
|
|
380
|
+
file,
|
|
381
|
+
userId,
|
|
382
|
+
directory,
|
|
383
|
+
assignmentId,
|
|
384
|
+
submissionId,
|
|
385
|
+
announcementId,
|
|
386
|
+
conversationId
|
|
387
|
+
)
|
|
338
388
|
);
|
|
339
389
|
return await Promise.all(uploadPromises);
|
|
340
390
|
} catch (error) {
|
|
@@ -348,4 +398,4 @@ export async function createDirectUploadFiles(
|
|
|
348
398
|
message: 'Failed to create direct upload files',
|
|
349
399
|
});
|
|
350
400
|
}
|
|
351
|
-
}
|
|
401
|
+
}
|
package/src/lib/prisma.ts
CHANGED
|
@@ -12,13 +12,32 @@ const getLogLevel = () => {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const getDatabaseUrl = () => {
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(env.DATABASE_URL);
|
|
18
|
+
// Reduce per-instance DB pressure in hosted/serverless environments.
|
|
19
|
+
if (env.NODE_ENV === 'production') {
|
|
20
|
+
if (!url.searchParams.has('connection_limit')) {
|
|
21
|
+
url.searchParams.set('connection_limit', '1');
|
|
22
|
+
}
|
|
23
|
+
if (!url.searchParams.has('pool_timeout')) {
|
|
24
|
+
url.searchParams.set('pool_timeout', '20');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return url.toString();
|
|
28
|
+
} catch {
|
|
29
|
+
return env.DATABASE_URL;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
15
33
|
const prismaClientSingleton = () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
return new PrismaClient({
|
|
35
|
+
datasources: {
|
|
36
|
+
db: {
|
|
37
|
+
url: getDatabaseUrl(),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
22
41
|
};
|
|
23
42
|
|
|
24
43
|
// Prevent multiple instances of Prisma Client in development
|
package/src/middleware/auth.ts
CHANGED
|
@@ -8,11 +8,28 @@ import {
|
|
|
8
8
|
findClassWithTeacher,
|
|
9
9
|
} from "../models/auth.js";
|
|
10
10
|
|
|
11
|
+
function getRequestToken(headers: MiddlewareContext["ctx"]["req"]["headers"]) {
|
|
12
|
+
const userHeader = headers["x-user"];
|
|
13
|
+
if (typeof userHeader === "string") {
|
|
14
|
+
return userHeader;
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(userHeader) && userHeader.length > 0) {
|
|
17
|
+
return userHeader[0];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const authorizationHeader = headers.authorization;
|
|
21
|
+
if (typeof authorizationHeader === "string" && authorizationHeader.startsWith("Bearer ")) {
|
|
22
|
+
return authorizationHeader.slice(7);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
export const createAuthMiddleware = (t: any) => {
|
|
12
29
|
const isAuthed = t.middleware(async ({ next, ctx }: MiddlewareContext) => {
|
|
13
|
-
const
|
|
30
|
+
const token = getRequestToken(ctx.req.headers);
|
|
14
31
|
|
|
15
|
-
if (!
|
|
32
|
+
if (!token) {
|
|
16
33
|
throw new TRPCError({
|
|
17
34
|
code: "UNAUTHORIZED",
|
|
18
35
|
message: "Not authenticated - no token found",
|
|
@@ -20,8 +37,6 @@ export const createAuthMiddleware = (t: any) => {
|
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
try {
|
|
23
|
-
const token =
|
|
24
|
-
typeof userHeader === "string" ? userHeader : userHeader[0];
|
|
25
40
|
const user = await findUserBySessionToken(token);
|
|
26
41
|
|
|
27
42
|
if (!user) {
|
|
@@ -19,7 +19,7 @@ const rateLimitHandler = (req: Request, res: Response) => {
|
|
|
19
19
|
|
|
20
20
|
// General API rate limiter - applies to all routes
|
|
21
21
|
export const generalLimiter = rateLimit({
|
|
22
|
-
windowMs: 10 * 60, // 10 minutes
|
|
22
|
+
windowMs: 10 * 60 * 1000, // 10 minutes
|
|
23
23
|
max: 100, // Limit each IP to 100 requests per windowMs
|
|
24
24
|
message: 'Too many requests from this IP, please try again later.',
|
|
25
25
|
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
package/src/models/auth.ts
CHANGED
|
@@ -2,9 +2,25 @@
|
|
|
2
2
|
* Auth model – user lookup, session CRUD, Redis session cache.
|
|
3
3
|
* findUserBySessionToken checks Redis first, then DB.
|
|
4
4
|
*/
|
|
5
|
+
import { SessionType } from "@prisma/client";
|
|
5
6
|
import { getRedis } from "../lib/redis.js";
|
|
6
7
|
import { prisma } from "../lib/prisma.js";
|
|
7
8
|
|
|
9
|
+
const AUTH_SESSION_CACHE_PREFIX = "auth-session:";
|
|
10
|
+
|
|
11
|
+
function getAuthSessionCacheKey(token: string) {
|
|
12
|
+
return `${AUTH_SESSION_CACHE_PREFIX}${token}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getSessionTtlSeconds(expiresAt?: Date | null) {
|
|
16
|
+
if (!expiresAt) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ttlSeconds = Math.floor((expiresAt.getTime() - Date.now()) / 1000);
|
|
21
|
+
return ttlSeconds > 0 ? ttlSeconds : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
/** @returns User by username (includes password for login). */
|
|
9
25
|
export function findUserByUsername(username: string) {
|
|
10
26
|
return prisma.user.findFirst({
|
|
@@ -102,8 +118,10 @@ export function createUser(data: {
|
|
|
102
118
|
/** Create a session. */
|
|
103
119
|
export function createSession(data: {
|
|
104
120
|
id: string;
|
|
105
|
-
|
|
121
|
+
type: SessionType;
|
|
106
122
|
expiresAt: Date;
|
|
123
|
+
userId?: string;
|
|
124
|
+
classId?: string;
|
|
107
125
|
}) {
|
|
108
126
|
return prisma.session.create({
|
|
109
127
|
data,
|
|
@@ -116,14 +134,14 @@ export async function deleteSessionsByUserId(userId: string) {
|
|
|
116
134
|
where: { userId },
|
|
117
135
|
});
|
|
118
136
|
for (const session of sessions) {
|
|
119
|
-
await getRedis()?.del(
|
|
137
|
+
await getRedis()?.del(getAuthSessionCacheKey(session.id));
|
|
120
138
|
}
|
|
121
139
|
return prisma.session.deleteMany({
|
|
122
140
|
where: { userId },
|
|
123
141
|
});
|
|
124
142
|
}
|
|
125
143
|
|
|
126
|
-
/** Delete
|
|
144
|
+
/** Delete password reset sessions expiring before date. */
|
|
127
145
|
export function deleteShortLivedSessionsByUserId(
|
|
128
146
|
userId: string,
|
|
129
147
|
expiresBefore: Date
|
|
@@ -131,7 +149,7 @@ export function deleteShortLivedSessionsByUserId(
|
|
|
131
149
|
return prisma.session.deleteMany({
|
|
132
150
|
where: {
|
|
133
151
|
userId,
|
|
134
|
-
|
|
152
|
+
type: SessionType.PASSWORD_RESET,
|
|
135
153
|
expiresAt: { lte: expiresBefore },
|
|
136
154
|
},
|
|
137
155
|
});
|
|
@@ -144,17 +162,17 @@ export function deleteUserById(id: string) {
|
|
|
144
162
|
});
|
|
145
163
|
}
|
|
146
164
|
|
|
147
|
-
/** @returns Session by ID. */
|
|
148
|
-
export function
|
|
149
|
-
return prisma.session.
|
|
150
|
-
where: { id },
|
|
165
|
+
/** @returns Session by ID and type. */
|
|
166
|
+
export function findSessionByIdAndType(id: string, type: SessionType) {
|
|
167
|
+
return prisma.session.findFirst({
|
|
168
|
+
where: { id, type },
|
|
151
169
|
});
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
/** @returns Session with user. */
|
|
155
|
-
export function
|
|
156
|
-
return prisma.session.
|
|
157
|
-
where: { id },
|
|
172
|
+
/** @returns Session with user by ID and type. */
|
|
173
|
+
export function findSessionByIdWithUserAndType(id: string, type: SessionType) {
|
|
174
|
+
return prisma.session.findFirst({
|
|
175
|
+
where: { id, type },
|
|
158
176
|
include: {
|
|
159
177
|
user: { select: { id: true } },
|
|
160
178
|
},
|
|
@@ -178,39 +196,51 @@ export function updateUserPassword(userId: string, password: string) {
|
|
|
178
196
|
}
|
|
179
197
|
|
|
180
198
|
/** Delete a session. */
|
|
181
|
-
export function deleteSessionById(id: string) {
|
|
199
|
+
export async function deleteSessionById(id: string) {
|
|
200
|
+
await getRedis()?.del(getAuthSessionCacheKey(id));
|
|
182
201
|
return prisma.session.delete({
|
|
183
202
|
where: { id },
|
|
184
203
|
});
|
|
185
204
|
}
|
|
186
205
|
|
|
187
|
-
/** @returns User from session token. Checks Redis first, then DB. Caches in Redis. */
|
|
206
|
+
/** @returns User from valid auth session token. Checks Redis first, then DB. Caches in Redis. */
|
|
188
207
|
export async function findUserBySessionToken(token: string) {
|
|
189
|
-
const
|
|
208
|
+
const cacheKey = getAuthSessionCacheKey(token);
|
|
209
|
+
const cachedUser = await getRedis()?.get(cacheKey);
|
|
190
210
|
|
|
191
|
-
if (
|
|
192
|
-
return JSON.parse(
|
|
211
|
+
if (cachedUser) {
|
|
212
|
+
return JSON.parse(cachedUser);
|
|
213
|
+
}
|
|
193
214
|
|
|
194
|
-
const
|
|
215
|
+
const session = await prisma.session.findFirst({
|
|
195
216
|
where: {
|
|
196
|
-
|
|
217
|
+
id: token,
|
|
218
|
+
type: SessionType.AUTH,
|
|
219
|
+
userId: { not: null },
|
|
220
|
+
expiresAt: { gt: new Date() },
|
|
197
221
|
},
|
|
198
222
|
select: {
|
|
199
|
-
|
|
200
|
-
|
|
223
|
+
expiresAt: true,
|
|
224
|
+
user: {
|
|
225
|
+
select: {
|
|
226
|
+
id: true,
|
|
227
|
+
username: true,
|
|
228
|
+
},
|
|
229
|
+
},
|
|
201
230
|
},
|
|
202
231
|
});
|
|
203
232
|
|
|
204
|
-
if (user) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
233
|
+
if (!session?.user) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ttlSeconds = getSessionTtlSeconds(session.expiresAt);
|
|
238
|
+
const r = getRedis();
|
|
239
|
+
if (r && ttlSeconds) {
|
|
240
|
+
await r.setex(cacheKey, ttlSeconds, JSON.stringify(session.user));
|
|
211
241
|
}
|
|
212
242
|
|
|
213
|
-
return
|
|
243
|
+
return session.user;
|
|
214
244
|
}
|
|
215
245
|
|
|
216
246
|
/** @returns Class IDs where user is a teacher. */
|
package/src/models/class.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* grading boundaries, syllabus, lab drafts, invite codes.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Assignment, Class, Folder, GradingBoundary, MarkScheme, Section, Worksheet } from "@prisma/client";
|
|
7
|
+
import { Assignment, Class, Folder, GradingBoundary, MarkScheme, Section, SessionType, Worksheet } from "@prisma/client";
|
|
8
8
|
import type { PrismaClient, WorksheetQuestion } from "@prisma/client";
|
|
9
9
|
|
|
10
10
|
import { prisma } from "../lib/prisma.js";
|
|
@@ -214,8 +214,18 @@ export function findClassWithAssignments(
|
|
|
214
214
|
? { studentId: options.userId }
|
|
215
215
|
: undefined;
|
|
216
216
|
|
|
217
|
-
return prisma.class.
|
|
218
|
-
where: {
|
|
217
|
+
return prisma.class.findFirst({
|
|
218
|
+
where: {
|
|
219
|
+
id: classId,
|
|
220
|
+
...(options.userId
|
|
221
|
+
? {
|
|
222
|
+
OR: [
|
|
223
|
+
{ students: { some: { id: options.userId } } },
|
|
224
|
+
{ teachers: { some: { id: options.userId } } },
|
|
225
|
+
],
|
|
226
|
+
}
|
|
227
|
+
: {}),
|
|
228
|
+
},
|
|
219
229
|
include: {
|
|
220
230
|
teachers: classIncludeForGet.teachers,
|
|
221
231
|
students: classIncludeForGet.students,
|
|
@@ -261,6 +271,7 @@ export function findSessionByClassCode(classCode: string) {
|
|
|
261
271
|
return prisma.session.findFirst({
|
|
262
272
|
where: {
|
|
263
273
|
id: { equals: classCode, mode: "insensitive" },
|
|
274
|
+
type: SessionType.CLASS_INVITE,
|
|
264
275
|
},
|
|
265
276
|
});
|
|
266
277
|
}
|
|
@@ -700,4 +711,4 @@ export async function createClassByImport(classId: string, userId: string, year:
|
|
|
700
711
|
}, { timeout: 30000 });
|
|
701
712
|
|
|
702
713
|
return newClassId;
|
|
703
|
-
}
|
|
714
|
+
}
|
|
@@ -18,9 +18,9 @@ export function findNotificationsByReceiverId(receiverId: string) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/** @returns Notification by ID. */
|
|
21
|
-
export function
|
|
22
|
-
return prisma.notification.
|
|
23
|
-
where: { id },
|
|
21
|
+
export function findNotificationByIdForReceiver(id: string, receiverId: string) {
|
|
22
|
+
return prisma.notification.findFirst({
|
|
23
|
+
where: { id, receiverId },
|
|
24
24
|
include: notificationInclude,
|
|
25
25
|
});
|
|
26
26
|
}
|
|
@@ -45,10 +45,16 @@ export function createManyNotifications(
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/** Mark a notification as read. */
|
|
49
|
-
export function
|
|
50
|
-
|
|
51
|
-
where: { id },
|
|
48
|
+
/** Mark a receiver's notification as read. */
|
|
49
|
+
export async function markNotificationAsReadForReceiver(id: string, receiverId: string) {
|
|
50
|
+
const result = await prisma.notification.updateMany({
|
|
51
|
+
where: { id, receiverId },
|
|
52
52
|
data: { read: true },
|
|
53
53
|
});
|
|
54
|
+
|
|
55
|
+
if (result.count === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return findNotificationByIdForReceiver(id, receiverId);
|
|
54
60
|
}
|
package/src/models/worksheet.ts
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
5
|
import type { WorksheetQuestionType, GenerationStatus } from "@prisma/client";
|
|
6
6
|
|
|
7
|
+
const worksheetClassMemberWhere = (userId: string) => ({
|
|
8
|
+
class: {
|
|
9
|
+
OR: [
|
|
10
|
+
{ teachers: { some: { id: userId } } },
|
|
11
|
+
{ students: { some: { id: userId } } },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const worksheetTeacherWhere = (userId: string) => ({
|
|
17
|
+
class: {
|
|
18
|
+
teachers: { some: { id: userId } },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
7
22
|
/** @returns Worksheet with questions and class. */
|
|
8
23
|
export function findWorksheetById(id: string) {
|
|
9
24
|
return prisma.worksheet.findUnique({
|
|
@@ -22,6 +37,34 @@ export function findWorksheetByIdMinimal(id: string) {
|
|
|
22
37
|
});
|
|
23
38
|
}
|
|
24
39
|
|
|
40
|
+
/** @returns Worksheet by ID for an authorized class member. */
|
|
41
|
+
export function findWorksheetByIdForClassMember(id: string, userId: string) {
|
|
42
|
+
return prisma.worksheet.findFirst({
|
|
43
|
+
where: {
|
|
44
|
+
id,
|
|
45
|
+
...worksheetClassMemberWhere(userId),
|
|
46
|
+
},
|
|
47
|
+
include: {
|
|
48
|
+
questions: { orderBy: { createdAt: "asc" as const } },
|
|
49
|
+
class: true,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @returns Worksheet by ID for an authorized teacher. */
|
|
55
|
+
export function findWorksheetByIdForTeacher(id: string, userId: string) {
|
|
56
|
+
return prisma.worksheet.findFirst({
|
|
57
|
+
where: {
|
|
58
|
+
id,
|
|
59
|
+
...worksheetTeacherWhere(userId),
|
|
60
|
+
},
|
|
61
|
+
include: {
|
|
62
|
+
questions: { orderBy: { createdAt: "asc" as const } },
|
|
63
|
+
class: true,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
25
68
|
/** @returns Worksheets for a class. */
|
|
26
69
|
export function findWorksheetsByClassId(classId: string) {
|
|
27
70
|
return prisma.worksheet.findMany({
|
|
@@ -33,6 +76,26 @@ export function findWorksheetsByClassId(classId: string) {
|
|
|
33
76
|
});
|
|
34
77
|
}
|
|
35
78
|
|
|
79
|
+
/** @returns Worksheet question by ID for an authorized teacher. */
|
|
80
|
+
export function findWorksheetQuestionByIdForTeacher(id: string, userId: string) {
|
|
81
|
+
return prisma.worksheetQuestion.findFirst({
|
|
82
|
+
where: {
|
|
83
|
+
id,
|
|
84
|
+
worksheet: worksheetTeacherWhere(userId),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @returns Worksheet question scoped to a worksheet. */
|
|
90
|
+
export function findWorksheetQuestionByIdInWorksheet(
|
|
91
|
+
id: string,
|
|
92
|
+
worksheetId: string
|
|
93
|
+
) {
|
|
94
|
+
return prisma.worksheetQuestion.findFirst({
|
|
95
|
+
where: { id, worksheetId },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
36
99
|
/** Create a worksheet. */
|
|
37
100
|
export function createWorksheet(data: { classId: string; name: string }) {
|
|
38
101
|
return prisma.worksheet.create({
|
|
@@ -118,6 +181,45 @@ export function findSubmissionById(id: string) {
|
|
|
118
181
|
});
|
|
119
182
|
}
|
|
120
183
|
|
|
184
|
+
/** @returns Submission if requester is the owning student or a class teacher for the worksheet. */
|
|
185
|
+
export function findSubmissionByIdForWorksheetAccess(
|
|
186
|
+
id: string,
|
|
187
|
+
worksheetId: string,
|
|
188
|
+
worksheetClassId: string,
|
|
189
|
+
userId: string
|
|
190
|
+
) {
|
|
191
|
+
return prisma.submission.findFirst({
|
|
192
|
+
where: {
|
|
193
|
+
id,
|
|
194
|
+
assignment: {
|
|
195
|
+
classId: worksheetClassId,
|
|
196
|
+
worksheets: { some: { id: worksheetId } },
|
|
197
|
+
},
|
|
198
|
+
OR: [
|
|
199
|
+
{
|
|
200
|
+
studentId: userId,
|
|
201
|
+
assignment: {
|
|
202
|
+
class: {
|
|
203
|
+
students: { some: { id: userId } },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
assignment: {
|
|
209
|
+
class: {
|
|
210
|
+
teachers: { some: { id: userId } },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
select: {
|
|
217
|
+
id: true,
|
|
218
|
+
studentId: true,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
121
223
|
const worksheetResponseInclude = {
|
|
122
224
|
responses: {
|
|
123
225
|
include: {
|
|
@@ -182,6 +284,29 @@ export function findWorksheetResponseById(id: string) {
|
|
|
182
284
|
});
|
|
183
285
|
}
|
|
184
286
|
|
|
287
|
+
/** @returns Worksheet response if requester is the owning student. */
|
|
288
|
+
export function findWorksheetResponseByIdForStudent(id: string, userId: string) {
|
|
289
|
+
return prisma.studentWorksheetResponse.findFirst({
|
|
290
|
+
where: {
|
|
291
|
+
id,
|
|
292
|
+
studentId: userId,
|
|
293
|
+
worksheet: worksheetClassMemberWhere(userId),
|
|
294
|
+
},
|
|
295
|
+
include: { responses: true, worksheet: true },
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @returns Worksheet response if requester is a class teacher. */
|
|
300
|
+
export function findWorksheetResponseByIdForTeacher(id: string, userId: string) {
|
|
301
|
+
return prisma.studentWorksheetResponse.findFirst({
|
|
302
|
+
where: {
|
|
303
|
+
id,
|
|
304
|
+
worksheet: worksheetTeacherWhere(userId),
|
|
305
|
+
},
|
|
306
|
+
include: { responses: true, worksheet: true },
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
185
310
|
/** @returns Worksheet response with question responses. */
|
|
186
311
|
export function findWorksheetResponseWithResponses(
|
|
187
312
|
id: string,
|
|
@@ -197,6 +322,43 @@ export function findWorksheetResponseWithResponses(
|
|
|
197
322
|
});
|
|
198
323
|
}
|
|
199
324
|
|
|
325
|
+
/** @returns Worksheet response with scoped responses if requester is the owning student. */
|
|
326
|
+
export function findWorksheetResponseWithResponsesForStudent(
|
|
327
|
+
id: string,
|
|
328
|
+
userId: string,
|
|
329
|
+
questionId?: string
|
|
330
|
+
) {
|
|
331
|
+
return prisma.studentWorksheetResponse.findFirst({
|
|
332
|
+
where: {
|
|
333
|
+
id,
|
|
334
|
+
studentId: userId,
|
|
335
|
+
worksheet: worksheetClassMemberWhere(userId),
|
|
336
|
+
},
|
|
337
|
+
include: {
|
|
338
|
+
worksheet: true,
|
|
339
|
+
responses: questionId ? { where: { questionId } } : true,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** @returns Worksheet response with scoped responses if requester is a class teacher. */
|
|
345
|
+
export function findWorksheetResponseWithResponsesForTeacher(
|
|
346
|
+
id: string,
|
|
347
|
+
userId: string,
|
|
348
|
+
questionId?: string
|
|
349
|
+
) {
|
|
350
|
+
return prisma.studentWorksheetResponse.findFirst({
|
|
351
|
+
where: {
|
|
352
|
+
id,
|
|
353
|
+
worksheet: worksheetTeacherWhere(userId),
|
|
354
|
+
},
|
|
355
|
+
include: {
|
|
356
|
+
worksheet: true,
|
|
357
|
+
responses: questionId ? { where: { questionId } } : true,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
200
362
|
/** @returns Worksheet question by ID. */
|
|
201
363
|
export function findWorksheetQuestionById(id: string) {
|
|
202
364
|
return prisma.worksheetQuestion.findUnique({
|
|
@@ -204,6 +366,46 @@ export function findWorksheetQuestionById(id: string) {
|
|
|
204
366
|
});
|
|
205
367
|
}
|
|
206
368
|
|
|
369
|
+
/** @returns Student question progress if requester is a class teacher. */
|
|
370
|
+
export function findStudentQuestionProgressByIdForTeacher(
|
|
371
|
+
id: string,
|
|
372
|
+
userId: string
|
|
373
|
+
) {
|
|
374
|
+
return prisma.studentQuestionProgress.findFirst({
|
|
375
|
+
where: {
|
|
376
|
+
id,
|
|
377
|
+
question: {
|
|
378
|
+
worksheet: worksheetTeacherWhere(userId),
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
include: {
|
|
382
|
+
question: true,
|
|
383
|
+
studentWorksheetResponse: true,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** @returns Student question progress by id and worksheet response if requester is a class teacher. */
|
|
389
|
+
export function findStudentQuestionProgressByIdForTeacherAndResponse(
|
|
390
|
+
id: string,
|
|
391
|
+
studentWorksheetResponseId: string,
|
|
392
|
+
userId: string
|
|
393
|
+
) {
|
|
394
|
+
return prisma.studentQuestionProgress.findFirst({
|
|
395
|
+
where: {
|
|
396
|
+
id,
|
|
397
|
+
studentWorksheetResponseId,
|
|
398
|
+
question: {
|
|
399
|
+
worksheet: worksheetTeacherWhere(userId),
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
include: {
|
|
403
|
+
question: true,
|
|
404
|
+
studentWorksheetResponse: true,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
207
409
|
/** Update student's question progress. */
|
|
208
410
|
export function updateStudentQuestionProgress(
|
|
209
411
|
id: string,
|