@studious-lms/server 1.4.0 → 1.4.2
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 +6 -0
- package/.env.test.example +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -50
- 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/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/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 +102 -18
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/class.d.ts +112 -64
- package/dist/models/class.d.ts.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 +25 -7
- package/dist/models/notification.d.ts.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 +237 -108
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +30 -6
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +157 -234
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +413 -0
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -0
- package/dist/pipelines/aiLabChatContract.js +74 -0
- package/dist/pipelines/aiLabChatContract.js.map +1 -0
- package/dist/pipelines/gradeWorksheet.d.ts +8 -8
- package/dist/pipelines/gradeWorksheet.js +4 -4
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/labChatPrompt.d.ts +29 -0
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -0
- package/dist/pipelines/labChatPrompt.js +146 -0
- package/dist/pipelines/labChatPrompt.js.map +1 -0
- package/dist/routers/_app.d.ts +1622 -1260
- 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 +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 +77 -71
- package/dist/routers/class.d.ts.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 +12 -12
- package/dist/routers/marketing.d.ts +2 -2
- package/dist/routers/message.d.ts +2 -2
- package/dist/routers/newtonChat.d.ts +1 -1
- package/dist/routers/notifications.d.ts +4 -4
- package/dist/routers/section.d.ts +7 -7
- package/dist/routers/studentProgress.d.ts +161 -0
- package/dist/routers/studentProgress.d.ts.map +1 -0
- package/dist/routers/studentProgress.js +43 -0
- package/dist/routers/studentProgress.js.map +1 -0
- package/dist/routers/user.d.ts +1 -1
- package/dist/routers/worksheet.d.ts +58 -58
- 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 +1 -1
- package/dist/services/class.d.ts +73 -67
- package/dist/services/class.d.ts.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/folder.d.ts +52 -52
- package/dist/services/labChat.d.ts +12 -12
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +31 -15
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/marketing.d.ts +2 -2
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +90 -48
- package/dist/services/message.js.map +1 -1
- package/dist/services/notification.d.ts +4 -4
- package/dist/services/section.d.ts +6 -6
- package/dist/services/studentProgress.d.ts +120 -0
- package/dist/services/studentProgress.d.ts.map +1 -0
- package/dist/services/studentProgress.js +481 -0
- package/dist/services/studentProgress.js.map +1 -0
- package/dist/services/worksheet.d.ts +49 -49
- package/dist/utils/inference.d.ts +0 -11
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +2 -50
- package/dist/utils/inference.js.map +1 -1
- package/package.json +2 -2
- package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +14 -0
- package/prisma/schema.prisma +14 -0
- package/sentry.properties +3 -0
- package/src/index.ts +39 -51
- package/src/lib/config/cors.ts +96 -0
- package/src/lib/config/env.ts +12 -1
- package/src/lib/prisma.ts +25 -6
- package/src/middleware/security.ts +1 -1
- package/src/pipelines/aiLabChat.ts +206 -246
- package/src/pipelines/aiLabChatContract.ts +75 -0
- package/src/pipelines/gradeWorksheet.ts +2 -2
- package/src/pipelines/labChatPrompt.ts +196 -0
- package/src/routers/_app.ts +4 -2
- package/src/routers/assignment.ts +1 -0
- package/src/routers/studentProgress.ts +71 -0
- package/src/services/assignment.ts +30 -2
- package/src/services/labChat.ts +31 -22
- package/src/services/message.ts +97 -48
- package/src/services/studentProgress.ts +691 -0
- package/src/utils/inference.ts +0 -61
- package/tests/lib/aiLabChatContract.test.ts +32 -0
- package/tests/lib/cors.test.ts +103 -0
- package/tests/pipelines/aiLabChat.test.ts +75 -0
- package/tests/routers/studentProgress.test.ts +254 -0
- package/tests/utils/aiLabChatPrompt.test.ts +126 -0
- package/tests/utils/studentProgress.test.ts +361 -0
- package/vitest.unit.config.ts +8 -1
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { bucket } from './lib/googleCloudStorage.js';
|
|
|
13
13
|
import { prisma } from './lib/prisma.js';
|
|
14
14
|
import { pusher } from './lib/pusher.js';
|
|
15
15
|
import { connectRedis, disconnectRedis } from './lib/redis.js';
|
|
16
|
+
import { createCorsOriginMatcher } from './lib/config/cors.js';
|
|
16
17
|
|
|
17
18
|
import { authLimiter, generalLimiter, helmetConfig, uploadLimiter } from './middleware/security.js';
|
|
18
19
|
|
|
@@ -27,6 +28,8 @@ import { openAIClient } from './utils/inference.js';
|
|
|
27
28
|
|
|
28
29
|
const app = express();
|
|
29
30
|
|
|
31
|
+
app.set('trust proxy', 1);
|
|
32
|
+
|
|
30
33
|
app.use(helmetConfig);
|
|
31
34
|
app.use(compression());
|
|
32
35
|
app.use(express.json());
|
|
@@ -38,34 +41,24 @@ app.use((req, res, next) => {
|
|
|
38
41
|
next();
|
|
39
42
|
});
|
|
40
43
|
|
|
41
|
-
const allowedOrigins = env
|
|
42
|
-
? [
|
|
43
|
-
'https://www.studious.sh',
|
|
44
|
-
'https://studious.sh',
|
|
45
|
-
'https://dev.studious.sh',
|
|
46
|
-
'https://www.dev.studious.sh',
|
|
47
|
-
env.NEXT_PUBLIC_APP_URL,
|
|
48
|
-
'http://localhost:3000',
|
|
49
|
-
|
|
50
|
-
].filter(Boolean)
|
|
51
|
-
: [
|
|
52
|
-
'http://localhost:3000',
|
|
53
|
-
'http://localhost:3001',
|
|
54
|
-
'http://127.0.0.1:3000',
|
|
55
|
-
'http://127.0.0.1:3001',
|
|
56
|
-
|
|
57
|
-
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
58
|
-
];
|
|
44
|
+
const { allowedOrigins, allowedOriginPatterns, isAllowedOrigin } = createCorsOriginMatcher(env);
|
|
59
45
|
|
|
60
46
|
// CORS middleware
|
|
61
47
|
app.use(cors({
|
|
62
|
-
origin:
|
|
48
|
+
origin: (origin, callback) => {
|
|
49
|
+
if (!origin || isAllowedOrigin(origin)) {
|
|
50
|
+
callback(null, true);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
logger.warn('CORS origin rejected', { origin, allowedOrigins });
|
|
55
|
+
callback(null, false);
|
|
56
|
+
},
|
|
63
57
|
credentials: true,
|
|
64
58
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
65
59
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-user'],
|
|
66
|
-
preflightContinue: false,
|
|
67
|
-
optionsSuccessStatus: 204,
|
|
68
|
-
|
|
60
|
+
preflightContinue: false,
|
|
61
|
+
optionsSuccessStatus: 204,
|
|
69
62
|
}));
|
|
70
63
|
|
|
71
64
|
app.use(generalLimiter);
|
|
@@ -243,15 +236,9 @@ app.post('/api/pusher/auth', async (req, res) => {
|
|
|
243
236
|
// Setup Socket.IO
|
|
244
237
|
const io = new Server(httpServer, {
|
|
245
238
|
cors: {
|
|
246
|
-
origin:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
'http://127.0.0.1:3000', // Alternative localhost
|
|
250
|
-
'http://127.0.0.1:3001', // Alternative localhost
|
|
251
|
-
'https://www.studious.sh', // Production frontend
|
|
252
|
-
'https://studious.sh', // Production frontend (without www)
|
|
253
|
-
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
254
|
-
],
|
|
239
|
+
origin: (origin, callback) => {
|
|
240
|
+
callback(null, !origin || isAllowedOrigin(origin));
|
|
241
|
+
},
|
|
255
242
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
256
243
|
credentials: true,
|
|
257
244
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Access-Control-Allow-Origin', 'x-user']
|
|
@@ -491,20 +478,13 @@ function handleFileUpload(req: any, res: any) {
|
|
|
491
478
|
|
|
492
479
|
// Set CORS headers for upload endpoint
|
|
493
480
|
const origin = req.headers.origin;
|
|
494
|
-
const allowedOrigins = [
|
|
495
|
-
'http://localhost:3000',
|
|
496
|
-
'http://localhost:3001',
|
|
497
|
-
'http://127.0.0.1:3000',
|
|
498
|
-
'http://127.0.0.1:3001',
|
|
499
|
-
'https://www.studious.sh', // Production frontend
|
|
500
|
-
'https://studious.sh', // Production frontend (without www)
|
|
501
|
-
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
502
|
-
];
|
|
503
481
|
|
|
504
|
-
if (origin &&
|
|
482
|
+
if (origin && !isAllowedOrigin(origin)) {
|
|
483
|
+
return res.status(403).json({ error: 'Origin not allowed' });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (origin) {
|
|
505
487
|
res.header('Access-Control-Allow-Origin', origin);
|
|
506
|
-
} else {
|
|
507
|
-
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
|
|
508
488
|
}
|
|
509
489
|
|
|
510
490
|
res.header('Access-Control-Allow-Credentials', 'true');
|
|
@@ -550,6 +530,19 @@ function handleFileUpload(req: any, res: any) {
|
|
|
550
530
|
// Create caller
|
|
551
531
|
const createCaller = createCallerFactory(appRouter);
|
|
552
532
|
|
|
533
|
+
// Fallback OPTIONS handler — prevents tRPC from rejecting preflight requests
|
|
534
|
+
// when the cors middleware passes through without ending the response.
|
|
535
|
+
app.options('/trpc/*', (req, res) => {
|
|
536
|
+
const origin = req.headers.origin;
|
|
537
|
+
if (origin && isAllowedOrigin(origin)) {
|
|
538
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
539
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
540
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
541
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, x-user');
|
|
542
|
+
}
|
|
543
|
+
res.sendStatus(204);
|
|
544
|
+
});
|
|
545
|
+
|
|
553
546
|
// Setup tRPC middleware
|
|
554
547
|
app.use(
|
|
555
548
|
'/trpc',
|
|
@@ -594,13 +587,8 @@ logger.info('Configurations', {
|
|
|
594
587
|
|
|
595
588
|
// Log CORS configuration
|
|
596
589
|
logger.info('CORS Configuration', {
|
|
597
|
-
allowedOrigins
|
|
598
|
-
|
|
599
|
-
'http://localhost:3001',
|
|
600
|
-
'http://127.0.0.1:3000',
|
|
601
|
-
'http://127.0.0.1:3001',
|
|
602
|
-
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
603
|
-
]
|
|
590
|
+
allowedOrigins,
|
|
591
|
+
allowedOriginPatterns: allowedOriginPatterns.map((pattern) => pattern.source),
|
|
604
592
|
});
|
|
605
593
|
|
|
606
594
|
const gracefulShutdown = (signal: string) => {
|
|
@@ -632,4 +620,4 @@ const gracefulShutdown = (signal: string) => {
|
|
|
632
620
|
};
|
|
633
621
|
|
|
634
622
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
635
|
-
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
623
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
type CorsEnv = {
|
|
2
|
+
NODE_ENV?: 'development' | 'production' | 'test';
|
|
3
|
+
NEXT_PUBLIC_APP_URL?: string;
|
|
4
|
+
CORS_ALLOWED_ORIGINS?: string;
|
|
5
|
+
CORS_ALLOWED_ORIGIN_PATTERNS?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const productionOrigins = [
|
|
9
|
+
'https://www.studious.sh',
|
|
10
|
+
'https://studious.sh',
|
|
11
|
+
'https://dev.studious.sh',
|
|
12
|
+
'https://www.dev.studious.sh',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const nonProductionOrigins = [
|
|
16
|
+
'http://localhost:3000',
|
|
17
|
+
'http://localhost:3001',
|
|
18
|
+
'http://127.0.0.1:3000',
|
|
19
|
+
'http://127.0.0.1:3001',
|
|
20
|
+
];
|
|
21
|
+
const MAX_PATTERN_LENGTH = 200;
|
|
22
|
+
// This heuristic only targets common nested-quantifier cases like `(a+)+`.
|
|
23
|
+
// It does not catch every risky construct (for example `.*.*` or deep alternation nesting),
|
|
24
|
+
// so `parseCorsOriginPatterns` should still be limited to operator-controlled, anchored patterns.
|
|
25
|
+
const SUSPICIOUS_QUANTIFIER_PATTERN = /(\([^)]*[+*{][^)]*\)|\[[^\]]+[+*{][^\]]*\])[+*{]/;
|
|
26
|
+
|
|
27
|
+
const parseList = (value?: string) =>
|
|
28
|
+
value
|
|
29
|
+
?.split(',')
|
|
30
|
+
.map((item) => item.trim())
|
|
31
|
+
.filter(Boolean) ?? [];
|
|
32
|
+
|
|
33
|
+
const isDefined = <T>(value: T | undefined | null): value is T => Boolean(value);
|
|
34
|
+
|
|
35
|
+
const toCorsOrigin = (value: string) => {
|
|
36
|
+
try {
|
|
37
|
+
return new URL(value).origin;
|
|
38
|
+
} catch {
|
|
39
|
+
return value.replace(/\/+$/, '');
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const parseCorsOrigins = (origins?: string) =>
|
|
44
|
+
parseList(origins).map(toCorsOrigin);
|
|
45
|
+
|
|
46
|
+
const validateCorsOriginPattern = (pattern: string) => {
|
|
47
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
48
|
+
throw new Error(`CORS pattern exceeds maximum length of ${MAX_PATTERN_LENGTH}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Prefer short, anchored patterns and avoid other complex regex features even if they pass this check.
|
|
52
|
+
if (SUSPICIOUS_QUANTIFIER_PATTERN.test(pattern)) {
|
|
53
|
+
throw new Error("CORS pattern contains nested or ambiguous quantifiers");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return pattern;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Patterns are operator-controlled and matched against bounded URL origins, so ReDoS risk is low.
|
|
60
|
+
// Syntax validation does not guarantee safety; prefer anchored patterns and avoid nested quantifiers.
|
|
61
|
+
export const parseCorsOriginPatterns = (patterns?: string) =>
|
|
62
|
+
parseList(patterns).map((pattern) => new RegExp(validateCorsOriginPattern(pattern)));
|
|
63
|
+
|
|
64
|
+
export const isValidCorsOriginPatternList = (patterns?: string) => {
|
|
65
|
+
try {
|
|
66
|
+
parseCorsOriginPatterns(patterns);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const createCorsOriginMatcher = (env: CorsEnv) => {
|
|
74
|
+
const allowedOrigins = Array.from(new Set([
|
|
75
|
+
...(env.NODE_ENV === 'production' ? productionOrigins : nonProductionOrigins),
|
|
76
|
+
env.NEXT_PUBLIC_APP_URL ? toCorsOrigin(env.NEXT_PUBLIC_APP_URL) : undefined,
|
|
77
|
+
...parseCorsOrigins(env.CORS_ALLOWED_ORIGINS),
|
|
78
|
+
].filter(isDefined)));
|
|
79
|
+
|
|
80
|
+
const allowedOriginPatterns = parseCorsOriginPatterns(env.CORS_ALLOWED_ORIGIN_PATTERNS);
|
|
81
|
+
|
|
82
|
+
const isAllowedOrigin = (origin?: string) => {
|
|
83
|
+
const corsOrigin = origin ? toCorsOrigin(origin) : undefined;
|
|
84
|
+
|
|
85
|
+
return Boolean(corsOrigin && (
|
|
86
|
+
allowedOrigins.includes(corsOrigin) ||
|
|
87
|
+
allowedOriginPatterns.some((pattern) => pattern.test(corsOrigin))
|
|
88
|
+
));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
allowedOrigins,
|
|
93
|
+
allowedOriginPatterns,
|
|
94
|
+
isAllowedOrigin,
|
|
95
|
+
};
|
|
96
|
+
};
|
package/src/lib/config/env.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import dotenv from 'dotenv';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
|
+
import { isValidCorsOriginPatternList } from './cors.js';
|
|
5
6
|
|
|
6
7
|
// Determine which env file to load based on NODE_ENV
|
|
7
8
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
@@ -23,6 +24,12 @@ dotenv.config({ path: envPath, override: true }); // Override with env-specific
|
|
|
23
24
|
const isTest = nodeEnv === 'test';
|
|
24
25
|
const isProduction = nodeEnv === 'production';
|
|
25
26
|
|
|
27
|
+
const corsAllowedOriginsSchema = z.string().optional();
|
|
28
|
+
const corsAllowedOriginPatternsSchema = z.string().optional().refine(
|
|
29
|
+
isValidCorsOriginPatternList,
|
|
30
|
+
{ message: 'CORS_ALLOWED_ORIGIN_PATTERNS must contain valid regex patterns' }
|
|
31
|
+
);
|
|
32
|
+
|
|
26
33
|
// Base schema with required vars for all environments
|
|
27
34
|
const baseSchema = z.object({
|
|
28
35
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
@@ -33,6 +40,8 @@ const baseSchema = z.object({
|
|
|
33
40
|
// Production/development schema with all required vars
|
|
34
41
|
const fullSchema = baseSchema.extend({
|
|
35
42
|
NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
|
|
43
|
+
CORS_ALLOWED_ORIGINS: corsAllowedOriginsSchema,
|
|
44
|
+
CORS_ALLOWED_ORIGIN_PATTERNS: corsAllowedOriginPatternsSchema,
|
|
36
45
|
BACKEND_URL: z.string().url().default('http://localhost:3001'),
|
|
37
46
|
SENTRY_DSN: z.string().url().optional(),
|
|
38
47
|
EMAIL_HOST: z.string().min(1, 'EMAIL_HOST is required'),
|
|
@@ -59,6 +68,8 @@ const fullSchema = baseSchema.extend({
|
|
|
59
68
|
// Test schema - only require what's needed for tests
|
|
60
69
|
const testSchema = baseSchema.extend({
|
|
61
70
|
NEXT_PUBLIC_APP_URL: z.string().url().optional().default('http://localhost:3000'),
|
|
71
|
+
CORS_ALLOWED_ORIGINS: corsAllowedOriginsSchema,
|
|
72
|
+
CORS_ALLOWED_ORIGIN_PATTERNS: corsAllowedOriginPatternsSchema,
|
|
62
73
|
BACKEND_URL: z.string().url().optional().default('http://localhost:3001'),
|
|
63
74
|
SENTRY_DSN: z.string().url().optional(),
|
|
64
75
|
EMAIL_HOST: z.string().optional().default('smtp.test.com'),
|
|
@@ -129,4 +140,4 @@ function validateEnv() {
|
|
|
129
140
|
export const env = validateEnv();
|
|
130
141
|
|
|
131
142
|
// Type-safe environment access
|
|
132
|
-
export type Env = z.infer<typeof envSchema>;
|
|
143
|
+
export type Env = z.infer<typeof envSchema>;
|
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
|
|
@@ -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
|