@studious-lms/server 1.1.24 → 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 (485) 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 +82 -15
  23. package/dist/lib/fileUpload.js.map +1 -0
  24. package/dist/lib/googleCloudStorage.d.ts +13 -0
  25. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  26. package/dist/lib/googleCloudStorage.js +45 -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 +25 -0
  35. package/dist/lib/notificationHandler.d.ts.map +1 -0
  36. package/dist/lib/notificationHandler.js +32 -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 +6403 -3741
  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 +547 -57
  163. package/dist/routers/announcement.js.map +1 -0
  164. package/dist/routers/assignment.d.ts +431 -318
  165. package/dist/routers/assignment.d.ts.map +1 -1
  166. package/dist/routers/assignment.js +104 -1559
  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 -295
  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 -877
  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 +37 -6
  221. package/dist/routers/section.d.ts.map +1 -1
  222. package/dist/routers/section.js +26 -167
  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 +311 -289
  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 +4 -0
  350. package/dist/utils/logger.d.ts.map +1 -1
  351. package/dist/utils/logger.js +35 -3
  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 +81 -16
  370. package/src/lib/googleCloudStorage.ts +42 -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 +622 -57
  403. package/src/routers/assignment.ts +157 -1688
  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 -341
  411. package/src/routers/folder.ts +107 -836
  412. package/src/routers/labChat.ts +29 -960
  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 -200
  418. package/src/routers/user.ts +49 -226
  419. package/src/routers/worksheet.ts +252 -0
  420. package/src/seedDatabase.ts +330 -290
  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 +33 -3
  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 -65
  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/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
- import { getSignedUrl } from "./googleCloudStorage.js";
4
- import { generateMediaThumbnail } from "./thumbnailGenerator.js";
3
+ import { getSignedUrl, objectExists } from "./googleCloudStorage.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
  });
@@ -182,7 +188,11 @@ export async function createDirectUploadFile(
182
188
  uploadSessionId
183
189
  };
184
190
  } catch (error) {
185
- console.error('Error creating direct upload file:', error);
191
+ logger.error('Error creating direct upload file:', {error: error instanceof Error ? {
192
+ name: error.name,
193
+ message: error.message,
194
+ stack: error.stack,
195
+ } : error});
186
196
  throw new TRPCError({
187
197
  code: 'INTERNAL_SERVER_ERROR',
188
198
  message: 'Failed to create direct upload file',
@@ -202,17 +212,53 @@ export async function confirmDirectUpload(
202
212
  errorMessage?: string
203
213
  ): Promise<void> {
204
214
  try {
215
+ // First fetch the file record to get the object path
216
+ const fileRecord = await prisma.file.findUnique({
217
+ where: { id: fileId },
218
+ select: { path: true }
219
+ });
220
+
221
+ if (!fileRecord) {
222
+ throw new TRPCError({
223
+ code: 'NOT_FOUND',
224
+ message: 'File record not found',
225
+ });
226
+ }
227
+
228
+ let actualUploadSuccess = uploadSuccess;
229
+ let actualErrorMessage = errorMessage;
230
+
231
+ // If uploadSuccess is true, verify the object actually exists in GCS
232
+ if (uploadSuccess) {
233
+ try {
234
+ const exists = await objectExists(env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
235
+ if (!exists) {
236
+ actualUploadSuccess = false;
237
+ actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';
238
+ logger.error(`File upload verification failed for ${fileId}: object ${fileRecord.path} not found in GCS`);
239
+ }
240
+ } catch (error) {
241
+ logger.error(`Error verifying file existence in GCS for ${fileId}:`, {error: error instanceof Error ? {
242
+ name: error.name,
243
+ message: error.message,
244
+ stack: error.stack,
245
+ } : error});
246
+ actualUploadSuccess = false;
247
+ actualErrorMessage = 'Failed to verify file existence in Google Cloud Storage';
248
+ }
249
+ }
250
+
205
251
  const updateData: any = {
206
- uploadStatus: uploadSuccess ? 'COMPLETED' : 'FAILED',
207
- uploadProgress: uploadSuccess ? 100 : 0,
252
+ uploadStatus: actualUploadSuccess ? 'COMPLETED' : 'FAILED',
253
+ uploadProgress: actualUploadSuccess ? 100 : 0,
208
254
  };
209
255
 
210
- if (!uploadSuccess && errorMessage) {
211
- updateData.uploadError = errorMessage;
256
+ if (!actualUploadSuccess && actualErrorMessage) {
257
+ updateData.uploadError = actualErrorMessage;
212
258
  updateData.uploadRetryCount = { increment: 1 };
213
259
  }
214
260
 
215
- if (uploadSuccess) {
261
+ if (actualUploadSuccess) {
216
262
  updateData.uploadedAt = new Date();
217
263
  }
218
264
 
@@ -221,7 +267,7 @@ export async function confirmDirectUpload(
221
267
  data: updateData
222
268
  });
223
269
  } catch (error) {
224
- console.error('Error confirming direct upload:', error);
270
+ logger.error('Error confirming direct upload:', {error});
225
271
  throw new TRPCError({
226
272
  code: 'INTERNAL_SERVER_ERROR',
227
273
  message: 'Failed to confirm upload',
@@ -239,15 +285,29 @@ export async function updateUploadProgress(
239
285
  progress: number
240
286
  ): Promise<void> {
241
287
  try {
288
+ // await prisma.file.update({
289
+ // where: { id: fileId },
290
+ // data: {
291
+ // uploadStatus: 'UPLOADING',
292
+ // uploadProgress: Math.min(100, Math.max(0, progress))
293
+ // }
294
+ // });
295
+ const current = await prisma.file.findUnique({ where: { id: fileId }, select: { uploadStatus: true } });
296
+ if (!current || ['COMPLETED','FAILED','CANCELLED'].includes(current.uploadStatus as string)) return;
297
+ const clamped = Math.min(100, Math.max(0, progress));
242
298
  await prisma.file.update({
243
299
  where: { id: fileId },
244
300
  data: {
245
301
  uploadStatus: 'UPLOADING',
246
- uploadProgress: Math.min(100, Math.max(0, progress))
302
+ uploadProgress: clamped
247
303
  }
248
304
  });
249
305
  } catch (error) {
250
- console.error('Error updating upload progress:', error);
306
+ logger.error('Error updating upload progress:', {error: error instanceof Error ? {
307
+ name: error.name,
308
+ message: error.message,
309
+ stack: error.stack,
310
+ } : error});
251
311
  throw new TRPCError({
252
312
  code: 'INTERNAL_SERVER_ERROR',
253
313
  message: 'Failed to update upload progress',
@@ -269,15 +329,20 @@ export async function createDirectUploadFiles(
269
329
  userId: string,
270
330
  directory?: string,
271
331
  assignmentId?: string,
272
- submissionId?: string
332
+ submissionId?: string,
333
+ announcementId?: string
273
334
  ): Promise<DirectUploadFile[]> {
274
335
  try {
275
336
  const uploadPromises = files.map(file =>
276
- createDirectUploadFile(file, userId, directory, assignmentId, submissionId)
337
+ createDirectUploadFile(file, userId, directory, assignmentId, submissionId, announcementId)
277
338
  );
278
339
  return await Promise.all(uploadPromises);
279
340
  } catch (error) {
280
- console.error('Error creating direct upload files:', error);
341
+ logger.error('Error creating direct upload files:', {error: error instanceof Error ? {
342
+ name: error.name,
343
+ message: error.message,
344
+ stack: error.stack,
345
+ } : error});
281
346
  throw new TRPCError({
282
347
  code: 'INTERNAL_SERVER_ERROR',
283
348
  message: 'Failed to create direct upload files',