@studious-lms/server 1.1.26 → 1.2.6

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 (486) hide show
  1. package/.coderabbit.yaml +9 -0
  2. package/.env.example +53 -0
  3. package/.env.test.example +37 -0
  4. package/README.md +34 -7
  5. package/dist/exportType.d.ts.map +1 -1
  6. package/dist/exportType.js +4 -0
  7. package/dist/exportType.js.map +1 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +212 -51
  11. package/dist/index.js.map +1 -0
  12. package/dist/instrument.d.ts +2 -0
  13. package/dist/instrument.d.ts.map +1 -0
  14. package/dist/instrument.js +18 -0
  15. package/dist/instrument.js.map +1 -0
  16. package/dist/lib/config/env.d.ts +190 -0
  17. package/dist/lib/config/env.d.ts.map +1 -0
  18. package/dist/lib/config/env.js +121 -0
  19. package/dist/lib/config/env.js.map +1 -0
  20. package/dist/lib/fileUpload.d.ts +2 -2
  21. package/dist/lib/fileUpload.d.ts.map +1 -1
  22. package/dist/lib/fileUpload.js +15 -5
  23. package/dist/lib/fileUpload.js.map +1 -0
  24. package/dist/lib/googleCloudStorage.d.ts +6 -0
  25. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  26. package/dist/lib/googleCloudStorage.js +26 -6
  27. package/dist/lib/googleCloudStorage.js.map +1 -0
  28. package/dist/lib/jsonConversion.d.ts.map +1 -1
  29. package/dist/lib/jsonConversion.js +16 -14
  30. package/dist/lib/jsonConversion.js.map +1 -0
  31. package/dist/lib/jsonStyles.d.ts.map +1 -1
  32. package/dist/lib/jsonStyles.js +4 -0
  33. package/dist/lib/jsonStyles.js.map +1 -0
  34. package/dist/lib/notificationHandler.d.ts +2 -2
  35. package/dist/lib/notificationHandler.d.ts.map +1 -1
  36. package/dist/lib/notificationHandler.js +4 -0
  37. package/dist/lib/notificationHandler.js.map +1 -0
  38. package/dist/lib/prisma.d.ts +2 -2
  39. package/dist/lib/prisma.d.ts.map +1 -1
  40. package/dist/lib/prisma.js +24 -1
  41. package/dist/lib/prisma.js.map +1 -0
  42. package/dist/lib/pusher.d.ts +4 -1
  43. package/dist/lib/pusher.d.ts.map +1 -1
  44. package/dist/lib/pusher.js +14 -6
  45. package/dist/lib/pusher.js.map +1 -0
  46. package/dist/lib/redis.d.ts +5 -0
  47. package/dist/lib/redis.d.ts.map +1 -0
  48. package/dist/lib/redis.js +53 -0
  49. package/dist/lib/redis.js.map +1 -0
  50. package/dist/lib/thumbnailGenerator.d.ts +0 -21
  51. package/dist/lib/thumbnailGenerator.d.ts.map +1 -1
  52. package/dist/lib/thumbnailGenerator.js +159 -158
  53. package/dist/lib/thumbnailGenerator.js.map +1 -0
  54. package/dist/middleware/auth.d.ts.map +1 -1
  55. package/dist/middleware/auth.js +41 -93
  56. package/dist/middleware/auth.js.map +1 -0
  57. package/dist/middleware/logging.d.ts.map +1 -1
  58. package/dist/middleware/logging.js +4 -0
  59. package/dist/middleware/logging.js.map +1 -0
  60. package/dist/middleware/security.d.ts +5 -0
  61. package/dist/middleware/security.d.ts.map +1 -0
  62. package/dist/middleware/security.js +77 -0
  63. package/dist/middleware/security.js.map +1 -0
  64. package/dist/models/agenda.d.ts +97 -0
  65. package/dist/models/agenda.d.ts.map +1 -0
  66. package/dist/models/agenda.js +40 -0
  67. package/dist/models/agenda.js.map +1 -0
  68. package/dist/models/announcement.d.ts +223 -0
  69. package/dist/models/announcement.d.ts.map +1 -0
  70. package/dist/models/announcement.js +120 -0
  71. package/dist/models/announcement.js.map +1 -0
  72. package/dist/models/assignment.d.ts +1292 -0
  73. package/dist/models/assignment.d.ts.map +1 -0
  74. package/dist/models/assignment.js +309 -0
  75. package/dist/models/assignment.js.map +1 -0
  76. package/dist/models/attendance.d.ts +180 -0
  77. package/dist/models/attendance.d.ts.map +1 -0
  78. package/dist/models/attendance.js +188 -0
  79. package/dist/models/attendance.js.map +1 -0
  80. package/dist/models/auth.d.ts +153 -0
  81. package/dist/models/auth.d.ts.map +1 -0
  82. package/dist/models/auth.js +217 -0
  83. package/dist/models/auth.js.map +1 -0
  84. package/dist/models/class.d.ts +439 -0
  85. package/dist/models/class.d.ts.map +1 -0
  86. package/dist/models/class.js +546 -0
  87. package/dist/models/class.js.map +1 -0
  88. package/dist/models/comment.d.ts +171 -0
  89. package/dist/models/comment.d.ts.map +1 -0
  90. package/dist/models/comment.js +138 -0
  91. package/dist/models/comment.js.map +1 -0
  92. package/dist/models/conversation.d.ts +164 -0
  93. package/dist/models/conversation.d.ts.map +1 -0
  94. package/dist/models/conversation.js +175 -0
  95. package/dist/models/conversation.js.map +1 -0
  96. package/dist/models/event.d.ts +295 -0
  97. package/dist/models/event.d.ts.map +1 -0
  98. package/dist/models/event.js +145 -0
  99. package/dist/models/event.js.map +1 -0
  100. package/dist/models/file.d.ts +536 -0
  101. package/dist/models/file.d.ts.map +1 -0
  102. package/dist/models/file.js +126 -0
  103. package/dist/models/file.js.map +1 -0
  104. package/dist/models/folder.d.ts +295 -0
  105. package/dist/models/folder.d.ts.map +1 -0
  106. package/dist/models/folder.js +202 -0
  107. package/dist/models/folder.js.map +1 -0
  108. package/dist/models/labChat.d.ts +243 -0
  109. package/dist/models/labChat.d.ts.map +1 -0
  110. package/dist/models/labChat.js +204 -0
  111. package/dist/models/labChat.js.map +1 -0
  112. package/dist/models/marketing.d.ts +72 -0
  113. package/dist/models/marketing.d.ts.map +1 -0
  114. package/dist/models/marketing.js +26 -0
  115. package/dist/models/marketing.js.map +1 -0
  116. package/dist/models/message.d.ts +100 -0
  117. package/dist/models/message.d.ts.map +1 -0
  118. package/dist/models/message.js +131 -0
  119. package/dist/models/message.js.map +1 -0
  120. package/dist/models/newtonChat.d.ts +72 -0
  121. package/dist/models/newtonChat.d.ts.map +1 -0
  122. package/dist/models/newtonChat.js +61 -0
  123. package/dist/models/newtonChat.js.map +1 -0
  124. package/dist/models/notification.d.ts +65 -0
  125. package/dist/models/notification.d.ts.map +1 -0
  126. package/dist/models/notification.js +46 -0
  127. package/dist/models/notification.js.map +1 -0
  128. package/dist/models/section.d.ts +102 -0
  129. package/dist/models/section.d.ts.map +1 -0
  130. package/dist/models/section.js +83 -0
  131. package/dist/models/section.js.map +1 -0
  132. package/dist/models/user.d.ts +39 -0
  133. package/dist/models/user.d.ts.map +1 -0
  134. package/dist/models/user.js +38 -0
  135. package/dist/models/user.js.map +1 -0
  136. package/dist/models/worksheet.d.ts +460 -0
  137. package/dist/models/worksheet.d.ts.map +1 -0
  138. package/dist/models/worksheet.js +200 -0
  139. package/dist/models/worksheet.js.map +1 -0
  140. package/dist/pipelines/aiLabChat.d.ts +21 -0
  141. package/dist/pipelines/aiLabChat.d.ts.map +1 -0
  142. package/dist/pipelines/aiLabChat.js +460 -0
  143. package/dist/pipelines/aiLabChat.js.map +1 -0
  144. package/dist/pipelines/aiNewtonChat.d.ts +30 -0
  145. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -0
  146. package/dist/pipelines/aiNewtonChat.js +289 -0
  147. package/dist/pipelines/aiNewtonChat.js.map +1 -0
  148. package/dist/pipelines/gradeWorksheet.d.ts +30 -0
  149. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -0
  150. package/dist/pipelines/gradeWorksheet.js +252 -0
  151. package/dist/pipelines/gradeWorksheet.js.map +1 -0
  152. package/dist/routers/_app.d.ts +6438 -3910
  153. package/dist/routers/_app.d.ts.map +1 -1
  154. package/dist/routers/_app.js +10 -0
  155. package/dist/routers/_app.js.map +1 -0
  156. package/dist/routers/agenda.d.ts +58 -6
  157. package/dist/routers/agenda.d.ts.map +1 -1
  158. package/dist/routers/agenda.js +6 -58
  159. package/dist/routers/agenda.js.map +1 -0
  160. package/dist/routers/announcement.d.ts +325 -6
  161. package/dist/routers/announcement.d.ts.map +1 -1
  162. package/dist/routers/announcement.js +543 -77
  163. package/dist/routers/announcement.js.map +1 -0
  164. package/dist/routers/assignment.d.ts +419 -357
  165. package/dist/routers/assignment.d.ts.map +1 -1
  166. package/dist/routers/assignment.js +100 -1689
  167. package/dist/routers/assignment.js.map +1 -0
  168. package/dist/routers/attendance.d.ts +20 -9
  169. package/dist/routers/attendance.d.ts.map +1 -1
  170. package/dist/routers/attendance.js +10 -263
  171. package/dist/routers/attendance.js.map +1 -0
  172. package/dist/routers/auth.d.ts +21 -1
  173. package/dist/routers/auth.d.ts.map +1 -1
  174. package/dist/routers/auth.js +37 -241
  175. package/dist/routers/auth.js.map +1 -0
  176. package/dist/routers/class.d.ts +198 -68
  177. package/dist/routers/class.d.ts.map +1 -1
  178. package/dist/routers/class.js +88 -909
  179. package/dist/routers/class.js.map +1 -0
  180. package/dist/routers/comment.d.ts +153 -0
  181. package/dist/routers/comment.d.ts.map +1 -0
  182. package/dist/routers/comment.js +58 -0
  183. package/dist/routers/comment.js.map +1 -0
  184. package/dist/routers/conversation.d.ts +73 -3
  185. package/dist/routers/conversation.d.ts.map +1 -1
  186. package/dist/routers/conversation.js +23 -265
  187. package/dist/routers/conversation.js.map +1 -0
  188. package/dist/routers/event.d.ts +46 -37
  189. package/dist/routers/event.d.ts.map +1 -1
  190. package/dist/routers/event.js +15 -431
  191. package/dist/routers/event.js.map +1 -0
  192. package/dist/routers/file.d.ts +4 -2
  193. package/dist/routers/file.d.ts.map +1 -1
  194. package/dist/routers/file.js +11 -298
  195. package/dist/routers/file.js.map +1 -0
  196. package/dist/routers/folder.d.ts +21 -14
  197. package/dist/routers/folder.d.ts.map +1 -1
  198. package/dist/routers/folder.js +36 -743
  199. package/dist/routers/folder.js.map +1 -0
  200. package/dist/routers/labChat.d.ts +12 -9
  201. package/dist/routers/labChat.d.ts.map +1 -1
  202. package/dist/routers/labChat.js +21 -885
  203. package/dist/routers/labChat.js.map +1 -0
  204. package/dist/routers/marketing.d.ts +2 -2
  205. package/dist/routers/marketing.d.ts.map +1 -1
  206. package/dist/routers/marketing.js +9 -54
  207. package/dist/routers/marketing.js.map +1 -0
  208. package/dist/routers/message.d.ts +2 -1
  209. package/dist/routers/message.d.ts.map +1 -1
  210. package/dist/routers/message.js +29 -519
  211. package/dist/routers/message.js.map +1 -0
  212. package/dist/routers/newtonChat.d.ts +55 -0
  213. package/dist/routers/newtonChat.d.ts.map +1 -0
  214. package/dist/routers/newtonChat.js +22 -0
  215. package/dist/routers/newtonChat.js.map +1 -0
  216. package/dist/routers/notifications.d.ts +8 -8
  217. package/dist/routers/notifications.d.ts.map +1 -1
  218. package/dist/routers/notifications.js +20 -81
  219. package/dist/routers/notifications.js.map +1 -0
  220. package/dist/routers/section.d.ts +23 -8
  221. package/dist/routers/section.d.ts.map +1 -1
  222. package/dist/routers/section.js +23 -273
  223. package/dist/routers/section.js.map +1 -0
  224. package/dist/routers/user.d.ts +1 -1
  225. package/dist/routers/user.d.ts.map +1 -1
  226. package/dist/routers/user.js +34 -204
  227. package/dist/routers/user.js.map +1 -0
  228. package/dist/routers/worksheet.d.ts +362 -0
  229. package/dist/routers/worksheet.d.ts.map +1 -0
  230. package/dist/routers/worksheet.js +153 -0
  231. package/dist/routers/worksheet.js.map +1 -0
  232. package/dist/seedDatabase.d.ts +2 -3
  233. package/dist/seedDatabase.d.ts.map +1 -1
  234. package/dist/seedDatabase.js +309 -288
  235. package/dist/seedDatabase.js.map +1 -0
  236. package/dist/server/pipelines/aiLabChat.d.ts +21 -0
  237. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  238. package/dist/server/pipelines/aiLabChat.js +456 -0
  239. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  240. package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
  241. package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
  242. package/dist/server/pipelines/aiNewtonChat.js +285 -0
  243. package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
  244. package/dist/server/pipelines/gradeWorksheet.d.ts +30 -0
  245. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  246. package/dist/server/pipelines/gradeWorksheet.js +248 -0
  247. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  248. package/dist/services/agenda.d.ts +100 -0
  249. package/dist/services/agenda.d.ts.map +1 -0
  250. package/dist/services/agenda.js +21 -0
  251. package/dist/services/agenda.js.map +1 -0
  252. package/dist/services/announcement.d.ts +135 -0
  253. package/dist/services/announcement.d.ts.map +1 -0
  254. package/dist/services/announcement.js +223 -0
  255. package/dist/services/announcement.js.map +1 -0
  256. package/dist/services/assignment.d.ts +1462 -0
  257. package/dist/services/assignment.d.ts.map +1 -0
  258. package/dist/services/assignment.js +898 -0
  259. package/dist/services/assignment.js.map +1 -0
  260. package/dist/services/attendance.d.ts +93 -0
  261. package/dist/services/attendance.d.ts.map +1 -0
  262. package/dist/services/attendance.js +61 -0
  263. package/dist/services/attendance.js.map +1 -0
  264. package/dist/services/auth.d.ts +68 -0
  265. package/dist/services/auth.d.ts.map +1 -0
  266. package/dist/services/auth.js +218 -0
  267. package/dist/services/auth.js.map +1 -0
  268. package/dist/services/class.d.ts +621 -0
  269. package/dist/services/class.d.ts.map +1 -0
  270. package/dist/services/class.js +474 -0
  271. package/dist/services/class.js.map +1 -0
  272. package/dist/services/comment.d.ts +100 -0
  273. package/dist/services/comment.d.ts.map +1 -0
  274. package/dist/services/comment.js +83 -0
  275. package/dist/services/comment.js.map +1 -0
  276. package/dist/services/conversation.d.ts +159 -0
  277. package/dist/services/conversation.d.ts.map +1 -0
  278. package/dist/services/conversation.js +138 -0
  279. package/dist/services/conversation.js.map +1 -0
  280. package/dist/services/event.d.ts +216 -0
  281. package/dist/services/event.d.ts.map +1 -0
  282. package/dist/services/event.js +168 -0
  283. package/dist/services/event.js.map +1 -0
  284. package/dist/services/file.d.ts +74 -0
  285. package/dist/services/file.d.ts.map +1 -0
  286. package/dist/services/file.js +133 -0
  287. package/dist/services/file.js.map +1 -0
  288. package/dist/services/folder.d.ts +239 -0
  289. package/dist/services/folder.d.ts.map +1 -0
  290. package/dist/services/folder.js +248 -0
  291. package/dist/services/folder.js.map +1 -0
  292. package/dist/services/labChat.d.ts +165 -0
  293. package/dist/services/labChat.d.ts.map +1 -0
  294. package/dist/services/labChat.js +289 -0
  295. package/dist/services/labChat.js.map +1 -0
  296. package/dist/services/marketing.d.ts +50 -0
  297. package/dist/services/marketing.d.ts.map +1 -0
  298. package/dist/services/marketing.js +32 -0
  299. package/dist/services/marketing.js.map +1 -0
  300. package/dist/services/message.d.ts +95 -0
  301. package/dist/services/message.d.ts.map +1 -0
  302. package/dist/services/message.js +350 -0
  303. package/dist/services/message.js.map +1 -0
  304. package/dist/services/newtonChat.d.ts +22 -0
  305. package/dist/services/newtonChat.d.ts.map +1 -0
  306. package/dist/services/newtonChat.js +174 -0
  307. package/dist/services/newtonChat.js.map +1 -0
  308. package/dist/services/notification.d.ts +65 -0
  309. package/dist/services/notification.d.ts.map +1 -0
  310. package/dist/services/notification.js +33 -0
  311. package/dist/services/notification.js.map +1 -0
  312. package/dist/services/section.d.ts +53 -0
  313. package/dist/services/section.d.ts.map +1 -0
  314. package/dist/services/section.js +199 -0
  315. package/dist/services/section.js.map +1 -0
  316. package/dist/services/user.d.ts +48 -0
  317. package/dist/services/user.d.ts.map +1 -0
  318. package/dist/services/user.js +141 -0
  319. package/dist/services/user.js.map +1 -0
  320. package/dist/services/worksheet.d.ts +239 -0
  321. package/dist/services/worksheet.d.ts.map +1 -0
  322. package/dist/services/worksheet.js +235 -0
  323. package/dist/services/worksheet.js.map +1 -0
  324. package/dist/socket/handlers.d.ts.map +1 -1
  325. package/dist/socket/handlers.js +4 -0
  326. package/dist/socket/handlers.js.map +1 -0
  327. package/dist/trpc.d.ts.map +1 -1
  328. package/dist/trpc.js +4 -0
  329. package/dist/trpc.js.map +1 -0
  330. package/dist/types/trpc.d.ts.map +1 -1
  331. package/dist/types/trpc.js +4 -0
  332. package/dist/types/trpc.js.map +1 -0
  333. package/dist/utils/aiUser.d.ts +1 -3
  334. package/dist/utils/aiUser.d.ts.map +1 -1
  335. package/dist/utils/aiUser.js +8 -3
  336. package/dist/utils/aiUser.js.map +1 -0
  337. package/dist/utils/email.d.ts +12 -1
  338. package/dist/utils/email.d.ts.map +1 -1
  339. package/dist/utils/email.js +26 -4
  340. package/dist/utils/email.js.map +1 -0
  341. package/dist/utils/generateInviteCode.d.ts +1 -2
  342. package/dist/utils/generateInviteCode.d.ts.map +1 -1
  343. package/dist/utils/generateInviteCode.js +5 -2
  344. package/dist/utils/generateInviteCode.js.map +1 -0
  345. package/dist/utils/inference.d.ts +8 -0
  346. package/dist/utils/inference.d.ts.map +1 -1
  347. package/dist/utils/inference.js +78 -10
  348. package/dist/utils/inference.js.map +1 -0
  349. package/dist/utils/logger.d.ts +3 -0
  350. package/dist/utils/logger.d.ts.map +1 -1
  351. package/dist/utils/logger.js +8 -1
  352. package/dist/utils/logger.js.map +1 -0
  353. package/dist/utils/prismaErrorHandler.d.ts.map +1 -1
  354. package/dist/utils/prismaErrorHandler.js +7 -0
  355. package/dist/utils/prismaErrorHandler.js.map +1 -0
  356. package/dist/utils/prismaWrapper.d.ts +1 -0
  357. package/dist/utils/prismaWrapper.d.ts.map +1 -1
  358. package/dist/utils/prismaWrapper.js +8 -0
  359. package/dist/utils/prismaWrapper.js.map +1 -0
  360. package/docker-compose.yml +19 -0
  361. package/package.json +21 -4
  362. package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +30 -0
  363. package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +35 -0
  364. package/prisma/schema.prisma +180 -12
  365. package/scripts/test-pre-push.ts +14 -0
  366. package/src/index.ts +247 -52
  367. package/src/instrument.ts +15 -0
  368. package/src/lib/config/env.ts +132 -0
  369. package/src/lib/fileUpload.ts +13 -6
  370. package/src/lib/googleCloudStorage.ts +23 -6
  371. package/src/lib/jsonConversion.ts +12 -14
  372. package/src/lib/prisma.ts +23 -2
  373. package/src/lib/pusher.ts +11 -6
  374. package/src/lib/redis.ts +56 -0
  375. package/src/lib/thumbnailGenerator.ts +170 -168
  376. package/src/middleware/auth.ts +86 -137
  377. package/src/middleware/security.ts +80 -0
  378. package/src/models/agenda.ts +46 -0
  379. package/src/models/announcement.ts +134 -0
  380. package/src/models/assignment.ts +322 -0
  381. package/src/models/attendance.ts +208 -0
  382. package/src/models/auth.ts +247 -0
  383. package/src/models/class.ts +598 -0
  384. package/src/models/comment.ts +152 -0
  385. package/src/models/conversation.ts +200 -0
  386. package/src/models/event.ts +177 -0
  387. package/src/models/file.ts +129 -0
  388. package/src/models/folder.ts +225 -0
  389. package/src/models/labChat.ts +213 -0
  390. package/src/models/marketing.ts +45 -0
  391. package/src/models/message.ts +153 -0
  392. package/src/models/newtonChat.ts +70 -0
  393. package/src/models/notification.ts +54 -0
  394. package/src/models/section.ts +98 -0
  395. package/src/models/user.ts +47 -0
  396. package/src/models/worksheet.ts +294 -0
  397. package/src/pipelines/aiLabChat.ts +511 -0
  398. package/src/pipelines/aiNewtonChat.ts +347 -0
  399. package/src/pipelines/gradeWorksheet.ts +286 -0
  400. package/src/routers/_app.ts +6 -0
  401. package/src/routers/agenda.ts +3 -61
  402. package/src/routers/announcement.ts +616 -79
  403. package/src/routers/assignment.ts +148 -1827
  404. package/src/routers/attendance.ts +16 -277
  405. package/src/routers/auth.ts +79 -313
  406. package/src/routers/class.ts +265 -1038
  407. package/src/routers/comment.ts +76 -0
  408. package/src/routers/conversation.ts +53 -284
  409. package/src/routers/event.ts +50 -481
  410. package/src/routers/file.ts +45 -344
  411. package/src/routers/folder.ts +107 -836
  412. package/src/routers/labChat.ts +29 -969
  413. package/src/routers/marketing.ts +35 -77
  414. package/src/routers/message.ts +45 -571
  415. package/src/routers/newtonChat.ts +36 -0
  416. package/src/routers/notifications.ts +32 -82
  417. package/src/routers/section.ts +58 -322
  418. package/src/routers/user.ts +49 -226
  419. package/src/routers/worksheet.ts +252 -0
  420. package/src/seedDatabase.ts +328 -289
  421. package/src/services/agenda.ts +21 -0
  422. package/src/services/announcement.ts +290 -0
  423. package/src/services/assignment.ts +1198 -0
  424. package/src/services/attendance.ts +85 -0
  425. package/src/services/auth.ts +277 -0
  426. package/src/services/class.ts +622 -0
  427. package/src/services/comment.ts +106 -0
  428. package/src/services/conversation.ts +213 -0
  429. package/src/services/event.ts +231 -0
  430. package/src/services/file.ts +167 -0
  431. package/src/services/folder.ts +316 -0
  432. package/src/services/labChat.ts +352 -0
  433. package/src/services/marketing.ts +57 -0
  434. package/src/services/message.ts +461 -0
  435. package/src/services/newtonChat.ts +222 -0
  436. package/src/services/notification.ts +50 -0
  437. package/src/services/section.ts +283 -0
  438. package/src/services/user.ts +172 -0
  439. package/src/services/worksheet.ts +358 -0
  440. package/src/trpc.ts +4 -0
  441. package/src/utils/aiUser.ts +4 -3
  442. package/src/utils/email.ts +33 -4
  443. package/src/utils/generateInviteCode.ts +1 -3
  444. package/src/utils/inference.ts +89 -10
  445. package/src/utils/logger.ts +4 -1
  446. package/src/utils/prismaErrorHandler.ts +3 -0
  447. package/src/utils/prismaWrapper.ts +4 -0
  448. package/tests/globalSetup.ts +62 -0
  449. package/tests/helpers.ts +22 -0
  450. package/tests/middleware/security.test.ts +42 -0
  451. package/tests/routers/agenda.test.ts +138 -0
  452. package/tests/routers/announcement.test.ts +490 -0
  453. package/tests/routers/assignment.test.ts +837 -0
  454. package/tests/routers/attendance.test.ts +160 -0
  455. package/tests/routers/auth.test.ts +171 -0
  456. package/tests/{class.test.ts → routers/class.test.ts} +163 -92
  457. package/tests/routers/comment.test.ts +126 -0
  458. package/tests/routers/conversation.test.ts +145 -0
  459. package/tests/routers/event.test.ts +289 -0
  460. package/tests/routers/folder.test.ts +178 -0
  461. package/tests/routers/labChat.test.ts +115 -0
  462. package/tests/routers/marketing.test.ts +59 -0
  463. package/tests/routers/message.test.ts +123 -0
  464. package/tests/routers/notification.test.ts +69 -0
  465. package/tests/routers/section.test.ts +208 -0
  466. package/tests/server/rateLimit.test.ts +73 -0
  467. package/tests/setup.ts +39 -59
  468. package/tests/user.test.ts +136 -0
  469. package/tests/utils/aiUser.test.ts +22 -0
  470. package/tests/utils/generateInviteCode.test.ts +24 -0
  471. package/tests/utils/logger.test.ts +74 -0
  472. package/tests/utils/prismaErrorHandler.test.ts +101 -0
  473. package/tests/utils/prismaWrapper.test.ts +82 -0
  474. package/tests/worksheet.test.ts +181 -0
  475. package/tsconfig.json +9 -2
  476. package/vitest.config.ts +30 -1
  477. package/vitest.unit.config.ts +21 -0
  478. package/API_SPECIFICATION.md +0 -1597
  479. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  480. package/CHAT_API_SPEC.md +0 -579
  481. package/LAB_CHAT_API_SPEC.md +0 -518
  482. package/dist/routers/school.d.ts +0 -208
  483. package/dist/routers/school.d.ts.map +0 -1
  484. package/dist/routers/school.js +0 -481
  485. package/src/lib/notificationHandler.ts +0 -36
  486. package/tests/auth.test.ts +0 -25
package/src/index.ts CHANGED
@@ -11,52 +11,64 @@ 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
+ import { pusher } from './lib/pusher.js';
15
+ import { connectRedis, disconnectRedis } from './lib/redis.js';
14
16
 
15
- dotenv.config();
17
+ import { authLimiter, generalLimiter, helmetConfig, uploadLimiter } from './middleware/security.js';
18
+
19
+ import * as Sentry from "@sentry/node";
20
+ import { env } from './lib/config/env.js';
21
+ import compression from 'compression';
22
+ import { v4 as uuidv4 } from 'uuid';
23
+
24
+
25
+ import "./instrument.js";
26
+ import { openAIClient } from './utils/inference.js';
16
27
 
17
28
  const app = express();
18
29
 
30
+ app.use(helmetConfig);
31
+ app.use(compression());
32
+ app.use(express.json());
33
+ app.use(express.urlencoded({ extended: true }));
34
+
35
+ app.use((req, res, next) => {
36
+ const requestId = uuidv4();
37
+ res.setHeader('X-Request-ID', requestId);
38
+ next();
39
+ });
40
+
41
+ const allowedOrigins = env.NODE_ENV === 'production'
42
+ ? [
43
+ 'https://www.studious.sh',
44
+ 'https://studious.sh',
45
+ 'https://dev.studious.sh',
46
+ 'https://www.dev.studious.sh',
47
+ env.NEXT_PUBLIC_APP_URL,
48
+ 'http://localhost:3000',
49
+
50
+ ].filter(Boolean)
51
+ : [
52
+ 'http://localhost:3000',
53
+ 'http://localhost:3001',
54
+ 'http://127.0.0.1:3000',
55
+ 'http://127.0.0.1:3001',
56
+
57
+ env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
58
+ ];
59
+
19
60
  // CORS middleware
20
61
  app.use(cors({
21
- origin: [
22
- 'http://localhost:3000', // Frontend development server
23
- 'http://localhost:3001', // Server port
24
- 'http://127.0.0.1:3000', // Alternative localhost
25
- 'http://127.0.0.1:3001', // Alternative localhost
26
- 'https://www.studious.sh', // Production frontend
27
- 'https://studious.sh', // Production frontend (without www)
28
- process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
29
- ],
62
+ origin: allowedOrigins,
30
63
  credentials: true,
31
64
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
32
65
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-user'],
33
- optionsSuccessStatus: 200
66
+ preflightContinue: false, // Important: stop further handling of OPTIONS
67
+ optionsSuccessStatus: 204, // Recommended for modern browsers
68
+
34
69
  }));
35
70
 
36
- // Handle preflight OPTIONS requests
37
- app.options('*', (req, res) => {
38
- const allowedOrigins = [
39
- 'http://localhost:3000',
40
- 'http://localhost:3001',
41
- 'http://127.0.0.1:3000',
42
- 'http://127.0.0.1:3001',
43
- 'https://www.studious.sh', // Production frontend
44
- 'https://studious.sh', // Production frontend (without www)
45
- process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
46
- ];
47
-
48
- const origin = req.headers.origin;
49
- if (origin && allowedOrigins.includes(origin)) {
50
- res.header('Access-Control-Allow-Origin', origin);
51
- } else {
52
- res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
53
- }
54
-
55
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
56
- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, x-user');
57
- res.header('Access-Control-Allow-Credentials', 'true');
58
- res.sendStatus(200);
59
- });
71
+ app.use(generalLimiter);
60
72
 
61
73
  // CORS debugging middleware
62
74
  app.use((req, res, next) => {
@@ -86,11 +98,146 @@ app.use((req, res, next) => {
86
98
  next();
87
99
  });
88
100
 
101
+ // app.use("/panel", async (_, res) => {
102
+ // if (env.NODE_ENV !== "development") {
103
+ // return res.status(404).send("Not Found");
104
+ // }
105
+
106
+ // // Dynamically import renderTrpcPanel only in development
107
+ // const { renderTrpcPanel } = await import("trpc-ui");
108
+
109
+ // return res.send(
110
+ // renderTrpcPanel(appRouter, {
111
+ // url: "/trpc", // Base url of your trpc server
112
+ // meta: {
113
+ // title: "Studious Backend",
114
+ // description:
115
+ // "This is the backend for the Studious application.",
116
+ // },
117
+ // })
118
+ // );
119
+ // });
120
+
121
+
89
122
  // Create HTTP server
90
123
  const httpServer = createServer(app);
91
124
 
92
- app.get('/health', (req, res) => {
93
- res.status(200).json({ message: 'OK' });
125
+ app.get('/health', async (req, res) => {
126
+
127
+ try {
128
+ // Check database connectivity
129
+ await prisma.$queryRaw`SELECT 1`;
130
+
131
+ res.status(200).json({
132
+ status: 'OK',
133
+ timestamp: new Date().toISOString(),
134
+ uptime: process.uptime(),
135
+ database: 'connected'
136
+ });
137
+ } catch (error) {
138
+ res.status(503).json({
139
+ status: 'ERROR',
140
+ database: 'disconnected',
141
+ error: error instanceof Error ? error.message : 'Unknown error'
142
+ });
143
+ }
144
+ });
145
+
146
+ // Pusher channel auth (for private-* and presence-* channels)
147
+ // Token from: x-user header, or cookie (same-origin requests send cookies automatically)
148
+ app.post('/api/pusher/auth', async (req, res) => {
149
+ try {
150
+ let token = req.headers['x-user'] as string | undefined;
151
+ if (!token && req.headers.cookie) {
152
+ const cookieName = env.PUSHER_AUTH_COOKIE_NAME || 'token';
153
+ const match = req.headers.cookie.match(new RegExp(`${cookieName}=([^;]+)`));
154
+ token = match?.[1]?.trim();
155
+ }
156
+ const { socket_id, channel_name } = req.body as { socket_id?: string; channel_name?: string };
157
+
158
+ if (!socket_id || !channel_name) {
159
+ return res.status(400).json({ error: 'socket_id and channel_name required' });
160
+ }
161
+
162
+ if (!token) {
163
+ return res.status(401).json({ error: 'Authentication required' });
164
+ }
165
+
166
+ const user = await prisma.user.findFirst({
167
+ where: { sessions: { some: { id: token } } },
168
+ select: { id: true, username: true },
169
+ });
170
+
171
+ if (!user) {
172
+ return res.status(401).json({ error: 'Invalid or expired session' });
173
+ }
174
+
175
+ // Verify channel access for private-conversation-* channels
176
+ if (channel_name.startsWith('private-conversation-')) {
177
+ const conversationId = channel_name.replace('private-conversation-', '');
178
+ const member = await prisma.conversationMember.findFirst({
179
+ where: { conversationId, userId: user.id },
180
+ });
181
+
182
+ if (!member) {
183
+ return res.status(403).json({ error: 'Not a member of this conversation' });
184
+ }
185
+ }
186
+
187
+ if (channel_name.startsWith('private-worksheet-')) {
188
+ const worksheetId = channel_name.replace('private-worksheet-', '');
189
+ const worksheet = await prisma.studentWorksheetResponse.findFirst({
190
+ where: { id: worksheetId, OR: [
191
+ { studentId: user.id },
192
+ { submission: { assignment: { class: { teachers: { some: { id: user.id } } } } } },
193
+ ] },
194
+ });
195
+ if (!worksheet) {
196
+ return res.status(403).json({ error: 'No access to this worksheet' });
197
+ }
198
+ }
199
+
200
+ if (channel_name.startsWith('private-teacher-')) {
201
+ const classId = channel_name.replace('private-teacher-', '');
202
+ const isTeacher = await prisma.class.findFirst({
203
+ where: { id: classId, teachers: { some: { id: user.id } } },
204
+ });
205
+ if (!isTeacher) {
206
+ return res.status(403).json({ error: 'Not a teacher of this class' });
207
+ }
208
+ }
209
+
210
+ // Verify channel access for private-class-* channels
211
+ // if (channel_name.startsWith('private-class-')) {
212
+ // const classId = channel_name.replace('private-class-', '');
213
+ // const isMember = await prisma.class.findFirst({
214
+ // where: {
215
+ // id: classId,
216
+ // OR: [
217
+ // { students: { some: { id: user.id } } },
218
+ // { teachers: { some: { id: user.id } } },
219
+ // ],
220
+ // },
221
+ // });
222
+ // if (!isMember) {
223
+ // return res.status(403).json({ error: 'Not a member of this class' });
224
+ // }
225
+ // }
226
+
227
+ if (channel_name.startsWith('presence-')) {
228
+ const authResponse = pusher.authorizeChannel(socket_id, channel_name, {
229
+ user_id: user.id,
230
+ user_info: { username: user.username },
231
+ });
232
+ return res.json(authResponse);
233
+ }
234
+
235
+ const authResponse = pusher.authorizeChannel(socket_id, channel_name);
236
+ return res.json(authResponse);
237
+ } catch (error) {
238
+ logger.error('Pusher auth error', { error });
239
+ return res.status(500).json({ error: 'Authentication failed' });
240
+ }
94
241
  });
95
242
 
96
243
  // Setup Socket.IO
@@ -103,7 +250,7 @@ const io = new Server(httpServer, {
103
250
  'http://127.0.0.1:3001', // Alternative localhost
104
251
  'https://www.studious.sh', // Production frontend
105
252
  'https://studious.sh', // Production frontend (without www)
106
- process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
253
+ env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
107
254
  ],
108
255
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
109
256
  credentials: true,
@@ -326,12 +473,15 @@ app.get('/api/files/:fileId', async (req, res) => {
326
473
  }
327
474
  });
328
475
 
476
+ app.use('/trpc/auth.login', authLimiter);
477
+ app.use('/trpc/auth.register', authLimiter);
478
+
329
479
  // File upload endpoint for secure file uploads (supports both POST and PUT)
330
- app.post('/api/upload/:filePath', async (req, res) => {
480
+ app.post('/api/upload/:filePath', uploadLimiter, async (req, res) => {
331
481
  handleFileUpload(req, res);
332
482
  });
333
483
 
334
- app.put('/api/upload/:filePath', async (req, res) => {
484
+ app.put('/api/upload/:filePath', uploadLimiter, async (req, res) => {
335
485
  handleFileUpload(req, res);
336
486
  });
337
487
 
@@ -348,7 +498,7 @@ function handleFileUpload(req: any, res: any) {
348
498
  'http://127.0.0.1:3001',
349
499
  'https://www.studious.sh', // Production frontend
350
500
  'https://studious.sh', // Production frontend (without www)
351
- process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
501
+ env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
352
502
  ];
353
503
 
354
504
  if (origin && allowedOrigins.includes(origin)) {
@@ -411,21 +561,35 @@ app.use(
411
561
  })
412
562
  );
413
563
 
414
- const PORT = process.env.PORT || 3001;
564
+ // IMPORTANT: Sentry error handler must be added AFTER all other middleware and routes
565
+ // but BEFORE any other error handlers
566
+ Sentry.setupExpressErrorHandler(app);
567
+
568
+ // app.use(function onError(err, req, res, next) {
569
+ // // The error id is attached to `res.sentry` to be returned
570
+ // // and optionally displayed to the user for support.
571
+ // res.statusCode = 500;
572
+ // res.end(res.sentry + "\n");
573
+ // });
415
574
 
416
- httpServer.listen(PORT, () => {
417
- logger.info(`Server running on port ${PORT}`, {
418
- port: PORT,
419
- services: ['tRPC', 'Socket.IO']
575
+
576
+ const PORT = env.PORT || 3001;
577
+
578
+ connectRedis().then(() => {
579
+ httpServer.listen(PORT, () => {
580
+ logger.info(`Server running on port ${PORT}`, {
581
+ port: PORT,
582
+ services: ['tRPC', 'Socket.IO', env.REDIS_URL ? 'Redis' : null].filter(Boolean),
583
+ });
420
584
  });
421
585
  });
422
586
 
423
587
  // log all env variables
424
588
  logger.info('Configurations', {
425
- NODE_ENV: process.env.NODE_ENV,
426
- PORT: process.env.PORT,
427
- NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
428
- LOG_MODE: process.env.LOG_MODE,
589
+ NODE_ENV: env.NODE_ENV,
590
+ PORT: env.PORT,
591
+ NEXT_PUBLIC_APP_URL: env.NEXT_PUBLIC_APP_URL,
592
+ LOG_MODE: env.LOG_MODE,
429
593
  });
430
594
 
431
595
  // Log CORS configuration
@@ -435,6 +599,37 @@ logger.info('CORS Configuration', {
435
599
  'http://localhost:3001',
436
600
  'http://127.0.0.1:3000',
437
601
  'http://127.0.0.1:3001',
438
- process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
602
+ env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
439
603
  ]
440
- });
604
+ });
605
+
606
+ const gracefulShutdown = (signal: string) => {
607
+ logger.info(`Received ${signal}, shutting down gracefully`);
608
+
609
+ httpServer.close(() => {
610
+ logger.info('HTTP server closed');
611
+
612
+ io.close(() => {
613
+ logger.info('Socket.IO server closed');
614
+
615
+ disconnectRedis().then(() =>
616
+ prisma.$disconnect().then(() => {
617
+ logger.info('Database connections closed');
618
+ process.exit(0);
619
+ }).catch((err) => {
620
+ logger.error('Error disconnecting from database', { error: err });
621
+ process.exit(1);
622
+ })
623
+ );
624
+ });
625
+ });
626
+
627
+ // Force shutdown after 10 seconds
628
+ setTimeout(() => {
629
+ logger.error('Forced shutdown after timeout');
630
+ process.exit(1);
631
+ }, 10000);
632
+ };
633
+
634
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
635
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
@@ -0,0 +1,15 @@
1
+ import * as Sentry from "@sentry/node";
2
+ import { env } from "./lib/config/env.js";
3
+
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,132 @@
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_PORT: z.string().transform(Number).default('587'),
40
+ EMAIL_USER: z.string().email('EMAIL_USER must be a valid email'),
41
+ EMAIL_PASS: z.string().min(1, 'EMAIL_PASS is required'),
42
+ EMAIL_DRY_RUN: z.string().optional().default('false'),
43
+ GOOGLE_CLOUD_PROJECT_ID: z.string().min(1, 'GOOGLE_CLOUD_PROJECT_ID is required'),
44
+ GOOGLE_CLOUD_CLIENT_EMAIL: z.string().email('GOOGLE_CLOUD_CLIENT_EMAIL must be a valid email'),
45
+ GOOGLE_CLOUD_PRIVATE_KEY: z.string().min(1, 'GOOGLE_CLOUD_PRIVATE_KEY is required'),
46
+ GOOGLE_CLOUD_BUCKET_NAME: z.string().min(1, 'GOOGLE_CLOUD_BUCKET_NAME is required'),
47
+ PUSHER_APP_ID: z.string().min(1, 'PUSHER_APP_ID is required'),
48
+ PUSHER_KEY: z.string().min(1, 'PUSHER_KEY is required'),
49
+ PUSHER_SECRET: z.string().min(1, 'PUSHER_SECRET is required'),
50
+ PUSHER_CLUSTER: z.string().min(1, 'PUSHER_CLUSTER is required'),
51
+ PUSHER_AUTH_COOKIE_NAME: z.string().optional(), // Cookie name for session token (default: 'token')
52
+ REDIS_URL: z.string().url().optional(), // Redis connection URL (e.g. redis://localhost:6379)
53
+ INFERENCE_API_KEY: z.string().optional(),
54
+ INFERENCE_API_BASE_URL: z.string().url().optional(),
55
+ OPENAI_API_KEY: z.string().optional(),
56
+ LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('normal'),
57
+ });
58
+
59
+ // Test schema - only require what's needed for tests
60
+ const testSchema = baseSchema.extend({
61
+ NEXT_PUBLIC_APP_URL: z.string().url().optional().default('http://localhost:3000'),
62
+ BACKEND_URL: z.string().url().optional().default('http://localhost:3001'),
63
+ SENTRY_DSN: z.string().url().optional(),
64
+ EMAIL_HOST: z.string().optional().default('smtp.test.com'),
65
+ EMAIL_PORT: z.string().transform(Number).default('587'),
66
+ EMAIL_USER: z.string().email().optional().default('test@test.com'),
67
+ EMAIL_PASS: z.string().optional().default('test'),
68
+ EMAIL_DRY_RUN: z.string().optional().default('false'),
69
+ GOOGLE_CLOUD_PROJECT_ID: z.string().optional().default('test-project'),
70
+ GOOGLE_CLOUD_CLIENT_EMAIL: z.string().email().optional().default('test@test.iam.gserviceaccount.com'),
71
+ GOOGLE_CLOUD_PRIVATE_KEY: z.string().optional().default('test-key'),
72
+ GOOGLE_CLOUD_BUCKET_NAME: z.string().optional().default('test-bucket'),
73
+ PUSHER_APP_ID: z.string().optional().default('test-app-id'),
74
+ PUSHER_KEY: z.string().optional().default('test-key'),
75
+ PUSHER_SECRET: z.string().optional().default('test-secret'),
76
+ PUSHER_CLUSTER: z.string().optional().default('us2'),
77
+ PUSHER_AUTH_COOKIE_NAME: z.string().optional(),
78
+ REDIS_URL: z.string().url().optional(),
79
+ INFERENCE_API_KEY: z.string().optional(),
80
+ OPENAI_API_KEY: z.string().optional(),
81
+ INFERENCE_API_BASE_URL: z.string().url().optional(),
82
+ LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('quiet'),
83
+ });
84
+
85
+ // Use test schema in test mode, full schema otherwise
86
+ const envSchema = isTest ? testSchema : fullSchema;
87
+
88
+ // Validate environment variables
89
+ function validateEnv() {
90
+ try {
91
+ const parsed = envSchema.parse(process.env);
92
+
93
+ // Only exit on validation failure in production
94
+ if (isProduction && !parsed.DATABASE_URL) {
95
+ logger.error('DATABASE_URL is required in production');
96
+ process.exit(1);
97
+ }
98
+
99
+ return parsed;
100
+ } catch (error) {
101
+ if (error instanceof z.ZodError) {
102
+ const missingVars = error.errors.map(err => ({
103
+ path: err.path.join('.'),
104
+ message: err.message,
105
+ }));
106
+
107
+ logger.error('Environment variable validation failed', {
108
+ envFile,
109
+ missingVars,
110
+ });
111
+
112
+ // Only exit in production - in test/dev, log warning but continue
113
+ if (isProduction) {
114
+ logger.error(`Please check your ${envFile} file and ensure all required variables are set.`);
115
+ process.exit(1);
116
+ } else {
117
+ logger.warn('Continuing with defaults - some features may not work correctly', {
118
+ envFile,
119
+ });
120
+ // Return parsed with defaults for non-production
121
+ return envSchema.parse({ ...process.env });
122
+ }
123
+ }
124
+ throw error;
125
+ }
126
+ }
127
+
128
+ // Export validated environment variables
129
+ export const env = validateEnv();
130
+
131
+ // Type-safe environment access
132
+ export type Env = z.infer<typeof envSchema>;
@@ -1,9 +1,9 @@
1
1
  import { TRPCError } from "@trpc/server";
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
  import { getSignedUrl, objectExists } from "./googleCloudStorage.js";
4
- import { generateMediaThumbnail } from "./thumbnailGenerator.js";
5
4
  import { prisma } from "./prisma.js";
6
5
  import { logger } from "../utils/logger.js";
6
+ import { env } from "./config/env.js";
7
7
 
8
8
  export interface FileData {
9
9
  name: string;
@@ -102,7 +102,8 @@ export async function createDirectUploadFile(
102
102
  userId: string,
103
103
  directory?: string,
104
104
  assignmentId?: string,
105
- submissionId?: string
105
+ submissionId?: string,
106
+ announcementId?: string
106
107
  ): Promise<DirectUploadFile> {
107
108
  try {
108
109
  // Validate file extension matches MIME type
@@ -135,7 +136,7 @@ export async function createDirectUploadFile(
135
136
  const uploadSessionId = uuidv4();
136
137
 
137
138
  // Generate backend proxy upload URL (not direct GCS)
138
- const baseUrl = process.env.BACKEND_URL || 'http://localhost:3001';
139
+ const baseUrl = env.BACKEND_URL || 'http://localhost:3001';
139
140
  const uploadUrl = `${baseUrl}/api/upload/${encodeURIComponent(filePath)}`;
140
141
  const uploadExpiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes from now
141
142
 
@@ -167,6 +168,11 @@ export async function createDirectUploadFile(
167
168
  submission: {
168
169
  connect: { id: submissionId }
169
170
  }
171
+ }),
172
+ ...(announcementId && {
173
+ announcement: {
174
+ connect: { id: announcementId }
175
+ }
170
176
  })
171
177
  },
172
178
  });
@@ -225,7 +231,7 @@ export async function confirmDirectUpload(
225
231
  // If uploadSuccess is true, verify the object actually exists in GCS
226
232
  if (uploadSuccess) {
227
233
  try {
228
- const exists = await objectExists(process.env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
234
+ const exists = await objectExists(env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
229
235
  if (!exists) {
230
236
  actualUploadSuccess = false;
231
237
  actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';
@@ -323,11 +329,12 @@ export async function createDirectUploadFiles(
323
329
  userId: string,
324
330
  directory?: string,
325
331
  assignmentId?: string,
326
- submissionId?: string
332
+ submissionId?: string,
333
+ announcementId?: string
327
334
  ): Promise<DirectUploadFile[]> {
328
335
  try {
329
336
  const uploadPromises = files.map(file =>
330
- createDirectUploadFile(file, userId, directory, assignmentId, submissionId)
337
+ createDirectUploadFile(file, userId, directory, assignmentId, submissionId, announcementId)
331
338
  );
332
339
  return await Promise.all(uploadPromises);
333
340
  } catch (error) {
@@ -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;
@@ -81,4 +81,21 @@ export async function objectExists(bucketName: string, objectPath: string): Prom
81
81
  message: 'Failed to check object existence',
82
82
  });
83
83
  }
84
+ }
85
+
86
+ /**
87
+ * Copies a file within the same bucket to a new path
88
+ * @param sourcePath The GCS path of the source file
89
+ * @param destPath The GCS path for the destination
90
+ */
91
+ export async function copyFile(sourcePath: string, destPath: string): Promise<void> {
92
+ try {
93
+ await bucket.file(sourcePath).copy(destPath);
94
+ } catch (error) {
95
+ console.error('Error copying file in Google Cloud Storage:', error);
96
+ throw new TRPCError({
97
+ code: 'INTERNAL_SERVER_ERROR',
98
+ message: 'Failed to copy file in storage',
99
+ });
100
+ }
84
101
  }