@studious-lms/server 1.2.45 → 1.2.46
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 +6 -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 +294 -98
- 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 +7 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +35 -18
- 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 +1 -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 +1 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +2 -3
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/marketing.d.ts +1 -1
- package/dist/routers/newtonChat.d.ts +55 -0
- package/dist/routers/newtonChat.d.ts.map +1 -0
- package/dist/routers/newtonChat.js +438 -0
- package/dist/routers/newtonChat.js.map +1 -0
- package/dist/routers/notifications.d.ts +4 -4
- package/dist/routers/section.d.ts +9 -4
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +8 -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 +30 -36
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +11 -33
- 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 +10 -0
- package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiLabChat.js +83 -0
- package/dist/server/pipelines/aiLabChat.js.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts +2 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.js +138 -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 +3 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +41 -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 +32 -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 +4 -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 +58 -40
- 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 +0 -1
- package/src/routers/newtonChat.ts +520 -0
- package/src/routers/section.ts +6 -6
- package/src/routers/user.ts +3 -2
- package/src/routers/worksheet.ts +9 -37
- package/src/seedDatabase.ts +290 -283
- package/src/server/pipelines/aiLabChat.ts +92 -0
- package/src/server/pipelines/gradeWorksheet.ts +152 -0
- package/src/trpc.ts +2 -0
- package/src/utils/email.ts +30 -3
- package/src/utils/inference.ts +50 -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
|
@@ -4,12 +4,13 @@ import { readFileSync } from 'fs'
|
|
|
4
4
|
import { join } from 'path'
|
|
5
5
|
import { writeFile } from 'fs'
|
|
6
6
|
import { DocumentBlock, FormatTypes, Fonts } from './jsonStyles.js'
|
|
7
|
+
import { logger } from '../utils/logger.js'
|
|
7
8
|
|
|
8
9
|
export async function createPdf(blocks: DocumentBlock[]) {
|
|
9
|
-
|
|
10
|
+
logger.info(`createPdf: Starting PDF creation with ${blocks.length} blocks`);
|
|
10
11
|
try {
|
|
11
12
|
const pdfDoc = await PDFDocument.create()
|
|
12
|
-
|
|
13
|
+
logger.info('createPdf: PDFDocument created successfully');
|
|
13
14
|
|
|
14
15
|
// Register fontkit to enable custom font embedding
|
|
15
16
|
pdfDoc.registerFontkit(fontkit)
|
|
@@ -33,9 +34,9 @@ export async function createPdf(blocks: DocumentBlock[]) {
|
|
|
33
34
|
notoSansItalic = await pdfDoc.embedFont(italicFontBytes)
|
|
34
35
|
courierFont = await pdfDoc.embedFont(StandardFonts.Courier) // Keep Courier for code blocks
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
logger.info('createPdf: Unicode fonts loaded successfully');
|
|
37
38
|
} catch (fontError) {
|
|
38
|
-
|
|
39
|
+
logger.warn(`createPdf: Failed to load custom fonts, falling back to standard fonts: ${fontError}`);
|
|
39
40
|
// Fallback to standard fonts if custom fonts fail
|
|
40
41
|
notoSansRegular = await pdfDoc.embedFont(StandardFonts.Helvetica)
|
|
41
42
|
notoSansBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
|
|
@@ -342,10 +343,10 @@ export async function createPdf(blocks: DocumentBlock[]) {
|
|
|
342
343
|
|
|
343
344
|
let y = height - marginTop
|
|
344
345
|
let lastLineHeight = -1
|
|
345
|
-
|
|
346
|
+
logger.info(`createPdf: Starting to process ${blocks.length} blocks`);
|
|
346
347
|
for (let i = 0; i < blocks.length; i++) {
|
|
347
348
|
const block = blocks[i];
|
|
348
|
-
|
|
349
|
+
logger.info(`createPdf: Processing block ${i + 1}/${blocks.length}, format: ${block.format}, content type: ${typeof block.content}`);
|
|
349
350
|
try {
|
|
350
351
|
const preset = STYLE_PRESETS[block.format] || { fontSize: defaultFontSize, lineHeight: defaultLineHeight }
|
|
351
352
|
|
|
@@ -725,24 +726,21 @@ export async function createPdf(blocks: DocumentBlock[]) {
|
|
|
725
726
|
}
|
|
726
727
|
}
|
|
727
728
|
}
|
|
728
|
-
|
|
729
|
+
logger.info(`createPdf: Successfully processed block ${i + 1}`);
|
|
729
730
|
y -= paragraphSpacing
|
|
730
731
|
lastLineHeight = lineHeight
|
|
731
732
|
} catch (blockError) {
|
|
732
|
-
|
|
733
|
+
logger.error(`createPdf: Error processing block ${i + 1}: ${blockError}`);
|
|
733
734
|
throw blockError;
|
|
734
735
|
}
|
|
735
736
|
}
|
|
736
737
|
|
|
737
|
-
|
|
738
|
+
logger.info('createPdf: About to save PDF document');
|
|
738
739
|
const pdfBytes = await pdfDoc.save()
|
|
739
|
-
|
|
740
|
-
// writeFile('output.pdf', pdfBytes, () => {
|
|
741
|
-
// console.log('PDF created successfully') // Still only saves file, no API yet
|
|
742
|
-
// })
|
|
740
|
+
logger.info(`createPdf: PDF saved successfully, bytes length: ${pdfBytes.length}`);
|
|
743
741
|
return pdfBytes
|
|
744
742
|
} catch (error) {
|
|
745
|
-
|
|
743
|
+
logger.error(`createPdf: Error during PDF creation: ${error}`);
|
|
746
744
|
throw error;
|
|
747
745
|
}
|
|
748
746
|
}
|
package/src/lib/prisma.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { PrismaClient } from '@prisma/client';
|
|
2
|
+
import { env } from './config/env.js';
|
|
3
|
+
|
|
4
|
+
const getLogLevel = () => {
|
|
5
|
+
switch (env.NODE_ENV) {
|
|
6
|
+
case 'development':
|
|
7
|
+
return ['query', 'error', 'warn'];
|
|
8
|
+
case 'production':
|
|
9
|
+
return ['error'];
|
|
10
|
+
default:
|
|
11
|
+
return ['error'];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
2
14
|
|
|
3
15
|
const prismaClientSingleton = () => {
|
|
16
|
+
// return new PrismaClient({
|
|
17
|
+
// log: env.NODE_ENV === 'development'
|
|
18
|
+
// ? ['query', 'error', 'warn']
|
|
19
|
+
// : ['error'],
|
|
20
|
+
// });
|
|
4
21
|
return new PrismaClient();
|
|
5
22
|
};
|
|
6
23
|
|
|
@@ -11,6 +28,10 @@ declare global {
|
|
|
11
28
|
|
|
12
29
|
export const prisma = globalThis.prisma ?? prismaClientSingleton();
|
|
13
30
|
|
|
14
|
-
if (
|
|
31
|
+
if (env.NODE_ENV !== 'production') {
|
|
15
32
|
globalThis.prisma = prisma;
|
|
16
|
-
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.on('beforeExit', async () => {
|
|
36
|
+
await prisma.$disconnect();
|
|
37
|
+
});
|
package/src/lib/pusher.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import Pusher from 'pusher';
|
|
2
|
+
import { env } from './config/env.js';
|
|
2
3
|
|
|
3
4
|
const pusher = new Pusher({
|
|
4
|
-
appId:
|
|
5
|
-
key:
|
|
6
|
-
secret:
|
|
7
|
-
cluster:
|
|
8
|
-
useTLS:
|
|
5
|
+
appId: env.PUSHER_APP_ID,
|
|
6
|
+
key: env.PUSHER_KEY,
|
|
7
|
+
secret: env.PUSHER_SECRET,
|
|
8
|
+
cluster: env.PUSHER_CLUSTER,
|
|
9
|
+
useTLS: env.NODE_ENV !== 'development',
|
|
9
10
|
});
|
|
10
11
|
|
|
11
12
|
export { pusher };
|
package/src/middleware/auth.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TRPCError } from '@trpc/server';
|
|
2
2
|
import { prisma } from '../lib/prisma.js';
|
|
3
3
|
import type { MiddlewareContext } from '../types/trpc.js';
|
|
4
|
+
import * as Sentry from "@sentry/node";
|
|
4
5
|
|
|
5
6
|
export const createAuthMiddleware = (t: any) => {
|
|
6
7
|
|
|
@@ -50,10 +51,10 @@ export const createAuthMiddleware = (t: any) => {
|
|
|
50
51
|
},
|
|
51
52
|
});
|
|
52
53
|
} catch (error) {
|
|
53
|
-
|
|
54
|
+
Sentry.captureException(error);
|
|
54
55
|
throw new TRPCError({
|
|
55
|
-
code: '
|
|
56
|
-
message: '
|
|
56
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
57
|
+
message: 'Internal server error',
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import helmet from 'helmet';
|
|
2
|
+
import rateLimit from 'express-rate-limit';
|
|
3
|
+
import type { Request, Response } from 'express';
|
|
4
|
+
|
|
5
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
6
|
+
|
|
7
|
+
// Custom handler for rate limit errors that returns JSON
|
|
8
|
+
// This format can be intercepted on the frontend with:
|
|
9
|
+
// error.data?.code === 'TOO_MANY_REQUESTS' || error.data?.httpStatus === 429
|
|
10
|
+
const rateLimitHandler = (req: Request, res: Response) => {
|
|
11
|
+
// Return JSON structure that can be intercepted on frontend with:
|
|
12
|
+
// error.data?.code === 'TOO_MANY_REQUESTS' || error.data?.httpStatus === 429
|
|
13
|
+
// When tRPC wraps this, the response body becomes error.data, so we put code/httpStatus at top level
|
|
14
|
+
res.status(429).json({
|
|
15
|
+
code: 'TOO_MANY_REQUESTS',
|
|
16
|
+
message: 'Too many requests, please try again later.',
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// General API rate limiter - applies to all routes
|
|
21
|
+
export const generalLimiter = rateLimit({
|
|
22
|
+
windowMs: 10 * 60 * 1000, // 10 minutes
|
|
23
|
+
max: 100, // Limit each IP to 100 requests per windowMs
|
|
24
|
+
message: 'Too many requests from this IP, please try again later.',
|
|
25
|
+
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
|
26
|
+
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
|
27
|
+
handler: rateLimitHandler,
|
|
28
|
+
skip: (req) => {
|
|
29
|
+
// Skip rate limiting for health checks
|
|
30
|
+
return req.path === '/health';
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Stricter rate limiter for authentication endpoints
|
|
35
|
+
export const authLimiter = rateLimit({
|
|
36
|
+
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
37
|
+
max: 5, // Limit each IP to 5 login attempts per windowMs
|
|
38
|
+
message: 'Too many authentication attempts, please try again later.',
|
|
39
|
+
standardHeaders: true,
|
|
40
|
+
legacyHeaders: false,
|
|
41
|
+
skipSuccessfulRequests: true, // Don't count successful requests
|
|
42
|
+
handler: rateLimitHandler,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// File upload rate limiter
|
|
46
|
+
export const uploadLimiter = rateLimit({
|
|
47
|
+
windowMs: 30 * 60 * 1000, // 30 minutes
|
|
48
|
+
max: 50, // Limit each IP to 50 uploads per hour
|
|
49
|
+
message: 'Too many file uploads, please try again later.',
|
|
50
|
+
standardHeaders: true,
|
|
51
|
+
legacyHeaders: false,
|
|
52
|
+
handler: rateLimitHandler,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Helmet configuration
|
|
56
|
+
export const helmetConfig = helmet({
|
|
57
|
+
contentSecurityPolicy: {
|
|
58
|
+
directives: {
|
|
59
|
+
defaultSrc: ["'self'"],
|
|
60
|
+
styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for tRPC panel
|
|
61
|
+
// Allow inline scripts only in development (for tRPC panel)
|
|
62
|
+
// In production, keep strict CSP without unsafe-inline
|
|
63
|
+
scriptSrc: isDevelopment
|
|
64
|
+
? ["'self'", "'unsafe-inline'"]
|
|
65
|
+
: ["'self'"],
|
|
66
|
+
imgSrc: ["'self'", "data:", "https:"], // Allow images from any HTTPS source
|
|
67
|
+
connectSrc: ["'self'", "https://*.sentry.io"], // Allow Sentry connections
|
|
68
|
+
fontSrc: ["'self'", "data:"],
|
|
69
|
+
objectSrc: ["'none'"],
|
|
70
|
+
mediaSrc: ["'self'"],
|
|
71
|
+
frameSrc: ["'none'"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
crossOriginEmbedderPolicy: false, // Disable if you need to embed resources
|
|
75
|
+
hsts: {
|
|
76
|
+
maxAge: 31536000, // 1 year
|
|
77
|
+
includeSubDomains: true,
|
|
78
|
+
preload: true,
|
|
79
|
+
},
|
|
80
|
+
});
|
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
|
|
@@ -173,11 +174,11 @@ async function getUnifiedList(tx: any, classId: string) {
|
|
|
173
174
|
// Updated to batch updates to prevent timeouts with large lists
|
|
174
175
|
async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Array<{ id: string; type: 'section' | 'assignment' }>) {
|
|
175
176
|
const BATCH_SIZE = 10; // Process 10 items at a time to avoid overwhelming the transaction
|
|
176
|
-
|
|
177
|
+
|
|
177
178
|
// Group items by type for more efficient updates
|
|
178
179
|
const sections: Array<{ id: string; order: number }> = [];
|
|
179
180
|
const assignments: Array<{ id: string; order: number }> = [];
|
|
180
|
-
|
|
181
|
+
|
|
181
182
|
orderedItems.forEach((item, index) => {
|
|
182
183
|
const orderData = { id: item.id, order: index + 1 };
|
|
183
184
|
if (item.type === 'section') {
|
|
@@ -186,7 +187,7 @@ async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Arra
|
|
|
186
187
|
assignments.push(orderData);
|
|
187
188
|
}
|
|
188
189
|
});
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
// Process updates in batches
|
|
191
192
|
const processBatch = async (items: Array<{ id: string; order: number }>, type: 'section' | 'assignment') => {
|
|
192
193
|
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
@@ -202,7 +203,7 @@ async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Arra
|
|
|
202
203
|
);
|
|
203
204
|
}
|
|
204
205
|
};
|
|
205
|
-
|
|
206
|
+
|
|
206
207
|
// Process sections and assignments sequentially to avoid transaction overload
|
|
207
208
|
await processBatch(sections, 'section');
|
|
208
209
|
await processBatch(assignments, 'assignment');
|
|
@@ -318,7 +319,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
318
319
|
return updated;
|
|
319
320
|
}),
|
|
320
321
|
|
|
321
|
-
|
|
322
|
+
move: protectedTeacherProcedure
|
|
322
323
|
.input(z.object({
|
|
323
324
|
id: z.string(),
|
|
324
325
|
classId: z.string(),
|
|
@@ -354,7 +355,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
354
355
|
return updated;
|
|
355
356
|
}),
|
|
356
357
|
|
|
357
|
-
create:
|
|
358
|
+
create: protectedTeacherProcedure
|
|
358
359
|
.input(createAssignmentSchema)
|
|
359
360
|
.mutation(async ({ ctx, input }) => {
|
|
360
361
|
const { classId, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
|
|
@@ -401,16 +402,14 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
401
402
|
}, 0);
|
|
402
403
|
}
|
|
403
404
|
|
|
404
|
-
console.log(studentIds);
|
|
405
|
-
|
|
406
405
|
// Prepare submission data outside transaction
|
|
407
|
-
const submissionData = studentIds && studentIds.length > 0
|
|
406
|
+
const submissionData = studentIds && studentIds.length > 0
|
|
408
407
|
? studentIds.map((studentId) => ({
|
|
409
|
-
|
|
410
|
-
|
|
408
|
+
student: { connect: { id: studentId } }
|
|
409
|
+
}))
|
|
411
410
|
: classData.students.map((student) => ({
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
student: { connect: { id: student.id } }
|
|
412
|
+
}));
|
|
414
413
|
|
|
415
414
|
const teacherId = ctx.user.id;
|
|
416
415
|
|
|
@@ -516,7 +515,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
516
515
|
order: { increment: 1 }
|
|
517
516
|
}
|
|
518
517
|
});
|
|
519
|
-
|
|
518
|
+
|
|
520
519
|
await tx.section.updateMany({
|
|
521
520
|
where: {
|
|
522
521
|
classId: classId,
|
|
@@ -581,12 +580,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
581
580
|
|
|
582
581
|
// Execute file operations in parallel
|
|
583
582
|
await Promise.all(fileOperations);
|
|
584
|
-
|
|
583
|
+
|
|
585
584
|
// Send notifications asynchronously (non-blocking)
|
|
586
585
|
sendNotifications(classData.students.map(student => student.id), {
|
|
587
586
|
title: `🔔 New assignment for ${classData.name}`,
|
|
588
587
|
content:
|
|
589
|
-
|
|
588
|
+
`The assignment "${title}" has been created in ${classData.name}.\n
|
|
590
589
|
Due date: ${new Date(dueDate).toLocaleDateString()}.
|
|
591
590
|
[Link to assignment](/class/${classId}/assignments/${assignment.id})`
|
|
592
591
|
}).catch(error => {
|
|
@@ -595,7 +594,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
595
594
|
|
|
596
595
|
return assignment;
|
|
597
596
|
}),
|
|
598
|
-
update:
|
|
597
|
+
update: protectedTeacherProcedure
|
|
599
598
|
.input(updateAssignmentSchema)
|
|
600
599
|
.mutation(async ({ ctx, input }) => {
|
|
601
600
|
const { id, title, instructions, dueDate, files, existingFileIds, worksheetIds, aiPolicyLevel, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, studentIds } = input;
|
|
@@ -640,10 +639,10 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
640
639
|
},
|
|
641
640
|
}),
|
|
642
641
|
prisma.class.findFirst({
|
|
643
|
-
where: {
|
|
644
|
-
assignments: {
|
|
645
|
-
some: { id }
|
|
646
|
-
}
|
|
642
|
+
where: {
|
|
643
|
+
assignments: {
|
|
644
|
+
some: { id }
|
|
645
|
+
}
|
|
647
646
|
},
|
|
648
647
|
include: {
|
|
649
648
|
students: {
|
|
@@ -661,13 +660,13 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
661
660
|
}
|
|
662
661
|
|
|
663
662
|
// Prepare submission data outside transaction if needed
|
|
664
|
-
const submissionData = studentIds && studentIds.length > 0
|
|
663
|
+
const submissionData = studentIds && studentIds.length > 0
|
|
665
664
|
? studentIds.map((studentId) => ({
|
|
666
|
-
|
|
667
|
-
|
|
665
|
+
student: { connect: { id: studentId } }
|
|
666
|
+
}))
|
|
668
667
|
: classData?.students.map((student) => ({
|
|
669
|
-
|
|
670
|
-
|
|
668
|
+
student: { connect: { id: student.id } }
|
|
669
|
+
}));
|
|
671
670
|
|
|
672
671
|
// Handle file deletion operations outside transaction
|
|
673
672
|
const fileDeletionPromises: Promise<void>[] = [];
|
|
@@ -929,7 +928,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
929
928
|
};
|
|
930
929
|
}),
|
|
931
930
|
|
|
932
|
-
get:
|
|
931
|
+
get: protectedClassMemberProcedure
|
|
933
932
|
.input(getAssignmentSchema)
|
|
934
933
|
.query(async ({ ctx, input }) => {
|
|
935
934
|
const { id, classId } = input;
|
|
@@ -1140,19 +1139,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1140
1139
|
};
|
|
1141
1140
|
}),
|
|
1142
1141
|
|
|
1143
|
-
getSubmissionById:
|
|
1142
|
+
getSubmissionById: protectedClassMemberProcedure
|
|
1144
1143
|
.input(z.object({
|
|
1145
|
-
submissionId: z.string(),
|
|
1146
1144
|
classId: z.string(),
|
|
1145
|
+
submissionId: z.string(),
|
|
1147
1146
|
}))
|
|
1148
1147
|
.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
1148
|
const { submissionId, classId } = input;
|
|
1157
1149
|
|
|
1158
1150
|
const submission = await prisma.submission.findFirst({
|
|
@@ -1161,11 +1153,22 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1161
1153
|
assignment: {
|
|
1162
1154
|
classId,
|
|
1163
1155
|
class: {
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1156
|
+
OR: [
|
|
1157
|
+
{
|
|
1158
|
+
teachers: {
|
|
1159
|
+
some: {
|
|
1160
|
+
id: ctx.user?.id
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
students: {
|
|
1166
|
+
some: {
|
|
1167
|
+
id: ctx.user?.id
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1167
1170
|
}
|
|
1168
|
-
|
|
1171
|
+
],
|
|
1169
1172
|
}
|
|
1170
1173
|
},
|
|
1171
1174
|
},
|
|
@@ -1287,6 +1290,21 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1287
1290
|
|
|
1288
1291
|
if (submit !== undefined) {
|
|
1289
1292
|
// Toggle submission status
|
|
1293
|
+
|
|
1294
|
+
if (submission.assignment.acceptWorksheet && submission.assignment.gradeWithAI) {
|
|
1295
|
+
|
|
1296
|
+
// Grade the submission with AI
|
|
1297
|
+
const worksheetResponses = await prisma.studentWorksheetResponse.findMany({
|
|
1298
|
+
where: {
|
|
1299
|
+
submissionId: submission.id,
|
|
1300
|
+
},
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
for (const worksheetResponse of worksheetResponses) {
|
|
1304
|
+
// Run it in the background, non-blocking
|
|
1305
|
+
gradeWorksheetPipeline(worksheetResponse.id);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1290
1308
|
return await prisma.submission.update({
|
|
1291
1309
|
where: { id: submission.id },
|
|
1292
1310
|
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(),
|