@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/index.ts
CHANGED
|
@@ -11,58 +11,57 @@ import { logger } from './utils/logger.js';
|
|
|
11
11
|
import { setupSocketHandlers } from './socket/handlers.js';
|
|
12
12
|
import { bucket } from './lib/googleCloudStorage.js';
|
|
13
13
|
import { prisma } from './lib/prisma.js';
|
|
14
|
+
|
|
15
|
+
import { authLimiter, generalLimiter, helmetConfig, uploadLimiter } from './middleware/security.js';
|
|
16
|
+
|
|
14
17
|
import * as Sentry from "@sentry/node";
|
|
18
|
+
import { env } from './lib/config/env.js';
|
|
19
|
+
import compression from 'compression';
|
|
20
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
15
21
|
|
|
16
|
-
import { renderTrpcPanel } from "trpc-panel";
|
|
17
22
|
|
|
18
23
|
import "./instrument.js";
|
|
19
|
-
|
|
20
|
-
dotenv.config();
|
|
24
|
+
import { openAIClient } from './utils/inference.js';
|
|
21
25
|
|
|
22
26
|
const app = express();
|
|
23
27
|
|
|
28
|
+
app.use(helmetConfig);
|
|
29
|
+
app.use(compression());
|
|
30
|
+
|
|
31
|
+
app.use((req, res, next) => {
|
|
32
|
+
const requestId = uuidv4();
|
|
33
|
+
res.setHeader('X-Request-ID', requestId);
|
|
34
|
+
next();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.use(generalLimiter);
|
|
38
|
+
|
|
39
|
+
const allowedOrigins = env.NODE_ENV === 'production'
|
|
40
|
+
? [
|
|
41
|
+
'https://www.studious.sh',
|
|
42
|
+
'https://studious.sh',
|
|
43
|
+
env.NEXT_PUBLIC_APP_URL,
|
|
44
|
+
'http://localhost:3000',
|
|
45
|
+
|
|
46
|
+
].filter(Boolean)
|
|
47
|
+
: [
|
|
48
|
+
'http://localhost:3000',
|
|
49
|
+
'http://localhost:3001',
|
|
50
|
+
'http://127.0.0.1:3000',
|
|
51
|
+
'http://127.0.0.1:3001',
|
|
52
|
+
|
|
53
|
+
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
54
|
+
];
|
|
55
|
+
|
|
24
56
|
// CORS middleware
|
|
25
57
|
app.use(cors({
|
|
26
|
-
origin:
|
|
27
|
-
'http://localhost:3000', // Frontend development server
|
|
28
|
-
'http://localhost:3001', // Server port
|
|
29
|
-
'http://127.0.0.1:3000', // Alternative localhost
|
|
30
|
-
'http://127.0.0.1:3001', // Alternative localhost
|
|
31
|
-
'https://www.studious.sh', // Production frontend
|
|
32
|
-
'https://studious.sh', // Production frontend (without www)
|
|
33
|
-
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
34
|
-
],
|
|
58
|
+
origin: allowedOrigins,
|
|
35
59
|
credentials: true,
|
|
36
60
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
37
61
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-user'],
|
|
38
62
|
optionsSuccessStatus: 200
|
|
39
63
|
}));
|
|
40
64
|
|
|
41
|
-
// Handle preflight OPTIONS requests
|
|
42
|
-
app.options('*', (req, res) => {
|
|
43
|
-
const allowedOrigins = [
|
|
44
|
-
'http://localhost:3000',
|
|
45
|
-
'http://localhost:3001',
|
|
46
|
-
'http://127.0.0.1:3000',
|
|
47
|
-
'http://127.0.0.1:3001',
|
|
48
|
-
'https://www.studious.sh', // Production frontend
|
|
49
|
-
'https://studious.sh', // Production frontend (without www)
|
|
50
|
-
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
const origin = req.headers.origin;
|
|
54
|
-
if (origin && allowedOrigins.includes(origin)) {
|
|
55
|
-
res.header('Access-Control-Allow-Origin', origin);
|
|
56
|
-
} else {
|
|
57
|
-
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
61
|
-
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, x-user');
|
|
62
|
-
res.header('Access-Control-Allow-Credentials', 'true');
|
|
63
|
-
res.sendStatus(200);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
65
|
// CORS debugging middleware
|
|
67
66
|
app.use((req, res, next) => {
|
|
68
67
|
if (req.method === 'OPTIONS' || req.path.includes('trpc')) {
|
|
@@ -92,7 +91,7 @@ app.use((req, res, next) => {
|
|
|
92
91
|
});
|
|
93
92
|
|
|
94
93
|
app.use("/panel", async (_, res) => {
|
|
95
|
-
if (
|
|
94
|
+
if (env.NODE_ENV !== "development") {
|
|
96
95
|
return res.status(404).send("Not Found");
|
|
97
96
|
}
|
|
98
97
|
|
|
@@ -115,12 +114,25 @@ app.use("/panel", async (_, res) => {
|
|
|
115
114
|
// Create HTTP server
|
|
116
115
|
const httpServer = createServer(app);
|
|
117
116
|
|
|
118
|
-
app.get('/health', (req, res) => {
|
|
119
|
-
res.status(200).json({ message: 'OK' });
|
|
120
|
-
});
|
|
117
|
+
app.get('/health', async (req, res) => {
|
|
121
118
|
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
try {
|
|
120
|
+
// Check database connectivity
|
|
121
|
+
await prisma.$queryRaw`SELECT 1`;
|
|
122
|
+
|
|
123
|
+
res.status(200).json({
|
|
124
|
+
status: 'OK',
|
|
125
|
+
timestamp: new Date().toISOString(),
|
|
126
|
+
uptime: process.uptime(),
|
|
127
|
+
database: 'connected'
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
res.status(503).json({
|
|
131
|
+
status: 'ERROR',
|
|
132
|
+
database: 'disconnected',
|
|
133
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
124
136
|
});
|
|
125
137
|
|
|
126
138
|
// Setup Socket.IO
|
|
@@ -133,7 +145,7 @@ const io = new Server(httpServer, {
|
|
|
133
145
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
134
146
|
'https://www.studious.sh', // Production frontend
|
|
135
147
|
'https://studious.sh', // Production frontend (without www)
|
|
136
|
-
|
|
148
|
+
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
137
149
|
],
|
|
138
150
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
139
151
|
credentials: true,
|
|
@@ -356,12 +368,15 @@ app.get('/api/files/:fileId', async (req, res) => {
|
|
|
356
368
|
}
|
|
357
369
|
});
|
|
358
370
|
|
|
371
|
+
app.use('/trpc/auth.login', authLimiter);
|
|
372
|
+
app.use('/trpc/auth.register', authLimiter);
|
|
373
|
+
|
|
359
374
|
// File upload endpoint for secure file uploads (supports both POST and PUT)
|
|
360
|
-
app.post('/api/upload/:filePath', async (req, res) => {
|
|
375
|
+
app.post('/api/upload/:filePath', uploadLimiter, async (req, res) => {
|
|
361
376
|
handleFileUpload(req, res);
|
|
362
377
|
});
|
|
363
378
|
|
|
364
|
-
app.put('/api/upload/:filePath', async (req, res) => {
|
|
379
|
+
app.put('/api/upload/:filePath', uploadLimiter, async (req, res) => {
|
|
365
380
|
handleFileUpload(req, res);
|
|
366
381
|
});
|
|
367
382
|
|
|
@@ -378,7 +393,7 @@ function handleFileUpload(req: any, res: any) {
|
|
|
378
393
|
'http://127.0.0.1:3001',
|
|
379
394
|
'https://www.studious.sh', // Production frontend
|
|
380
395
|
'https://studious.sh', // Production frontend (without www)
|
|
381
|
-
|
|
396
|
+
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
382
397
|
];
|
|
383
398
|
|
|
384
399
|
if (origin && allowedOrigins.includes(origin)) {
|
|
@@ -453,7 +468,7 @@ Sentry.setupExpressErrorHandler(app);
|
|
|
453
468
|
// });
|
|
454
469
|
|
|
455
470
|
|
|
456
|
-
const PORT =
|
|
471
|
+
const PORT = env.PORT || 3001;
|
|
457
472
|
|
|
458
473
|
httpServer.listen(PORT, () => {
|
|
459
474
|
logger.info(`Server running on port ${PORT}`, {
|
|
@@ -464,10 +479,10 @@ httpServer.listen(PORT, () => {
|
|
|
464
479
|
|
|
465
480
|
// log all env variables
|
|
466
481
|
logger.info('Configurations', {
|
|
467
|
-
NODE_ENV:
|
|
468
|
-
PORT:
|
|
469
|
-
NEXT_PUBLIC_APP_URL:
|
|
470
|
-
LOG_MODE:
|
|
482
|
+
NODE_ENV: env.NODE_ENV,
|
|
483
|
+
PORT: env.PORT,
|
|
484
|
+
NEXT_PUBLIC_APP_URL: env.NEXT_PUBLIC_APP_URL,
|
|
485
|
+
LOG_MODE: env.LOG_MODE,
|
|
471
486
|
});
|
|
472
487
|
|
|
473
488
|
// Log CORS configuration
|
|
@@ -477,6 +492,35 @@ logger.info('CORS Configuration', {
|
|
|
477
492
|
'http://localhost:3001',
|
|
478
493
|
'http://127.0.0.1:3000',
|
|
479
494
|
'http://127.0.0.1:3001',
|
|
480
|
-
|
|
495
|
+
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
481
496
|
]
|
|
482
|
-
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const gracefulShutdown = (signal: string) => {
|
|
500
|
+
logger.info(`Received ${signal}, shutting down gracefully`);
|
|
501
|
+
|
|
502
|
+
httpServer.close(() => {
|
|
503
|
+
logger.info('HTTP server closed');
|
|
504
|
+
|
|
505
|
+
io.close(() => {
|
|
506
|
+
logger.info('Socket.IO server closed');
|
|
507
|
+
|
|
508
|
+
prisma.$disconnect().then(() => {
|
|
509
|
+
logger.info('Database connections closed');
|
|
510
|
+
process.exit(0);
|
|
511
|
+
}).catch((err) => {
|
|
512
|
+
logger.error('Error disconnecting from database', { error: err });
|
|
513
|
+
process.exit(1);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Force shutdown after 10 seconds
|
|
519
|
+
setTimeout(() => {
|
|
520
|
+
logger.error('Forced shutdown after timeout');
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}, 10000);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
526
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
package/src/instrument.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/node";
|
|
2
|
+
import { env } from "./lib/config/env.js";
|
|
2
3
|
|
|
3
|
-
Sentry
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
// Only initialize Sentry in non-test environments
|
|
5
|
+
if (env.NODE_ENV !== 'test') {
|
|
6
|
+
Sentry.init({
|
|
7
|
+
dsn: env.SENTRY_DSN,
|
|
8
|
+
environment: env.NODE_ENV || 'development',
|
|
9
|
+
// Setting this option to true will send default PII data to Sentry.
|
|
10
|
+
// For example, automatic IP address collection on events
|
|
11
|
+
sendDefaultPii: true,
|
|
12
|
+
// @todo: disable in test environment
|
|
13
|
+
enabled: true, // Explicitly disable in test environment
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
// Determine which env file to load based on NODE_ENV
|
|
7
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
8
|
+
const envFileMap: Record<string, string> = {
|
|
9
|
+
test: '.env.test',
|
|
10
|
+
development: '.env.development',
|
|
11
|
+
production: '.env.production',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Load the appropriate env file
|
|
15
|
+
const envFile = envFileMap[nodeEnv] || '.env';
|
|
16
|
+
const envPath = resolve(process.cwd(), envFile);
|
|
17
|
+
|
|
18
|
+
// Load environment variables from the correct file
|
|
19
|
+
// First load .env (base), then override with environment-specific file
|
|
20
|
+
dotenv.config(); // Load .env first (base config)
|
|
21
|
+
dotenv.config({ path: envPath, override: true }); // Override with env-specific
|
|
22
|
+
|
|
23
|
+
const isTest = nodeEnv === 'test';
|
|
24
|
+
const isProduction = nodeEnv === 'production';
|
|
25
|
+
|
|
26
|
+
// Base schema with required vars for all environments
|
|
27
|
+
const baseSchema = z.object({
|
|
28
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
29
|
+
PORT: z.string().transform(Number).default('3001'),
|
|
30
|
+
DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Production/development schema with all required vars
|
|
34
|
+
const fullSchema = baseSchema.extend({
|
|
35
|
+
NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
|
|
36
|
+
BACKEND_URL: z.string().url().default('http://localhost:3001'),
|
|
37
|
+
SENTRY_DSN: z.string().url().optional(),
|
|
38
|
+
EMAIL_HOST: z.string().min(1, 'EMAIL_HOST is required'),
|
|
39
|
+
EMAIL_USER: z.string().email('EMAIL_USER must be a valid email'),
|
|
40
|
+
EMAIL_PASS: z.string().min(1, 'EMAIL_PASS is required'),
|
|
41
|
+
EMAIL_DRY_RUN: z.string().optional().default('false'),
|
|
42
|
+
GOOGLE_CLOUD_PROJECT_ID: z.string().min(1, 'GOOGLE_CLOUD_PROJECT_ID is required'),
|
|
43
|
+
GOOGLE_CLOUD_CLIENT_EMAIL: z.string().email('GOOGLE_CLOUD_CLIENT_EMAIL must be a valid email'),
|
|
44
|
+
GOOGLE_CLOUD_PRIVATE_KEY: z.string().min(1, 'GOOGLE_CLOUD_PRIVATE_KEY is required'),
|
|
45
|
+
GOOGLE_CLOUD_BUCKET_NAME: z.string().min(1, 'GOOGLE_CLOUD_BUCKET_NAME is required'),
|
|
46
|
+
PUSHER_APP_ID: z.string().min(1, 'PUSHER_APP_ID is required'),
|
|
47
|
+
PUSHER_KEY: z.string().min(1, 'PUSHER_KEY is required'),
|
|
48
|
+
PUSHER_SECRET: z.string().min(1, 'PUSHER_SECRET is required'),
|
|
49
|
+
PUSHER_CLUSTER: z.string().min(1, 'PUSHER_CLUSTER is required'),
|
|
50
|
+
INFERENCE_API_KEY: z.string().optional(),
|
|
51
|
+
INFERENCE_API_BASE_URL: z.string().url().optional(),
|
|
52
|
+
OPENAI_API_KEY: z.string().optional(),
|
|
53
|
+
LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('normal'),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Test schema - only require what's needed for tests
|
|
57
|
+
const testSchema = baseSchema.extend({
|
|
58
|
+
NEXT_PUBLIC_APP_URL: z.string().url().optional().default('http://localhost:3000'),
|
|
59
|
+
BACKEND_URL: z.string().url().optional().default('http://localhost:3001'),
|
|
60
|
+
SENTRY_DSN: z.string().url().optional(),
|
|
61
|
+
EMAIL_HOST: z.string().optional().default('smtp.test.com'),
|
|
62
|
+
EMAIL_USER: z.string().email().optional().default('test@test.com'),
|
|
63
|
+
EMAIL_PASS: z.string().optional().default('test'),
|
|
64
|
+
EMAIL_DRY_RUN: z.string().optional().default('false'),
|
|
65
|
+
GOOGLE_CLOUD_PROJECT_ID: z.string().optional().default('test-project'),
|
|
66
|
+
GOOGLE_CLOUD_CLIENT_EMAIL: z.string().email().optional().default('test@test.iam.gserviceaccount.com'),
|
|
67
|
+
GOOGLE_CLOUD_PRIVATE_KEY: z.string().optional().default('test-key'),
|
|
68
|
+
GOOGLE_CLOUD_BUCKET_NAME: z.string().optional().default('test-bucket'),
|
|
69
|
+
PUSHER_APP_ID: z.string().optional().default('test-app-id'),
|
|
70
|
+
PUSHER_KEY: z.string().optional().default('test-key'),
|
|
71
|
+
PUSHER_SECRET: z.string().optional().default('test-secret'),
|
|
72
|
+
PUSHER_CLUSTER: z.string().optional().default('us2'),
|
|
73
|
+
INFERENCE_API_KEY: z.string().optional(),
|
|
74
|
+
OPENAI_API_KEY: z.string().optional(),
|
|
75
|
+
INFERENCE_API_BASE_URL: z.string().url().optional(),
|
|
76
|
+
LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('quiet'),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Use test schema in test mode, full schema otherwise
|
|
80
|
+
const envSchema = isTest ? testSchema : fullSchema;
|
|
81
|
+
|
|
82
|
+
// Validate environment variables
|
|
83
|
+
function validateEnv() {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = envSchema.parse(process.env);
|
|
86
|
+
|
|
87
|
+
// Only exit on validation failure in production
|
|
88
|
+
if (isProduction && !parsed.DATABASE_URL) {
|
|
89
|
+
logger.error('DATABASE_URL is required in production');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return parsed;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof z.ZodError) {
|
|
96
|
+
const missingVars = error.errors.map(err => ({
|
|
97
|
+
path: err.path.join('.'),
|
|
98
|
+
message: err.message,
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
logger.error('Environment variable validation failed', {
|
|
102
|
+
envFile,
|
|
103
|
+
missingVars,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Only exit in production - in test/dev, log warning but continue
|
|
107
|
+
if (isProduction) {
|
|
108
|
+
logger.error(`Please check your ${envFile} file and ensure all required variables are set.`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
} else {
|
|
111
|
+
logger.warn('Continuing with defaults - some features may not work correctly', {
|
|
112
|
+
envFile,
|
|
113
|
+
});
|
|
114
|
+
// Return parsed with defaults for non-production
|
|
115
|
+
return envSchema.parse({ ...process.env });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Export validated environment variables
|
|
123
|
+
export const env = validateEnv();
|
|
124
|
+
|
|
125
|
+
// Type-safe environment access
|
|
126
|
+
export type Env = z.infer<typeof envSchema>;
|
package/src/lib/fileUpload.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getSignedUrl, objectExists } from "./googleCloudStorage.js";
|
|
|
4
4
|
import { generateMediaThumbnail } from "./thumbnailGenerator.js";
|
|
5
5
|
import { prisma } from "./prisma.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { env } from "./config/env.js";
|
|
7
8
|
|
|
8
9
|
export interface FileData {
|
|
9
10
|
name: string;
|
|
@@ -136,7 +137,7 @@ export async function createDirectUploadFile(
|
|
|
136
137
|
const uploadSessionId = uuidv4();
|
|
137
138
|
|
|
138
139
|
// Generate backend proxy upload URL (not direct GCS)
|
|
139
|
-
const baseUrl =
|
|
140
|
+
const baseUrl = env.BACKEND_URL || 'http://localhost:3001';
|
|
140
141
|
const uploadUrl = `${baseUrl}/api/upload/${encodeURIComponent(filePath)}`;
|
|
141
142
|
const uploadExpiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes from now
|
|
142
143
|
|
|
@@ -231,7 +232,7 @@ export async function confirmDirectUpload(
|
|
|
231
232
|
// If uploadSuccess is true, verify the object actually exists in GCS
|
|
232
233
|
if (uploadSuccess) {
|
|
233
234
|
try {
|
|
234
|
-
const exists = await objectExists(
|
|
235
|
+
const exists = await objectExists(env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
|
|
235
236
|
if (!exists) {
|
|
236
237
|
actualUploadSuccess = false;
|
|
237
238
|
actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
dotenv.config();
|
|
1
|
+
|
|
3
2
|
import { Storage } from '@google-cloud/storage';
|
|
4
3
|
import { TRPCError } from '@trpc/server';
|
|
4
|
+
import { env } from './config/env.js';
|
|
5
5
|
|
|
6
6
|
const storage = new Storage({
|
|
7
|
-
projectId:
|
|
7
|
+
projectId: env.GOOGLE_CLOUD_PROJECT_ID,
|
|
8
8
|
credentials: {
|
|
9
|
-
client_email:
|
|
10
|
-
private_key:
|
|
9
|
+
client_email: env.GOOGLE_CLOUD_CLIENT_EMAIL,
|
|
10
|
+
private_key: env.GOOGLE_CLOUD_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
|
11
11
|
},
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
export const bucket = storage.bucket(
|
|
14
|
+
export const bucket = storage.bucket(env.GOOGLE_CLOUD_BUCKET_NAME!);
|
|
15
15
|
|
|
16
16
|
// Short expiration time for signed URLs (5 minutes)
|
|
17
17
|
const SIGNED_URL_EXPIRATION = 5 * 60 * 1000;
|
|
@@ -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,11 @@ export const createAuthMiddleware = (t: any) => {
|
|
|
50
51
|
},
|
|
51
52
|
});
|
|
52
53
|
} catch (error) {
|
|
53
|
-
|
|
54
|
+
Sentry.captureException(error);
|
|
55
|
+
console.error(error);
|
|
54
56
|
throw new TRPCError({
|
|
55
|
-
code: '
|
|
56
|
-
message: '
|
|
57
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
58
|
+
message: 'Internal server error',
|
|
57
59
|
});
|
|
58
60
|
}
|
|
59
61
|
});
|
|
@@ -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
|
+
});
|