@studious-lms/server 1.2.45 → 1.2.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) 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 +6 -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 +294 -98
  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 +7 -4
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/assignment.js +35 -18
  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 +1 -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 +1 -0
  135. package/dist/routers/labChat.d.ts.map +1 -1
  136. package/dist/routers/labChat.js +2 -3
  137. package/dist/routers/labChat.js.map +1 -1
  138. package/dist/routers/marketing.d.ts +1 -1
  139. package/dist/routers/newtonChat.d.ts +55 -0
  140. package/dist/routers/newtonChat.d.ts.map +1 -0
  141. package/dist/routers/newtonChat.js +438 -0
  142. package/dist/routers/newtonChat.js.map +1 -0
  143. package/dist/routers/notifications.d.ts +4 -4
  144. package/dist/routers/section.d.ts +9 -4
  145. package/dist/routers/section.d.ts.map +1 -1
  146. package/dist/routers/section.js +8 -8
  147. package/dist/routers/section.js.map +1 -1
  148. package/dist/routers/user.d.ts.map +1 -1
  149. package/dist/routers/user.js +5 -4
  150. package/dist/routers/user.js.map +1 -1
  151. package/dist/routers/worksheet.d.ts +30 -36
  152. package/dist/routers/worksheet.d.ts.map +1 -1
  153. package/dist/routers/worksheet.js +11 -33
  154. package/dist/routers/worksheet.js.map +1 -1
  155. package/dist/seedDatabase.d.ts +1 -1
  156. package/dist/seedDatabase.js +275 -284
  157. package/dist/seedDatabase.js.map +1 -1
  158. package/dist/server/pipelines/aiLabChat.d.ts +10 -0
  159. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  160. package/dist/server/pipelines/aiLabChat.js +83 -0
  161. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  162. package/dist/server/pipelines/gradeWorksheet.d.ts +2 -0
  163. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  164. package/dist/server/pipelines/gradeWorksheet.js +138 -0
  165. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  166. package/dist/trpc.d.ts.map +1 -1
  167. package/dist/trpc.js +2 -2
  168. package/dist/trpc.js.map +1 -1
  169. package/dist/utils/email.d.ts +9 -1
  170. package/dist/utils/email.d.ts.map +1 -1
  171. package/dist/utils/email.js +20 -5
  172. package/dist/utils/email.js.map +1 -1
  173. package/dist/utils/inference.d.ts +3 -0
  174. package/dist/utils/inference.d.ts.map +1 -1
  175. package/dist/utils/inference.js +41 -7
  176. package/dist/utils/inference.js.map +1 -1
  177. package/dist/utils/logger.d.ts.map +1 -1
  178. package/dist/utils/logger.js +3 -3
  179. package/dist/utils/logger.js.map +1 -1
  180. package/docker-compose.yml +14 -0
  181. package/package.json +13 -4
  182. package/prisma/schema.prisma +32 -5
  183. package/scripts/test-pre-push.ts +14 -0
  184. package/src/index.ts +98 -54
  185. package/src/instrument.ts +13 -6
  186. package/src/lib/config/env.ts +126 -0
  187. package/src/lib/fileUpload.ts +3 -2
  188. package/src/lib/googleCloudStorage.ts +6 -6
  189. package/src/lib/jsonConversion.ts +12 -14
  190. package/src/lib/prisma.ts +23 -2
  191. package/src/lib/pusher.ts +6 -5
  192. package/src/middleware/auth.ts +4 -3
  193. package/src/middleware/security.ts +80 -0
  194. package/src/routers/_app.ts +2 -0
  195. package/src/routers/agenda.ts +10 -7
  196. package/src/routers/announcement.ts +4 -2
  197. package/src/routers/assignment.ts +58 -40
  198. package/src/routers/attendance.ts +2 -2
  199. package/src/routers/auth.ts +143 -14
  200. package/src/routers/class.ts +52 -3
  201. package/src/routers/conversation.ts +49 -29
  202. package/src/routers/file.ts +29 -5
  203. package/src/routers/labChat.ts +0 -1
  204. package/src/routers/newtonChat.ts +520 -0
  205. package/src/routers/section.ts +6 -6
  206. package/src/routers/user.ts +3 -2
  207. package/src/routers/worksheet.ts +9 -37
  208. package/src/seedDatabase.ts +290 -283
  209. package/src/server/pipelines/aiLabChat.ts +92 -0
  210. package/src/server/pipelines/gradeWorksheet.ts +152 -0
  211. package/src/trpc.ts +2 -0
  212. package/src/utils/email.ts +30 -3
  213. package/src/utils/inference.ts +50 -5
  214. package/src/utils/logger.ts +2 -1
  215. package/tests/announcement.test.ts +164 -0
  216. package/tests/assignment.test.ts +296 -0
  217. package/tests/attendance.test.ts +168 -0
  218. package/tests/auth.test.ts +33 -10
  219. package/tests/class.test.ts +34 -9
  220. package/tests/event.test.ts +228 -0
  221. package/tests/section.test.ts +216 -0
  222. package/tests/setup.ts +70 -16
  223. package/tests/user.test.ts +158 -0
  224. package/vitest.config.ts +26 -0
  225. package/API_SPECIFICATION.md +0 -1597
  226. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  227. package/CHAT_API_SPEC.md +0 -579
  228. package/LAB_CHAT_API_SPEC.md +0 -518
  229. package/dist/routers/school.d.ts +0 -208
  230. package/dist/routers/school.d.ts.map +0 -1
  231. package/dist/routers/school.js +0 -483
@@ -1 +1 @@
1
- {"version":3,"file":"logger.js","sources":["utils/logger.ts"],"sourceRoot":"/","sourcesContent":["export enum LogLevel {\n INFO = 'info',\n WARN = 'warn',\n ERROR = 'error',\n DEBUG = 'debug'\n}\n\ntype LogMode = 'silent' | 'minimal' | 'normal' | 'verbose';\n\ninterface LogMessage {\n level: LogLevel;\n message: string;\n timestamp: string;\n context?: Record<string, any>;\n}\n\n// ANSI color codes\nconst colors = {\n reset: '\\x1b[0m',\n bright: '\\x1b[1m',\n dim: '\\x1b[2m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n blue: '\\x1b[34m',\n magenta: '\\x1b[35m',\n cyan: '\\x1b[36m',\n white: '\\x1b[37m',\n gray: '\\x1b[90m',\n // Background colors\n bgRed: '\\x1b[41m',\n bgGreen: '\\x1b[42m',\n bgYellow: '\\x1b[43m',\n bgBlue: '\\x1b[44m',\n bgMagenta: '\\x1b[45m',\n bgCyan: '\\x1b[46m',\n bgWhite: '\\x1b[47m',\n bgGray: '\\x1b[100m',\n // Bright background colors\n bgBrightRed: '\\x1b[101m',\n bgBrightGreen: '\\x1b[102m',\n bgBrightYellow: '\\x1b[103m',\n bgBrightBlue: '\\x1b[104m',\n bgBrightMagenta: '\\x1b[105m',\n bgBrightCyan: '\\x1b[106m',\n bgBrightWhite: '\\x1b[107m'\n};\n\nclass Logger {\n private static instance: Logger;\n private isDevelopment: boolean;\n private mode: LogMode;\n private levelColors: Record<LogLevel, string>;\n private levelBgColors: Record<LogLevel, string>;\n private levelEmojis: Record<LogLevel, string>;\n\n private constructor() {\n\n // this.isDevelopment = process.env.NODE_ENV === 'development';\n this.isDevelopment = true;\n\n this.mode = (process.env.LOG_MODE as LogMode) || 'normal';\n \n\n this.levelColors = {\n [LogLevel.INFO]: colors.blue,\n [LogLevel.WARN]: colors.yellow,\n [LogLevel.ERROR]: colors.red,\n [LogLevel.DEBUG]: colors.magenta\n };\n\n this.levelBgColors = {\n [LogLevel.INFO]: colors.bgBlue,\n [LogLevel.WARN]: colors.bgYellow,\n [LogLevel.ERROR]: colors.bgRed,\n [LogLevel.DEBUG]: colors.bgMagenta\n };\n\n this.levelEmojis = {\n [LogLevel.INFO]: 'ℹ️',\n [LogLevel.WARN]: '⚠️',\n [LogLevel.ERROR]: '❌',\n [LogLevel.DEBUG]: '🔍'\n };\n }\n\n public static getInstance(): Logger {\n if (!Logger.instance) {\n Logger.instance = new Logger();\n }\n return Logger.instance;\n }\n\n public setMode(mode: LogMode) {\n this.mode = mode;\n }\n\n private shouldLog(level: LogLevel): boolean {\n const silent = [\n LogLevel.ERROR,\n ];\n\n const minimal = [\n LogLevel.ERROR,\n LogLevel.WARN,\n ];\n\n const normal = [\n LogLevel.ERROR,\n LogLevel.WARN,\n LogLevel.INFO,\n ];\n\n if (this.mode === 'silent') return silent.includes(level);\n if (this.mode === 'minimal') return minimal.includes(level);\n if (this.mode === 'normal') return normal.includes(level);\n return true; // verbose mode\n }\n\n private formatMessage(logMessage: LogMessage): string {\n const { level, message, timestamp, context } = logMessage;\n const color = this.levelColors[level];\n const bgColor = this.levelBgColors[level];\n const emoji = this.levelEmojis[level];\n \n const timestampStr = colors.gray + `[${timestamp}]` + colors.reset;\n // Use background color for level badge like Vitest\n const levelStr = colors.white + bgColor + ` ${level.toUpperCase()} ` + colors.reset;\n const emojiStr = emoji + ' ';\n const messageStr = colors.bright + message + colors.reset;\n \n const contextStr = context \n ? '\\n' + colors.dim + 'Context: ' + JSON.stringify(context, null, 2) + colors.reset\n : '';\n\n return `${timestampStr} ${levelStr} ${emojiStr}${messageStr}${contextStr}`;\n }\n\n private log(level: LogLevel, message: string, context?: Record<string, any>) {\n if (!this.shouldLog(level)) \n return; \n\n if (level == LogLevel.WARN || level == LogLevel.ERROR) {\n if (level == LogLevel.ERROR) {\n // alert me\n }\n // store in database\n }\n \n const logMessage: LogMessage = {\n level,\n message,\n timestamp: new Date().toISOString(),\n context\n };\n\n const formattedMessage = this.formatMessage(logMessage);\n\n switch (level) {\n case LogLevel.ERROR:\n console.error(formattedMessage);\n break;\n case LogLevel.WARN:\n console.warn(formattedMessage);\n break;\n case LogLevel.DEBUG:\n if (this.isDevelopment) {\n console.debug(formattedMessage);\n }\n break;\n default:\n console.log(formattedMessage);\n }\n }\n\n public info(message: string, context?: Record<string, any>) {\n this.log(LogLevel.INFO, message, context);\n }\n\n public warn(message: string, context?: Record<string, any>) {\n this.log(LogLevel.WARN, message, context);\n }\n\n public error(message: string, context?: Record<string, any>) {\n this.log(LogLevel.ERROR, message, context);\n }\n\n public debug(message: string, context?: Record<string, any>) {\n this.log(LogLevel.DEBUG, message, context);\n }\n}\n\nexport const logger = Logger.getInstance(); "],"names":[],"mappings":"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","debug_id":"efbd088e-b76f-5ea3-8654-16c301d16dcd"}
1
+ {"version":3,"file":"logger.js","sources":["utils/logger.ts"],"sourceRoot":"/","sourcesContent":["\nexport enum LogLevel {\n INFO = 'info',\n WARN = 'warn',\n ERROR = 'error',\n DEBUG = 'debug'\n}\n\ntype LogMode = 'silent' | 'minimal' | 'normal' | 'verbose';\n\ninterface LogMessage {\n level: LogLevel;\n message: string;\n timestamp: string;\n context?: Record<string, any>;\n}\n\n// ANSI color codes\nconst colors = {\n reset: '\\x1b[0m',\n bright: '\\x1b[1m',\n dim: '\\x1b[2m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n blue: '\\x1b[34m',\n magenta: '\\x1b[35m',\n cyan: '\\x1b[36m',\n white: '\\x1b[37m',\n gray: '\\x1b[90m',\n // Background colors\n bgRed: '\\x1b[41m',\n bgGreen: '\\x1b[42m',\n bgYellow: '\\x1b[43m',\n bgBlue: '\\x1b[44m',\n bgMagenta: '\\x1b[45m',\n bgCyan: '\\x1b[46m',\n bgWhite: '\\x1b[47m',\n bgGray: '\\x1b[100m',\n // Bright background colors\n bgBrightRed: '\\x1b[101m',\n bgBrightGreen: '\\x1b[102m',\n bgBrightYellow: '\\x1b[103m',\n bgBrightBlue: '\\x1b[104m',\n bgBrightMagenta: '\\x1b[105m',\n bgBrightCyan: '\\x1b[106m',\n bgBrightWhite: '\\x1b[107m'\n};\n\nclass Logger {\n private static instance: Logger;\n private isDevelopment: boolean;\n private mode: LogMode;\n private levelColors: Record<LogLevel, string>;\n private levelBgColors: Record<LogLevel, string>;\n private levelEmojis: Record<LogLevel, string>;\n\n private constructor() {\n\n // this.isDevelopment = process.env.NODE_ENV === 'development';\n this.isDevelopment = true;\n\n this.mode = (process.env.NODE_ENV === 'test' ? 'silent' : ((process.env.LOG_MODE as LogMode) || 'normal'));\n \n\n this.levelColors = {\n [LogLevel.INFO]: colors.blue,\n [LogLevel.WARN]: colors.yellow,\n [LogLevel.ERROR]: colors.red,\n [LogLevel.DEBUG]: colors.magenta\n };\n\n this.levelBgColors = {\n [LogLevel.INFO]: colors.bgBlue,\n [LogLevel.WARN]: colors.bgYellow,\n [LogLevel.ERROR]: colors.bgRed,\n [LogLevel.DEBUG]: colors.bgMagenta\n };\n\n this.levelEmojis = {\n [LogLevel.INFO]: 'ℹ️',\n [LogLevel.WARN]: '⚠️',\n [LogLevel.ERROR]: '❌',\n [LogLevel.DEBUG]: '🔍'\n };\n }\n\n public static getInstance(): Logger {\n if (!Logger.instance) {\n Logger.instance = new Logger();\n }\n return Logger.instance;\n }\n\n public setMode(mode: LogMode) {\n this.mode = mode;\n }\n\n private shouldLog(level: LogLevel): boolean {\n const silent = [\n LogLevel.ERROR,\n ];\n\n const minimal = [\n LogLevel.ERROR,\n LogLevel.WARN,\n ];\n\n const normal = [\n LogLevel.ERROR,\n LogLevel.WARN,\n LogLevel.INFO,\n ];\n\n if (this.mode === 'silent') return silent.includes(level);\n if (this.mode === 'minimal') return minimal.includes(level);\n if (this.mode === 'normal') return normal.includes(level);\n return true; // verbose mode\n }\n\n private formatMessage(logMessage: LogMessage): string {\n const { level, message, timestamp, context } = logMessage;\n const color = this.levelColors[level];\n const bgColor = this.levelBgColors[level];\n const emoji = this.levelEmojis[level];\n \n const timestampStr = colors.gray + `[${timestamp}]` + colors.reset;\n // Use background color for level badge like Vitest\n const levelStr = colors.white + bgColor + ` ${level.toUpperCase()} ` + colors.reset;\n const emojiStr = emoji + ' ';\n const messageStr = colors.bright + message + colors.reset;\n \n const contextStr = context \n ? '\\n' + colors.dim + 'Context: ' + JSON.stringify(context, null, 2) + colors.reset\n : '';\n\n return `${timestampStr} ${levelStr} ${emojiStr}${messageStr}${contextStr}`;\n }\n\n private log(level: LogLevel, message: string, context?: Record<string, any>) {\n if (!this.shouldLog(level)) \n return; \n\n if (level == LogLevel.WARN || level == LogLevel.ERROR) {\n if (level == LogLevel.ERROR) {\n // alert me\n }\n // store in database\n }\n \n const logMessage: LogMessage = {\n level,\n message,\n timestamp: new Date().toISOString(),\n context\n };\n\n const formattedMessage = this.formatMessage(logMessage);\n\n switch (level) {\n case LogLevel.ERROR:\n console.error(formattedMessage);\n break;\n case LogLevel.WARN:\n console.warn(formattedMessage);\n break;\n case LogLevel.DEBUG:\n if (this.isDevelopment) {\n console.debug(formattedMessage);\n }\n break;\n default:\n console.log(formattedMessage);\n }\n }\n\n public info(message: string, context?: Record<string, any>) {\n this.log(LogLevel.INFO, message, context);\n }\n\n public warn(message: string, context?: Record<string, any>) {\n this.log(LogLevel.WARN, message, context);\n }\n\n public error(message: string, context?: Record<string, any>) {\n this.log(LogLevel.ERROR, message, context);\n }\n\n public debug(message: string, context?: Record<string, any>) {\n this.log(LogLevel.DEBUG, message, context);\n }\n}\n\nexport const logger = Logger.getInstance(); "],"names":[],"mappings":"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","debug_id":"3ad19480-6fd4-5009-abf4-d8f8caeb9a0e"}
@@ -0,0 +1,14 @@
1
+ services:
2
+ dev-db:
3
+ image: postgres
4
+ environment:
5
+ POSTGRES_PASSWORD: password
6
+ ports:
7
+ - "5433:5432"
8
+
9
+ test-db:
10
+ image: postgres
11
+ environment:
12
+ POSTGRES_PASSWORD: password
13
+ ports:
14
+ - "5434:5432"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.2.45",
3
+ "version": "1.2.46",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -17,8 +17,12 @@
17
17
  "start": "node dist/index.js",
18
18
  "generate": "npx prisma generate",
19
19
  "prepublishOnly": "npm run generate && npm run build",
20
- "test": "vitest",
21
- "seed": "tsx src/seedDatabase.ts",
20
+ "test:pre": "NODE_ENV=test tsx scripts/test-pre-push.ts",
21
+ "test": "npm run test:pre && NODE_ENV=test vitest",
22
+ "test:run": "NODE_ENV=test vitest run",
23
+ "test:watch": "NODE_ENV=test vitest watch",
24
+ "test:coverage": "NODE_ENV=test vitest run --coverage",
25
+ "db:seed": "tsx src/seedDatabase.ts",
22
26
  "sentry:sourcemaps": "sentry-cli sourcemaps inject --org studious-fp --project server ./dist && sentry-cli sourcemaps upload --org studious-fp --project server ./dist"
23
27
  },
24
28
  "dependencies": {
@@ -28,11 +32,15 @@
28
32
  "@sentry/cli": "^2.58.2",
29
33
  "@sentry/node": "^10.26.0",
30
34
  "@trpc/server": "^11.4.3",
35
+ "@unkey/ratelimit": "^2.1.3",
31
36
  "bcryptjs": "^3.0.2",
37
+ "compression": "^1.8.1",
32
38
  "cors": "^2.8.5",
33
39
  "date-fns": "^4.1.0",
34
40
  "dotenv": "^16.5.0",
35
41
  "express": "^4.18.3",
42
+ "express-rate-limit": "^8.2.1",
43
+ "helmet": "^8.1.0",
36
44
  "nodemailer": "^7.0.4",
37
45
  "openai": "^5.23.0",
38
46
  "pdf-lib": "^1.17.1",
@@ -41,17 +49,18 @@
41
49
  "sharp": "^0.34.2",
42
50
  "socket.io": "^4.8.1",
43
51
  "superjson": "^2.2.2",
44
- "trpc-panel": "^1.3.4",
45
52
  "uuid": "^11.1.0",
46
53
  "zod": "^3.22.4"
47
54
  },
48
55
  "devDependencies": {
56
+ "@types/compression": "^1.8.1",
49
57
  "@types/cors": "^2.8.17",
50
58
  "@types/express": "^4.17.21",
51
59
  "@types/node": "^20.11.24",
52
60
  "@types/nodemailer": "^6.4.17",
53
61
  "@types/supertest": "^6.0.3",
54
62
  "@types/uuid": "^10.0.0",
63
+ "@vitest/coverage-v8": "^3.2.4",
55
64
  "supertest": "^7.1.4",
56
65
  "trpc-ui": "^1.0.15",
57
66
  "ts-node": "^10.9.2",
@@ -9,6 +9,11 @@ generator client {
9
9
  // output = "../generated/prisma"
10
10
  }
11
11
 
12
+ // @todo: add type safety to json fields.
13
+ // generator json {
14
+ // provider = "prisma-json-types-generator"
15
+ // }
16
+
12
17
  datasource db {
13
18
  provider = "postgresql"
14
19
  url = env("DATABASE_URL")
@@ -42,6 +47,15 @@ enum UploadStatus {
42
47
  CANCELLED
43
48
  }
44
49
 
50
+ enum GenerationStatus {
51
+ NOT_STARTED
52
+ PENDING
53
+ GENERATING
54
+ COMPLETED
55
+ FAILED
56
+ CANCELLED
57
+ }
58
+
45
59
  enum WorksheetQuestionType {
46
60
  MULTIPLE_CHOICE
47
61
  TRUE_FALSE
@@ -305,6 +319,7 @@ model Comment {
305
319
  announcementId String?
306
320
  parentComment Comment? @relation("CommentReplies", fields: [parentCommentId], references: [id], onDelete: Cascade)
307
321
  parentCommentId String?
322
+ status GenerationStatus?
308
323
  replies Comment[] @relation("CommentReplies")
309
324
  reactions Reaction[]
310
325
  createdAt DateTime @default(now())
@@ -345,7 +360,6 @@ model Submission {
345
360
  annotations File[] @relation("SubmissionAnnotations")
346
361
  worksheetResponses StudentWorksheetResponse[]
347
362
  extendedResponse String?
348
-
349
363
  gradeReceived Int?
350
364
 
351
365
  rubricState String?
@@ -354,6 +368,18 @@ model Submission {
354
368
  submittedAt DateTime?
355
369
  submitted Boolean? @default(false)
356
370
  returned Boolean? @default(false)
371
+ newtonTutors NewtonChat[]
372
+ }
373
+
374
+ model NewtonChat {
375
+ id String @id @default(uuid())
376
+ submissionId String
377
+ submission Submission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
378
+ conversationId String @unique
379
+ conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
380
+ createdAt DateTime @default(now())
381
+ updatedAt DateTime @updatedAt
382
+ title String @default("Session with Newton Tutor")
357
383
  }
358
384
 
359
385
  model Section {
@@ -441,6 +467,7 @@ model Conversation {
441
467
  members ConversationMember[]
442
468
  messages Message[]
443
469
  labChat LabChat?
470
+ newtonChat NewtonChat?
444
471
  }
445
472
 
446
473
  model LabChat {
@@ -451,8 +478,7 @@ model LabChat {
451
478
  conversationId String @unique
452
479
  createdById String // Teacher who created the lab
453
480
  createdAt DateTime @default(now())
454
- updatedAt DateTime @updatedAt
455
-
481
+ updatedAt DateTime @updatedAt
456
482
  class Class @relation("ClassLabChats", fields: [classId], references: [id], onDelete: Cascade)
457
483
  conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
458
484
  createdBy User @relation("CreatedLabChats", fields: [createdById], references: [id], onDelete: NoAction)
@@ -479,7 +505,7 @@ model Message {
479
505
  senderId String
480
506
  conversationId String
481
507
  createdAt DateTime @default(now())
482
-
508
+ status GenerationStatus? @default(NOT_STARTED)
483
509
  sender User @relation("SentMessages", fields: [senderId], references: [id], onDelete: Cascade)
484
510
  conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
485
511
  mentions Mention[]
@@ -557,6 +583,7 @@ model StudentQuestionProgress {
557
583
  updatedAt DateTime? @updatedAt
558
584
  studentWorksheetResponseId String?
559
585
  studentWorksheetResponse StudentWorksheetResponse? @relation(fields: [studentWorksheetResponseId], references: [id], onDelete: Cascade)
586
+ status GenerationStatus? @default(NOT_STARTED)
560
587
 
561
588
  @@index([studentId, questionId])
562
589
  }
@@ -582,7 +609,7 @@ model SchoolDevelopementProgram {
582
609
  submittedAt DateTime? @default(now())
583
610
  reviewedAt DateTime?
584
611
 
585
- status String @default("PENDING") // PENDING, APPROVED, REJECTED, REFERRED
612
+ status String @default("NOT_STARTED") // NOT_STARTED, PENDING, APPROVED, REJECTED, REFERRED
586
613
 
587
614
  supportingDocumentation File[] @relation("SchoolDevelopementProgramSupportingDocumentation")
588
615
  }
@@ -0,0 +1,14 @@
1
+ import { execSync } from 'child_process';
2
+ import { logger } from '../src/utils/logger';
3
+ import dotenv from 'dotenv';
4
+ import { resolve } from 'path';
5
+
6
+ dotenv.config({ path: resolve(process.cwd(), '.env.test') });
7
+
8
+ logger.info("Syncing Prisma schema to test database...");
9
+
10
+ execSync("npx prisma db push --force-reset --skip-generate", {
11
+ stdio: 'inherit',
12
+ });
13
+
14
+ logger.info("Test database is ready!");
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;