@studious-lms/server 1.2.45 → 1.2.47
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/.env.example +45 -0
- package/.env.test.example +37 -0
- package/README.md +34 -7
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +12110 -0
- package/coverage/coverage-final.json +44 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +221 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/server/index.html +116 -0
- package/coverage/server/src/exportType.ts.html +109 -0
- package/coverage/server/src/index.html +161 -0
- package/coverage/server/src/index.ts.html +1702 -0
- package/coverage/server/src/instrument.ts.html +130 -0
- package/coverage/server/src/lib/config/env.ts.html +448 -0
- package/coverage/server/src/lib/config/index.html +116 -0
- package/coverage/server/src/lib/fileUpload.ts.html +1138 -0
- package/coverage/server/src/lib/googleCloudStorage.ts.html +334 -0
- package/coverage/server/src/lib/index.html +206 -0
- package/coverage/server/src/lib/jsonConversion.ts.html +2323 -0
- package/coverage/server/src/lib/jsonStyles.ts.html +193 -0
- package/coverage/server/src/lib/notificationHandler.ts.html +193 -0
- package/coverage/server/src/lib/pusher.ts.html +121 -0
- package/coverage/server/src/lib/thumbnailGenerator.ts.html +592 -0
- package/coverage/server/src/middleware/auth.ts.html +646 -0
- package/coverage/server/src/middleware/index.html +146 -0
- package/coverage/server/src/middleware/logging.ts.html +244 -0
- package/coverage/server/src/middleware/security.ts.html +271 -0
- package/coverage/server/src/routers/_app.ts.html +232 -0
- package/coverage/server/src/routers/agenda.ts.html +319 -0
- package/coverage/server/src/routers/announcement.ts.html +3481 -0
- package/coverage/server/src/routers/assignment.ts.html +7633 -0
- package/coverage/server/src/routers/attendance.ts.html +1030 -0
- package/coverage/server/src/routers/auth.ts.html +1081 -0
- package/coverage/server/src/routers/class.ts.html +3535 -0
- package/coverage/server/src/routers/comment.ts.html +991 -0
- package/coverage/server/src/routers/conversation.ts.html +982 -0
- package/coverage/server/src/routers/event.ts.html +1609 -0
- package/coverage/server/src/routers/file.ts.html +1144 -0
- package/coverage/server/src/routers/folder.ts.html +2797 -0
- package/coverage/server/src/routers/index.html +386 -0
- package/coverage/server/src/routers/labChat.ts.html +3073 -0
- package/coverage/server/src/routers/marketing.ts.html +340 -0
- package/coverage/server/src/routers/message.ts.html +1912 -0
- package/coverage/server/src/routers/notifications.ts.html +364 -0
- package/coverage/server/src/routers/section.ts.html +1120 -0
- package/coverage/server/src/routers/user.ts.html +862 -0
- package/coverage/server/src/routers/worksheet.ts.html +1729 -0
- package/coverage/server/src/trpc.ts.html +397 -0
- package/coverage/server/src/types/index.html +116 -0
- package/coverage/server/src/types/trpc.ts.html +127 -0
- package/coverage/server/src/utils/aiUser.ts.html +280 -0
- package/coverage/server/src/utils/email.ts.html +121 -0
- package/coverage/server/src/utils/generateInviteCode.ts.html +106 -0
- package/coverage/server/src/utils/index.html +206 -0
- package/coverage/server/src/utils/inference.ts.html +709 -0
- package/coverage/server/src/utils/logger.ts.html +664 -0
- package/coverage/server/src/utils/prismaErrorHandler.ts.html +907 -0
- package/coverage/server/src/utils/prismaWrapper.ts.html +355 -0
- package/coverage/server/vitest.config.ts.html +196 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +83 -52
- package/dist/index.js.map +1 -1
- package/dist/instrument.js +15 -8
- package/dist/instrument.js.map +1 -1
- package/dist/lib/config/env.d.ts +169 -0
- package/dist/lib/config/env.d.ts.map +1 -0
- package/dist/lib/config/env.js +115 -0
- package/dist/lib/config/env.js.map +1 -0
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +5 -4
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +7 -8
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/jsonConversion.d.ts.map +1 -1
- package/dist/lib/jsonConversion.js +14 -16
- package/dist/lib/jsonConversion.js.map +1 -1
- package/dist/lib/notificationHandler.d.ts +2 -2
- package/dist/lib/prisma.d.ts +2 -2
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +22 -3
- package/dist/lib/prisma.js.map +1 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +8 -7
- package/dist/lib/pusher.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +7 -5
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/security.d.ts +5 -0
- package/dist/middleware/security.d.ts.map +1 -0
- package/dist/middleware/security.js +77 -0
- package/dist/middleware/security.js.map +1 -0
- package/dist/routers/_app.d.ts +368 -108
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +12 -9
- package/dist/routers/agenda.js.map +1 -1
- package/dist/routers/announcement.d.ts +8 -0
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +6 -4
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +17 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +51 -19
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +1 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +4 -4
- package/dist/routers/attendance.js.map +1 -1
- package/dist/routers/auth.d.ts +20 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +132 -15
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +10 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +49 -5
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/comment.d.ts +2 -0
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/conversation.d.ts +2 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +46 -31
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +30 -7
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/labChat.d.ts +2 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +5 -322
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/marketing.d.ts +1 -1
- package/dist/routers/message.d.ts +1 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +3 -2
- package/dist/routers/message.js.map +1 -1
- package/dist/routers/newtonChat.d.ts +55 -0
- package/dist/routers/newtonChat.d.ts.map +1 -0
- package/dist/routers/newtonChat.js +262 -0
- package/dist/routers/newtonChat.js.map +1 -0
- package/dist/routers/notifications.d.ts +4 -4
- package/dist/routers/section.d.ts +19 -4
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +26 -8
- package/dist/routers/section.js.map +1 -1
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +5 -4
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +44 -41
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +25 -34
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/seedDatabase.d.ts +1 -1
- package/dist/seedDatabase.js +275 -284
- package/dist/seedDatabase.js.map +1 -1
- package/dist/server/pipelines/aiLabChat.d.ts +21 -0
- package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiLabChat.js +456 -0
- package/dist/server/pipelines/aiLabChat.js.map +1 -0
- package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
- package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiNewtonChat.js +280 -0
- package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts +15 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.js +139 -0
- package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +2 -2
- package/dist/trpc.js.map +1 -1
- package/dist/utils/email.d.ts +9 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +20 -5
- package/dist/utils/email.js.map +1 -1
- package/dist/utils/inference.d.ts +5 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +71 -7
- package/dist/utils/inference.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +3 -3
- package/dist/utils/logger.js.map +1 -1
- package/docker-compose.yml +14 -0
- package/package.json +13 -4
- package/prisma/schema.prisma +34 -5
- package/scripts/test-pre-push.ts +14 -0
- package/src/index.ts +98 -54
- package/src/instrument.ts +13 -6
- package/src/lib/config/env.ts +126 -0
- package/src/lib/fileUpload.ts +3 -2
- package/src/lib/googleCloudStorage.ts +6 -6
- package/src/lib/jsonConversion.ts +12 -14
- package/src/lib/prisma.ts +23 -2
- package/src/lib/pusher.ts +6 -5
- package/src/middleware/auth.ts +5 -3
- package/src/middleware/security.ts +80 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/agenda.ts +10 -7
- package/src/routers/announcement.ts +4 -2
- package/src/routers/assignment.ts +74 -41
- package/src/routers/attendance.ts +2 -2
- package/src/routers/auth.ts +143 -14
- package/src/routers/class.ts +52 -3
- package/src/routers/conversation.ts +49 -29
- package/src/routers/file.ts +29 -5
- package/src/routers/labChat.ts +3 -367
- package/src/routers/message.ts +1 -1
- package/src/routers/newtonChat.ts +299 -0
- package/src/routers/section.ts +26 -6
- package/src/routers/user.ts +3 -2
- package/src/routers/worksheet.ts +26 -38
- package/src/seedDatabase.ts +290 -283
- package/src/server/pipelines/aiLabChat.ts +507 -0
- package/src/server/pipelines/aiNewtonChat.ts +338 -0
- package/src/server/pipelines/gradeWorksheet.ts +151 -0
- package/src/trpc.ts +2 -0
- package/src/utils/email.ts +30 -3
- package/src/utils/inference.ts +85 -5
- package/src/utils/logger.ts +2 -1
- package/tests/announcement.test.ts +164 -0
- package/tests/assignment.test.ts +296 -0
- package/tests/attendance.test.ts +168 -0
- package/tests/auth.test.ts +33 -10
- package/tests/class.test.ts +34 -9
- package/tests/event.test.ts +228 -0
- package/tests/section.test.ts +216 -0
- package/tests/setup.ts +70 -16
- package/tests/user.test.ts +158 -0
- package/vitest.config.ts +26 -0
- package/API_SPECIFICATION.md +0 -1597
- package/BASE64_REMOVAL_SUMMARY.md +0 -164
- package/CHAT_API_SPEC.md +0 -579
- package/LAB_CHAT_API_SPEC.md +0 -518
- package/dist/routers/school.d.ts +0 -208
- package/dist/routers/school.d.ts.map +0 -1
- package/dist/routers/school.js +0 -483
package/src/routers/_app.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { notificationRouter } from "./notifications.js";
|
|
|
16
16
|
import { conversationRouter } from "./conversation.js";
|
|
17
17
|
import { messageRouter } from "./message.js";
|
|
18
18
|
import { labChatRouter } from "./labChat.js";
|
|
19
|
+
import { newtonChatRouter } from "./newtonChat.js";
|
|
19
20
|
import { marketingRouter } from "./marketing.js";
|
|
20
21
|
import { worksheetRouter } from "./worksheet.js";
|
|
21
22
|
import { commentRouter } from "./comment.js";
|
|
@@ -36,6 +37,7 @@ export const appRouter = createTRPCRouter({
|
|
|
36
37
|
conversation: conversationRouter,
|
|
37
38
|
message: messageRouter,
|
|
38
39
|
labChat: labChatRouter,
|
|
40
|
+
newtonChat: newtonChatRouter,
|
|
39
41
|
marketing: marketingRouter,
|
|
40
42
|
worksheet: worksheetRouter,
|
|
41
43
|
comment: commentRouter,
|
package/src/routers/agenda.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
3
3
|
import { prisma } from "../lib/prisma.js";
|
|
4
4
|
import { TRPCError } from "@trpc/server";
|
|
5
|
-
import { addDays, startOfDay, endOfDay } from "date-fns";
|
|
5
|
+
import { addDays, addMonths, subMonths, startOfDay, endOfDay } from "date-fns";
|
|
6
6
|
|
|
7
7
|
export const agendaRouter = createTRPCRouter({
|
|
8
8
|
get: protectedProcedure
|
|
@@ -17,8 +17,11 @@ export const agendaRouter = createTRPCRouter({
|
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Expand query range to 6 months (3 months before and after the reference date)
|
|
21
|
+
// to allow calendar navigation and ensure newly created events are visible
|
|
22
|
+
const referenceDate = new Date(input.weekStart);
|
|
23
|
+
const rangeStart = startOfDay(subMonths(referenceDate, 3));
|
|
24
|
+
const rangeEnd = endOfDay(addMonths(referenceDate, 3));
|
|
22
25
|
|
|
23
26
|
const [personalEvents, classEvents] = await Promise.all([
|
|
24
27
|
// Get personal events
|
|
@@ -26,8 +29,8 @@ export const agendaRouter = createTRPCRouter({
|
|
|
26
29
|
where: {
|
|
27
30
|
userId: ctx.user.id,
|
|
28
31
|
startTime: {
|
|
29
|
-
gte:
|
|
30
|
-
lte:
|
|
32
|
+
gte: rangeStart,
|
|
33
|
+
lte: rangeEnd,
|
|
31
34
|
},
|
|
32
35
|
class: {
|
|
33
36
|
is: null,
|
|
@@ -59,8 +62,8 @@ export const agendaRouter = createTRPCRouter({
|
|
|
59
62
|
],
|
|
60
63
|
},
|
|
61
64
|
startTime: {
|
|
62
|
-
gte:
|
|
63
|
-
lte:
|
|
65
|
+
gte: rangeStart,
|
|
66
|
+
lte: rangeEnd,
|
|
64
67
|
},
|
|
65
68
|
},
|
|
66
69
|
include: {
|
|
@@ -206,9 +206,10 @@ export const announcementRouter = createTRPCRouter({
|
|
|
206
206
|
};
|
|
207
207
|
}),
|
|
208
208
|
|
|
209
|
-
update:
|
|
209
|
+
update: protectedTeacherProcedure
|
|
210
210
|
.input(z.object({
|
|
211
211
|
id: z.string(),
|
|
212
|
+
classId: z.string(),
|
|
212
213
|
data: z.object({
|
|
213
214
|
remarks: z.string().min(1, "Remarks cannot be empty").optional(),
|
|
214
215
|
files: z.array(directFileSchema).optional(),
|
|
@@ -339,9 +340,10 @@ export const announcementRouter = createTRPCRouter({
|
|
|
339
340
|
};
|
|
340
341
|
}),
|
|
341
342
|
|
|
342
|
-
delete:
|
|
343
|
+
delete: protectedTeacherProcedure
|
|
343
344
|
.input(z.object({
|
|
344
345
|
id: z.string(),
|
|
346
|
+
classId: z.string(),
|
|
345
347
|
}))
|
|
346
348
|
.mutation(async ({ ctx, input }) => {
|
|
347
349
|
if (!ctx.user) {
|
|
@@ -6,6 +6,7 @@ import { createDirectUploadFiles, type DirectUploadFile, confirmDirectUpload, up
|
|
|
6
6
|
import { deleteFile } from "../lib/googleCloudStorage.js";
|
|
7
7
|
import { sendNotifications } from "../lib/notificationHandler.js";
|
|
8
8
|
import { logger } from "../utils/logger.js";
|
|
9
|
+
import { gradeWorksheetPipeline } from "../server/pipelines/gradeWorksheet.js";
|
|
9
10
|
|
|
10
11
|
// DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
|
|
11
12
|
// Use directFileSchema instead
|
|
@@ -20,6 +21,7 @@ const directFileSchema = z.object({
|
|
|
20
21
|
|
|
21
22
|
const createAssignmentSchema = z.object({
|
|
22
23
|
classId: z.string(),
|
|
24
|
+
id: z.string().optional(),
|
|
23
25
|
title: z.string(),
|
|
24
26
|
instructions: z.string(),
|
|
25
27
|
dueDate: z.string(),
|
|
@@ -173,11 +175,11 @@ async function getUnifiedList(tx: any, classId: string) {
|
|
|
173
175
|
// Updated to batch updates to prevent timeouts with large lists
|
|
174
176
|
async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Array<{ id: string; type: 'section' | 'assignment' }>) {
|
|
175
177
|
const BATCH_SIZE = 10; // Process 10 items at a time to avoid overwhelming the transaction
|
|
176
|
-
|
|
178
|
+
|
|
177
179
|
// Group items by type for more efficient updates
|
|
178
180
|
const sections: Array<{ id: string; order: number }> = [];
|
|
179
181
|
const assignments: Array<{ id: string; order: number }> = [];
|
|
180
|
-
|
|
182
|
+
|
|
181
183
|
orderedItems.forEach((item, index) => {
|
|
182
184
|
const orderData = { id: item.id, order: index + 1 };
|
|
183
185
|
if (item.type === 'section') {
|
|
@@ -186,7 +188,7 @@ async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Arra
|
|
|
186
188
|
assignments.push(orderData);
|
|
187
189
|
}
|
|
188
190
|
});
|
|
189
|
-
|
|
191
|
+
|
|
190
192
|
// Process updates in batches
|
|
191
193
|
const processBatch = async (items: Array<{ id: string; order: number }>, type: 'section' | 'assignment') => {
|
|
192
194
|
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
@@ -202,7 +204,7 @@ async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Arra
|
|
|
202
204
|
);
|
|
203
205
|
}
|
|
204
206
|
};
|
|
205
|
-
|
|
207
|
+
|
|
206
208
|
// Process sections and assignments sequentially to avoid transaction overload
|
|
207
209
|
await processBatch(sections, 'section');
|
|
208
210
|
await processBatch(assignments, 'assignment');
|
|
@@ -318,7 +320,21 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
318
320
|
return updated;
|
|
319
321
|
}),
|
|
320
322
|
|
|
321
|
-
|
|
323
|
+
exists: protectedClassMemberProcedure
|
|
324
|
+
.input(z.object({
|
|
325
|
+
id: z.string(),
|
|
326
|
+
}))
|
|
327
|
+
.query(async ({ ctx, input }) => {
|
|
328
|
+
if (!ctx.user) {
|
|
329
|
+
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User must be authenticated' });
|
|
330
|
+
}
|
|
331
|
+
const assignment = await prisma.assignment.findUnique({
|
|
332
|
+
where: { id: input.id },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return assignment ? true : false;
|
|
336
|
+
}),
|
|
337
|
+
move: protectedTeacherProcedure
|
|
322
338
|
.input(z.object({
|
|
323
339
|
id: z.string(),
|
|
324
340
|
classId: z.string(),
|
|
@@ -354,10 +370,10 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
354
370
|
return updated;
|
|
355
371
|
}),
|
|
356
372
|
|
|
357
|
-
create:
|
|
373
|
+
create: protectedTeacherProcedure
|
|
358
374
|
.input(createAssignmentSchema)
|
|
359
375
|
.mutation(async ({ ctx, input }) => {
|
|
360
|
-
const { classId, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
|
|
376
|
+
const { classId, id, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
|
|
361
377
|
|
|
362
378
|
if (!ctx.user) {
|
|
363
379
|
throw new TRPCError({
|
|
@@ -401,16 +417,14 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
401
417
|
}, 0);
|
|
402
418
|
}
|
|
403
419
|
|
|
404
|
-
console.log(studentIds);
|
|
405
|
-
|
|
406
420
|
// Prepare submission data outside transaction
|
|
407
|
-
const submissionData = studentIds && studentIds.length > 0
|
|
421
|
+
const submissionData = studentIds && studentIds.length > 0
|
|
408
422
|
? studentIds.map((studentId) => ({
|
|
409
|
-
|
|
410
|
-
|
|
423
|
+
student: { connect: { id: studentId } }
|
|
424
|
+
}))
|
|
411
425
|
: classData.students.map((student) => ({
|
|
412
|
-
|
|
413
|
-
|
|
426
|
+
student: { connect: { id: student.id } }
|
|
427
|
+
}));
|
|
414
428
|
|
|
415
429
|
const teacherId = ctx.user.id;
|
|
416
430
|
|
|
@@ -419,6 +433,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
419
433
|
// Create assignment with order 0 (will be at top)
|
|
420
434
|
const created = await tx.assignment.create({
|
|
421
435
|
data: {
|
|
436
|
+
...(id && { id }),
|
|
422
437
|
title,
|
|
423
438
|
instructions,
|
|
424
439
|
dueDate: new Date(dueDate),
|
|
@@ -516,7 +531,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
516
531
|
order: { increment: 1 }
|
|
517
532
|
}
|
|
518
533
|
});
|
|
519
|
-
|
|
534
|
+
|
|
520
535
|
await tx.section.updateMany({
|
|
521
536
|
where: {
|
|
522
537
|
classId: classId,
|
|
@@ -581,12 +596,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
581
596
|
|
|
582
597
|
// Execute file operations in parallel
|
|
583
598
|
await Promise.all(fileOperations);
|
|
584
|
-
|
|
599
|
+
|
|
585
600
|
// Send notifications asynchronously (non-blocking)
|
|
586
601
|
sendNotifications(classData.students.map(student => student.id), {
|
|
587
602
|
title: `🔔 New assignment for ${classData.name}`,
|
|
588
603
|
content:
|
|
589
|
-
|
|
604
|
+
`The assignment "${title}" has been created in ${classData.name}.\n
|
|
590
605
|
Due date: ${new Date(dueDate).toLocaleDateString()}.
|
|
591
606
|
[Link to assignment](/class/${classId}/assignments/${assignment.id})`
|
|
592
607
|
}).catch(error => {
|
|
@@ -595,7 +610,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
595
610
|
|
|
596
611
|
return assignment;
|
|
597
612
|
}),
|
|
598
|
-
update:
|
|
613
|
+
update: protectedTeacherProcedure
|
|
599
614
|
.input(updateAssignmentSchema)
|
|
600
615
|
.mutation(async ({ ctx, input }) => {
|
|
601
616
|
const { id, title, instructions, dueDate, files, existingFileIds, worksheetIds, aiPolicyLevel, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, studentIds } = input;
|
|
@@ -640,10 +655,10 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
640
655
|
},
|
|
641
656
|
}),
|
|
642
657
|
prisma.class.findFirst({
|
|
643
|
-
where: {
|
|
644
|
-
assignments: {
|
|
645
|
-
some: { id }
|
|
646
|
-
}
|
|
658
|
+
where: {
|
|
659
|
+
assignments: {
|
|
660
|
+
some: { id }
|
|
661
|
+
}
|
|
647
662
|
},
|
|
648
663
|
include: {
|
|
649
664
|
students: {
|
|
@@ -661,13 +676,13 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
661
676
|
}
|
|
662
677
|
|
|
663
678
|
// Prepare submission data outside transaction if needed
|
|
664
|
-
const submissionData = studentIds && studentIds.length > 0
|
|
679
|
+
const submissionData = studentIds && studentIds.length > 0
|
|
665
680
|
? studentIds.map((studentId) => ({
|
|
666
|
-
|
|
667
|
-
|
|
681
|
+
student: { connect: { id: studentId } }
|
|
682
|
+
}))
|
|
668
683
|
: classData?.students.map((student) => ({
|
|
669
|
-
|
|
670
|
-
|
|
684
|
+
student: { connect: { id: student.id } }
|
|
685
|
+
}));
|
|
671
686
|
|
|
672
687
|
// Handle file deletion operations outside transaction
|
|
673
688
|
const fileDeletionPromises: Promise<void>[] = [];
|
|
@@ -929,7 +944,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
929
944
|
};
|
|
930
945
|
}),
|
|
931
946
|
|
|
932
|
-
get:
|
|
947
|
+
get: protectedClassMemberProcedure
|
|
933
948
|
.input(getAssignmentSchema)
|
|
934
949
|
.query(async ({ ctx, input }) => {
|
|
935
950
|
const { id, classId } = input;
|
|
@@ -1140,19 +1155,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1140
1155
|
};
|
|
1141
1156
|
}),
|
|
1142
1157
|
|
|
1143
|
-
getSubmissionById:
|
|
1158
|
+
getSubmissionById: protectedClassMemberProcedure
|
|
1144
1159
|
.input(z.object({
|
|
1145
|
-
submissionId: z.string(),
|
|
1146
1160
|
classId: z.string(),
|
|
1161
|
+
submissionId: z.string(),
|
|
1147
1162
|
}))
|
|
1148
1163
|
.query(async ({ ctx, input }) => {
|
|
1149
|
-
if (!ctx.user) {
|
|
1150
|
-
throw new TRPCError({
|
|
1151
|
-
code: "UNAUTHORIZED",
|
|
1152
|
-
message: "User must be authenticated",
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
1164
|
const { submissionId, classId } = input;
|
|
1157
1165
|
|
|
1158
1166
|
const submission = await prisma.submission.findFirst({
|
|
@@ -1161,11 +1169,22 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1161
1169
|
assignment: {
|
|
1162
1170
|
classId,
|
|
1163
1171
|
class: {
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1172
|
+
OR: [
|
|
1173
|
+
{
|
|
1174
|
+
teachers: {
|
|
1175
|
+
some: {
|
|
1176
|
+
id: ctx.user?.id
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
students: {
|
|
1182
|
+
some: {
|
|
1183
|
+
id: ctx.user?.id
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1167
1186
|
}
|
|
1168
|
-
|
|
1187
|
+
],
|
|
1169
1188
|
}
|
|
1170
1189
|
},
|
|
1171
1190
|
},
|
|
@@ -1287,6 +1306,20 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1287
1306
|
|
|
1288
1307
|
if (submit !== undefined) {
|
|
1289
1308
|
// Toggle submission status
|
|
1309
|
+
if (submission.assignment.acceptWorksheet && submission.assignment.gradeWithAI) {
|
|
1310
|
+
|
|
1311
|
+
// Grade the submission with AI
|
|
1312
|
+
const worksheetResponses = await prisma.studentWorksheetResponse.findMany({
|
|
1313
|
+
where: {
|
|
1314
|
+
submissionId: submission.id,
|
|
1315
|
+
},
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
for (const worksheetResponse of worksheetResponses) {
|
|
1319
|
+
// Run it in the background, non-blocking
|
|
1320
|
+
gradeWorksheetPipeline(worksheetResponse.id);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1290
1323
|
return await prisma.submission.update({
|
|
1291
1324
|
where: { id: submission.id },
|
|
1292
1325
|
data: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
2
|
+
import { createTRPCRouter, protectedClassMemberProcedure, protectedProcedure } from "../trpc.js";
|
|
3
3
|
import { TRPCError } from "@trpc/server";
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ const attendanceSchema = z.object({
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
export const attendanceRouter = createTRPCRouter({
|
|
14
|
-
get:
|
|
14
|
+
get: protectedClassMemberProcedure
|
|
15
15
|
.input(z.object({
|
|
16
16
|
classId: z.string(),
|
|
17
17
|
eventId: z.string().optional(),
|
package/src/routers/auth.ts
CHANGED
|
@@ -4,8 +4,10 @@ import { TRPCError } from "@trpc/server";
|
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
import { compare, hash } from "bcryptjs";
|
|
7
|
-
import {
|
|
7
|
+
import { sendMail } from "../utils/email.js";
|
|
8
8
|
import { prismaWrapper } from "../utils/prismaWrapper.js";
|
|
9
|
+
import { env } from "../lib/config/env.js";
|
|
10
|
+
import { logger } from "../utils/logger.js";
|
|
9
11
|
|
|
10
12
|
const loginSchema = z.object({
|
|
11
13
|
username: z.string(),
|
|
@@ -106,14 +108,18 @@ export const authRouter = createTRPCRouter({
|
|
|
106
108
|
'creating verification token'
|
|
107
109
|
);
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
try {
|
|
112
|
+
await sendMail({
|
|
113
|
+
from: 'noreply@studious.sh',
|
|
114
|
+
to: user.email,
|
|
115
|
+
subject: 'Verify your email',
|
|
116
|
+
text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
logger.error('Failed to send verification email', { email: user.email, err });
|
|
120
|
+
}
|
|
115
121
|
|
|
116
|
-
|
|
122
|
+
// logger.info(`Password verification email sent to ${user.email} at ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`);
|
|
117
123
|
|
|
118
124
|
return {
|
|
119
125
|
user: {
|
|
@@ -280,12 +286,18 @@ export const authRouter = createTRPCRouter({
|
|
|
280
286
|
},
|
|
281
287
|
});
|
|
282
288
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
+
try {
|
|
290
|
+
await sendMail({
|
|
291
|
+
from: 'noreply@studious.sh',
|
|
292
|
+
to: user.email,
|
|
293
|
+
subject: 'Verify your email',
|
|
294
|
+
text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
|
|
295
|
+
});
|
|
296
|
+
} catch (err) {
|
|
297
|
+
logger.error('Failed to send verification email', { email: user.email, err });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// logger.info(`Password verification email sent to ${user.email} at ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`);
|
|
289
301
|
|
|
290
302
|
return { success: true };
|
|
291
303
|
}),
|
|
@@ -326,6 +338,123 @@ export const authRouter = createTRPCRouter({
|
|
|
326
338
|
where: { id: token },
|
|
327
339
|
});
|
|
328
340
|
|
|
341
|
+
return { success: true };
|
|
342
|
+
}),
|
|
343
|
+
|
|
344
|
+
requestPasswordReset: publicProcedure
|
|
345
|
+
.input(z.object({
|
|
346
|
+
email: z.string().email(),
|
|
347
|
+
}))
|
|
348
|
+
.mutation(async ({ input }) => {
|
|
349
|
+
const { email } = input;
|
|
350
|
+
|
|
351
|
+
const user = await prisma.user.findFirst({
|
|
352
|
+
where: { email },
|
|
353
|
+
select: {
|
|
354
|
+
id: true,
|
|
355
|
+
email: true,
|
|
356
|
+
username: true,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Don't reveal if user exists or not for security
|
|
361
|
+
if (!user) {
|
|
362
|
+
return { success: true };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Delete any existing password reset tokens for this user
|
|
366
|
+
// Only delete tokens that expire within 2 hours (likely password reset tokens)
|
|
367
|
+
const twoHoursFromNow = new Date(Date.now() + 1000 * 60 * 60 * 2);
|
|
368
|
+
await prisma.session.deleteMany({
|
|
369
|
+
where: {
|
|
370
|
+
userId: user.id,
|
|
371
|
+
classId: null,
|
|
372
|
+
expiresAt: {
|
|
373
|
+
lte: twoHoursFromNow, // Only delete short-lived tokens (password reset tokens)
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Create a new password reset token (expires in 1 hour)
|
|
379
|
+
const resetToken = await prisma.session.create({
|
|
380
|
+
data: {
|
|
381
|
+
id: uuidv4(),
|
|
382
|
+
userId: user.id,
|
|
383
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60), // 1 hour
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Send password reset email
|
|
388
|
+
try {
|
|
389
|
+
await sendMail({
|
|
390
|
+
from: 'noreply@studious.sh',
|
|
391
|
+
to: user.email,
|
|
392
|
+
subject: 'Reset your password',
|
|
393
|
+
text: `Click the link to reset your password: ${env.NEXT_PUBLIC_APP_URL}/reset-password/${resetToken.id}`,
|
|
394
|
+
});
|
|
395
|
+
} catch (err) {
|
|
396
|
+
logger.error('Failed to send password reset email', { email: user.email, err });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// logger.info(`Password reset email sent to ${user.email} at ${env.NEXT_PUBLIC_APP_URL}/reset-password/${resetToken.id}`);
|
|
400
|
+
|
|
401
|
+
return { success: true };
|
|
402
|
+
}),
|
|
403
|
+
|
|
404
|
+
resetPassword: publicProcedure
|
|
405
|
+
.input(z.object({
|
|
406
|
+
token: z.string(),
|
|
407
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
408
|
+
confirmPassword: z.string(),
|
|
409
|
+
}).refine((data) => data.password === data.confirmPassword, {
|
|
410
|
+
message: "Passwords don't match",
|
|
411
|
+
path: ["confirmPassword"],
|
|
412
|
+
}))
|
|
413
|
+
.mutation(async ({ input }) => {
|
|
414
|
+
const { token, password } = input;
|
|
415
|
+
|
|
416
|
+
const session = await prisma.session.findUnique({
|
|
417
|
+
where: { id: token },
|
|
418
|
+
include: {
|
|
419
|
+
user: {
|
|
420
|
+
select: {
|
|
421
|
+
id: true,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!session || !session.userId) {
|
|
428
|
+
throw new TRPCError({
|
|
429
|
+
code: "NOT_FOUND",
|
|
430
|
+
message: "Invalid or expired reset token",
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (session.expiresAt && session.expiresAt < new Date()) {
|
|
435
|
+
// Clean up expired token
|
|
436
|
+
await prisma.session.delete({
|
|
437
|
+
where: { id: token },
|
|
438
|
+
});
|
|
439
|
+
throw new TRPCError({
|
|
440
|
+
code: "UNAUTHORIZED",
|
|
441
|
+
message: "Reset token has expired",
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Update the user's password
|
|
446
|
+
await prisma.user.update({
|
|
447
|
+
where: { id: session.userId },
|
|
448
|
+
data: {
|
|
449
|
+
password: await hash(password, 10),
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Clean up the reset token
|
|
454
|
+
await prisma.session.delete({
|
|
455
|
+
where: { id: token },
|
|
456
|
+
});
|
|
457
|
+
|
|
329
458
|
return { success: true };
|
|
330
459
|
}),
|
|
331
460
|
});
|
package/src/routers/class.ts
CHANGED
|
@@ -465,6 +465,51 @@ export const classRouter = createTRPCRouter({
|
|
|
465
465
|
removedUserId: userId,
|
|
466
466
|
};
|
|
467
467
|
}),
|
|
468
|
+
leaveClass: protectedProcedure
|
|
469
|
+
.input(z.object({
|
|
470
|
+
classId: z.string(),
|
|
471
|
+
}))
|
|
472
|
+
.mutation(async ({ ctx, input }) => {
|
|
473
|
+
const { classId } = input;
|
|
474
|
+
const userId = ctx.user?.id;
|
|
475
|
+
|
|
476
|
+
if (!userId) {
|
|
477
|
+
throw new TRPCError({
|
|
478
|
+
code: 'UNAUTHORIZED',
|
|
479
|
+
message: 'User not authenticated',
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const classData = await prisma.class.findFirst({
|
|
484
|
+
where: {
|
|
485
|
+
id: classId,
|
|
486
|
+
students: {
|
|
487
|
+
some: { id: userId },
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (!classData) {
|
|
493
|
+
throw new TRPCError({
|
|
494
|
+
code: 'NOT_FOUND',
|
|
495
|
+
message: 'Class not found or you are not a student in this class',
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
await prisma.class.update({
|
|
500
|
+
where: { id: classId },
|
|
501
|
+
data: {
|
|
502
|
+
students: {
|
|
503
|
+
disconnect: { id: userId },
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
success: true,
|
|
510
|
+
leftClassId: classId,
|
|
511
|
+
};
|
|
512
|
+
}),
|
|
468
513
|
join: protectedProcedure
|
|
469
514
|
.input(z.object({
|
|
470
515
|
classCode: z.string(),
|
|
@@ -472,9 +517,13 @@ export const classRouter = createTRPCRouter({
|
|
|
472
517
|
.mutation(async ({ ctx, input }) => {
|
|
473
518
|
const { classCode } = input;
|
|
474
519
|
|
|
520
|
+
// Case-insensitive search for invite code
|
|
475
521
|
const session = await prisma.session.findFirst({
|
|
476
522
|
where: {
|
|
477
|
-
id:
|
|
523
|
+
id: {
|
|
524
|
+
equals: classCode,
|
|
525
|
+
mode: 'insensitive',
|
|
526
|
+
},
|
|
478
527
|
},
|
|
479
528
|
});
|
|
480
529
|
|
|
@@ -676,7 +725,7 @@ export const classRouter = createTRPCRouter({
|
|
|
676
725
|
|
|
677
726
|
return events;
|
|
678
727
|
}),
|
|
679
|
-
listMarkSchemes:
|
|
728
|
+
listMarkSchemes: protectedClassMemberProcedure
|
|
680
729
|
.input(z.object({
|
|
681
730
|
classId: z.string(),
|
|
682
731
|
}))
|
|
@@ -755,7 +804,7 @@ export const classRouter = createTRPCRouter({
|
|
|
755
804
|
|
|
756
805
|
return markScheme;
|
|
757
806
|
}),
|
|
758
|
-
listGradingBoundaries:
|
|
807
|
+
listGradingBoundaries: protectedClassMemberProcedure
|
|
759
808
|
.input(z.object({
|
|
760
809
|
classId: z.string(),
|
|
761
810
|
}))
|