@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.
Files changed (241) hide show
  1. package/.env.example +45 -0
  2. package/.env.test.example +37 -0
  3. package/README.md +34 -7
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/clover.xml +12110 -0
  7. package/coverage/coverage-final.json +44 -0
  8. package/coverage/favicon.png +0 -0
  9. package/coverage/index.html +221 -0
  10. package/coverage/prettify.css +1 -0
  11. package/coverage/prettify.js +2 -0
  12. package/coverage/server/index.html +116 -0
  13. package/coverage/server/src/exportType.ts.html +109 -0
  14. package/coverage/server/src/index.html +161 -0
  15. package/coverage/server/src/index.ts.html +1702 -0
  16. package/coverage/server/src/instrument.ts.html +130 -0
  17. package/coverage/server/src/lib/config/env.ts.html +448 -0
  18. package/coverage/server/src/lib/config/index.html +116 -0
  19. package/coverage/server/src/lib/fileUpload.ts.html +1138 -0
  20. package/coverage/server/src/lib/googleCloudStorage.ts.html +334 -0
  21. package/coverage/server/src/lib/index.html +206 -0
  22. package/coverage/server/src/lib/jsonConversion.ts.html +2323 -0
  23. package/coverage/server/src/lib/jsonStyles.ts.html +193 -0
  24. package/coverage/server/src/lib/notificationHandler.ts.html +193 -0
  25. package/coverage/server/src/lib/pusher.ts.html +121 -0
  26. package/coverage/server/src/lib/thumbnailGenerator.ts.html +592 -0
  27. package/coverage/server/src/middleware/auth.ts.html +646 -0
  28. package/coverage/server/src/middleware/index.html +146 -0
  29. package/coverage/server/src/middleware/logging.ts.html +244 -0
  30. package/coverage/server/src/middleware/security.ts.html +271 -0
  31. package/coverage/server/src/routers/_app.ts.html +232 -0
  32. package/coverage/server/src/routers/agenda.ts.html +319 -0
  33. package/coverage/server/src/routers/announcement.ts.html +3481 -0
  34. package/coverage/server/src/routers/assignment.ts.html +7633 -0
  35. package/coverage/server/src/routers/attendance.ts.html +1030 -0
  36. package/coverage/server/src/routers/auth.ts.html +1081 -0
  37. package/coverage/server/src/routers/class.ts.html +3535 -0
  38. package/coverage/server/src/routers/comment.ts.html +991 -0
  39. package/coverage/server/src/routers/conversation.ts.html +982 -0
  40. package/coverage/server/src/routers/event.ts.html +1609 -0
  41. package/coverage/server/src/routers/file.ts.html +1144 -0
  42. package/coverage/server/src/routers/folder.ts.html +2797 -0
  43. package/coverage/server/src/routers/index.html +386 -0
  44. package/coverage/server/src/routers/labChat.ts.html +3073 -0
  45. package/coverage/server/src/routers/marketing.ts.html +340 -0
  46. package/coverage/server/src/routers/message.ts.html +1912 -0
  47. package/coverage/server/src/routers/notifications.ts.html +364 -0
  48. package/coverage/server/src/routers/section.ts.html +1120 -0
  49. package/coverage/server/src/routers/user.ts.html +862 -0
  50. package/coverage/server/src/routers/worksheet.ts.html +1729 -0
  51. package/coverage/server/src/trpc.ts.html +397 -0
  52. package/coverage/server/src/types/index.html +116 -0
  53. package/coverage/server/src/types/trpc.ts.html +127 -0
  54. package/coverage/server/src/utils/aiUser.ts.html +280 -0
  55. package/coverage/server/src/utils/email.ts.html +121 -0
  56. package/coverage/server/src/utils/generateInviteCode.ts.html +106 -0
  57. package/coverage/server/src/utils/index.html +206 -0
  58. package/coverage/server/src/utils/inference.ts.html +709 -0
  59. package/coverage/server/src/utils/logger.ts.html +664 -0
  60. package/coverage/server/src/utils/prismaErrorHandler.ts.html +907 -0
  61. package/coverage/server/src/utils/prismaWrapper.ts.html +355 -0
  62. package/coverage/server/vitest.config.ts.html +196 -0
  63. package/coverage/sort-arrow-sprite.png +0 -0
  64. package/coverage/sorter.js +210 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +83 -52
  67. package/dist/index.js.map +1 -1
  68. package/dist/instrument.js +15 -8
  69. package/dist/instrument.js.map +1 -1
  70. package/dist/lib/config/env.d.ts +169 -0
  71. package/dist/lib/config/env.d.ts.map +1 -0
  72. package/dist/lib/config/env.js +115 -0
  73. package/dist/lib/config/env.js.map +1 -0
  74. package/dist/lib/fileUpload.d.ts.map +1 -1
  75. package/dist/lib/fileUpload.js +5 -4
  76. package/dist/lib/fileUpload.js.map +1 -1
  77. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  78. package/dist/lib/googleCloudStorage.js +7 -8
  79. package/dist/lib/googleCloudStorage.js.map +1 -1
  80. package/dist/lib/jsonConversion.d.ts.map +1 -1
  81. package/dist/lib/jsonConversion.js +14 -16
  82. package/dist/lib/jsonConversion.js.map +1 -1
  83. package/dist/lib/notificationHandler.d.ts +2 -2
  84. package/dist/lib/prisma.d.ts +2 -2
  85. package/dist/lib/prisma.d.ts.map +1 -1
  86. package/dist/lib/prisma.js +22 -3
  87. package/dist/lib/prisma.js.map +1 -1
  88. package/dist/lib/pusher.d.ts.map +1 -1
  89. package/dist/lib/pusher.js +8 -7
  90. package/dist/lib/pusher.js.map +1 -1
  91. package/dist/middleware/auth.d.ts.map +1 -1
  92. package/dist/middleware/auth.js +7 -5
  93. package/dist/middleware/auth.js.map +1 -1
  94. package/dist/middleware/security.d.ts +5 -0
  95. package/dist/middleware/security.d.ts.map +1 -0
  96. package/dist/middleware/security.js +77 -0
  97. package/dist/middleware/security.js.map +1 -0
  98. package/dist/routers/_app.d.ts +368 -108
  99. package/dist/routers/_app.d.ts.map +1 -1
  100. package/dist/routers/_app.js +4 -2
  101. package/dist/routers/_app.js.map +1 -1
  102. package/dist/routers/agenda.d.ts.map +1 -1
  103. package/dist/routers/agenda.js +12 -9
  104. package/dist/routers/agenda.js.map +1 -1
  105. package/dist/routers/announcement.d.ts +8 -0
  106. package/dist/routers/announcement.d.ts.map +1 -1
  107. package/dist/routers/announcement.js +6 -4
  108. package/dist/routers/announcement.js.map +1 -1
  109. package/dist/routers/assignment.d.ts +17 -4
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/assignment.js +51 -19
  112. package/dist/routers/assignment.js.map +1 -1
  113. package/dist/routers/attendance.d.ts +1 -0
  114. package/dist/routers/attendance.d.ts.map +1 -1
  115. package/dist/routers/attendance.js +4 -4
  116. package/dist/routers/attendance.js.map +1 -1
  117. package/dist/routers/auth.d.ts +20 -0
  118. package/dist/routers/auth.d.ts.map +1 -1
  119. package/dist/routers/auth.js +132 -15
  120. package/dist/routers/auth.js.map +1 -1
  121. package/dist/routers/class.d.ts +10 -0
  122. package/dist/routers/class.d.ts.map +1 -1
  123. package/dist/routers/class.js +49 -5
  124. package/dist/routers/class.js.map +1 -1
  125. package/dist/routers/comment.d.ts +2 -0
  126. package/dist/routers/comment.d.ts.map +1 -1
  127. package/dist/routers/conversation.d.ts +2 -0
  128. package/dist/routers/conversation.d.ts.map +1 -1
  129. package/dist/routers/conversation.js +46 -31
  130. package/dist/routers/conversation.js.map +1 -1
  131. package/dist/routers/file.d.ts.map +1 -1
  132. package/dist/routers/file.js +30 -7
  133. package/dist/routers/file.js.map +1 -1
  134. package/dist/routers/labChat.d.ts +2 -0
  135. package/dist/routers/labChat.d.ts.map +1 -1
  136. package/dist/routers/labChat.js +5 -322
  137. package/dist/routers/labChat.js.map +1 -1
  138. package/dist/routers/marketing.d.ts +1 -1
  139. package/dist/routers/message.d.ts +1 -0
  140. package/dist/routers/message.d.ts.map +1 -1
  141. package/dist/routers/message.js +3 -2
  142. package/dist/routers/message.js.map +1 -1
  143. package/dist/routers/newtonChat.d.ts +55 -0
  144. package/dist/routers/newtonChat.d.ts.map +1 -0
  145. package/dist/routers/newtonChat.js +262 -0
  146. package/dist/routers/newtonChat.js.map +1 -0
  147. package/dist/routers/notifications.d.ts +4 -4
  148. package/dist/routers/section.d.ts +19 -4
  149. package/dist/routers/section.d.ts.map +1 -1
  150. package/dist/routers/section.js +26 -8
  151. package/dist/routers/section.js.map +1 -1
  152. package/dist/routers/user.d.ts.map +1 -1
  153. package/dist/routers/user.js +5 -4
  154. package/dist/routers/user.js.map +1 -1
  155. package/dist/routers/worksheet.d.ts +44 -41
  156. package/dist/routers/worksheet.d.ts.map +1 -1
  157. package/dist/routers/worksheet.js +25 -34
  158. package/dist/routers/worksheet.js.map +1 -1
  159. package/dist/seedDatabase.d.ts +1 -1
  160. package/dist/seedDatabase.js +275 -284
  161. package/dist/seedDatabase.js.map +1 -1
  162. package/dist/server/pipelines/aiLabChat.d.ts +21 -0
  163. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  164. package/dist/server/pipelines/aiLabChat.js +456 -0
  165. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  166. package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
  167. package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
  168. package/dist/server/pipelines/aiNewtonChat.js +280 -0
  169. package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
  170. package/dist/server/pipelines/gradeWorksheet.d.ts +15 -0
  171. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  172. package/dist/server/pipelines/gradeWorksheet.js +139 -0
  173. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  174. package/dist/trpc.d.ts.map +1 -1
  175. package/dist/trpc.js +2 -2
  176. package/dist/trpc.js.map +1 -1
  177. package/dist/utils/email.d.ts +9 -1
  178. package/dist/utils/email.d.ts.map +1 -1
  179. package/dist/utils/email.js +20 -5
  180. package/dist/utils/email.js.map +1 -1
  181. package/dist/utils/inference.d.ts +5 -0
  182. package/dist/utils/inference.d.ts.map +1 -1
  183. package/dist/utils/inference.js +71 -7
  184. package/dist/utils/inference.js.map +1 -1
  185. package/dist/utils/logger.d.ts.map +1 -1
  186. package/dist/utils/logger.js +3 -3
  187. package/dist/utils/logger.js.map +1 -1
  188. package/docker-compose.yml +14 -0
  189. package/package.json +13 -4
  190. package/prisma/schema.prisma +34 -5
  191. package/scripts/test-pre-push.ts +14 -0
  192. package/src/index.ts +98 -54
  193. package/src/instrument.ts +13 -6
  194. package/src/lib/config/env.ts +126 -0
  195. package/src/lib/fileUpload.ts +3 -2
  196. package/src/lib/googleCloudStorage.ts +6 -6
  197. package/src/lib/jsonConversion.ts +12 -14
  198. package/src/lib/prisma.ts +23 -2
  199. package/src/lib/pusher.ts +6 -5
  200. package/src/middleware/auth.ts +5 -3
  201. package/src/middleware/security.ts +80 -0
  202. package/src/routers/_app.ts +2 -0
  203. package/src/routers/agenda.ts +10 -7
  204. package/src/routers/announcement.ts +4 -2
  205. package/src/routers/assignment.ts +74 -41
  206. package/src/routers/attendance.ts +2 -2
  207. package/src/routers/auth.ts +143 -14
  208. package/src/routers/class.ts +52 -3
  209. package/src/routers/conversation.ts +49 -29
  210. package/src/routers/file.ts +29 -5
  211. package/src/routers/labChat.ts +3 -367
  212. package/src/routers/message.ts +1 -1
  213. package/src/routers/newtonChat.ts +299 -0
  214. package/src/routers/section.ts +26 -6
  215. package/src/routers/user.ts +3 -2
  216. package/src/routers/worksheet.ts +26 -38
  217. package/src/seedDatabase.ts +290 -283
  218. package/src/server/pipelines/aiLabChat.ts +507 -0
  219. package/src/server/pipelines/aiNewtonChat.ts +338 -0
  220. package/src/server/pipelines/gradeWorksheet.ts +151 -0
  221. package/src/trpc.ts +2 -0
  222. package/src/utils/email.ts +30 -3
  223. package/src/utils/inference.ts +85 -5
  224. package/src/utils/logger.ts +2 -1
  225. package/tests/announcement.test.ts +164 -0
  226. package/tests/assignment.test.ts +296 -0
  227. package/tests/attendance.test.ts +168 -0
  228. package/tests/auth.test.ts +33 -10
  229. package/tests/class.test.ts +34 -9
  230. package/tests/event.test.ts +228 -0
  231. package/tests/section.test.ts +216 -0
  232. package/tests/setup.ts +70 -16
  233. package/tests/user.test.ts +158 -0
  234. package/vitest.config.ts +26 -0
  235. package/API_SPECIFICATION.md +0 -1597
  236. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  237. package/CHAT_API_SPEC.md +0 -579
  238. package/LAB_CHAT_API_SPEC.md +0 -518
  239. package/dist/routers/school.d.ts +0 -208
  240. package/dist/routers/school.d.ts.map +0 -1
  241. 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 (process.env.NODE_ENV !== "development") {
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
- app.get('/test-sentry', (req, res) => {
123
- throw new Error('Test error');
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
- process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
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
- process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
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 = process.env.PORT || 3001;
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: process.env.NODE_ENV,
468
- PORT: process.env.PORT,
469
- NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
470
- LOG_MODE: process.env.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
- process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
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.init({
4
- dsn: "https://5440f33877e8e19d7644624f5d3d2f10@o4510401976860672.ingest.de.sentry.io/4510402093514832",
5
- // Setting this option to true will send default PII data to Sentry.
6
- // For example, automatic IP address collection on events
7
- sendDefaultPii: true,
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>;
@@ -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 = process.env.BACKEND_URL || 'http://localhost:3001';
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(process.env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
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
- import dotenv from 'dotenv';
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: process.env.GOOGLE_CLOUD_PROJECT_ID,
7
+ projectId: env.GOOGLE_CLOUD_PROJECT_ID,
8
8
  credentials: {
9
- client_email: process.env.GOOGLE_CLOUD_CLIENT_EMAIL,
10
- private_key: process.env.GOOGLE_CLOUD_PRIVATE_KEY?.replace(/\\n/g, '\n'),
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(process.env.GOOGLE_CLOUD_BUCKET_NAME!);
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
- console.log('createPdf: Starting PDF creation with', blocks.length, 'blocks');
10
+ logger.info(`createPdf: Starting PDF creation with ${blocks.length} blocks`);
10
11
  try {
11
12
  const pdfDoc = await PDFDocument.create()
12
- console.log('createPdf: PDFDocument created successfully');
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
- console.log('createPdf: Unicode fonts loaded successfully');
37
+ logger.info('createPdf: Unicode fonts loaded successfully');
37
38
  } catch (fontError) {
38
- console.warn('createPdf: Failed to load custom fonts, falling back to standard fonts:', fontError);
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
- console.log('createPdf: Starting to process', blocks.length, 'blocks');
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
- console.log(`createPdf: Processing block ${i + 1}/${blocks.length}, format: ${block.format}, content type: ${typeof block.content}`);
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
- console.log(`createPdf: Successfully processed block ${i + 1}`);
729
+ logger.info(`createPdf: Successfully processed block ${i + 1}`);
729
730
  y -= paragraphSpacing
730
731
  lastLineHeight = lineHeight
731
732
  } catch (blockError) {
732
- console.error(`createPdf: Error processing block ${i + 1}:`, blockError);
733
+ logger.error(`createPdf: Error processing block ${i + 1}: ${blockError}`);
733
734
  throw blockError;
734
735
  }
735
736
  }
736
737
 
737
- console.log('createPdf: About to save PDF document');
738
+ logger.info('createPdf: About to save PDF document');
738
739
  const pdfBytes = await pdfDoc.save()
739
- console.log('createPdf: PDF saved successfully, bytes length:', pdfBytes.length);
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
- console.error('createPdf: Error during PDF creation:', error);
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 (process.env.NODE_ENV !== 'production') {
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: process.env.PUSHER_APP_ID!,
5
- key: process.env.PUSHER_KEY!,
6
- secret: process.env.PUSHER_SECRET!,
7
- cluster: process.env.PUSHER_CLUSTER!,
8
- useTLS: true,
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 };
@@ -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
- console.log(error)
54
+ Sentry.captureException(error);
55
+ console.error(error);
54
56
  throw new TRPCError({
55
- code: 'UNAUTHORIZED',
56
- message: 'Invalid user data',
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
+ });