@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.
- package/.coderabbit.yaml +9 -0
- package/.env.example +53 -0
- package/.env.test.example +37 -0
- package/README.md +34 -7
- package/dist/exportType.d.ts.map +1 -1
- package/dist/exportType.js +4 -0
- package/dist/exportType.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +212 -51
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts +2 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +18 -0
- package/dist/instrument.js.map +1 -0
- package/dist/lib/config/env.d.ts +190 -0
- package/dist/lib/config/env.d.ts.map +1 -0
- package/dist/lib/config/env.js +121 -0
- package/dist/lib/config/env.js.map +1 -0
- package/dist/lib/fileUpload.d.ts +2 -2
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +82 -15
- package/dist/lib/fileUpload.js.map +1 -0
- package/dist/lib/googleCloudStorage.d.ts +13 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +45 -6
- package/dist/lib/googleCloudStorage.js.map +1 -0
- package/dist/lib/jsonConversion.d.ts.map +1 -1
- package/dist/lib/jsonConversion.js +16 -14
- package/dist/lib/jsonConversion.js.map +1 -0
- package/dist/lib/jsonStyles.d.ts.map +1 -1
- package/dist/lib/jsonStyles.js +4 -0
- package/dist/lib/jsonStyles.js.map +1 -0
- package/dist/lib/notificationHandler.d.ts +25 -0
- package/dist/lib/notificationHandler.d.ts.map +1 -0
- package/dist/lib/notificationHandler.js +32 -0
- package/dist/lib/notificationHandler.js.map +1 -0
- package/dist/lib/prisma.d.ts +2 -2
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +24 -1
- package/dist/lib/prisma.js.map +1 -0
- package/dist/lib/pusher.d.ts +4 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +14 -6
- package/dist/lib/pusher.js.map +1 -0
- package/dist/lib/redis.d.ts +5 -0
- package/dist/lib/redis.d.ts.map +1 -0
- package/dist/lib/redis.js +53 -0
- package/dist/lib/redis.js.map +1 -0
- package/dist/lib/thumbnailGenerator.d.ts +0 -21
- package/dist/lib/thumbnailGenerator.d.ts.map +1 -1
- package/dist/lib/thumbnailGenerator.js +159 -158
- package/dist/lib/thumbnailGenerator.js.map +1 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +41 -93
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/logging.d.ts.map +1 -1
- package/dist/middleware/logging.js +4 -0
- package/dist/middleware/logging.js.map +1 -0
- package/dist/middleware/security.d.ts +5 -0
- package/dist/middleware/security.d.ts.map +1 -0
- package/dist/middleware/security.js +77 -0
- package/dist/middleware/security.js.map +1 -0
- package/dist/models/agenda.d.ts +97 -0
- package/dist/models/agenda.d.ts.map +1 -0
- package/dist/models/agenda.js +40 -0
- package/dist/models/agenda.js.map +1 -0
- package/dist/models/announcement.d.ts +223 -0
- package/dist/models/announcement.d.ts.map +1 -0
- package/dist/models/announcement.js +120 -0
- package/dist/models/announcement.js.map +1 -0
- package/dist/models/assignment.d.ts +1292 -0
- package/dist/models/assignment.d.ts.map +1 -0
- package/dist/models/assignment.js +309 -0
- package/dist/models/assignment.js.map +1 -0
- package/dist/models/attendance.d.ts +180 -0
- package/dist/models/attendance.d.ts.map +1 -0
- package/dist/models/attendance.js +188 -0
- package/dist/models/attendance.js.map +1 -0
- package/dist/models/auth.d.ts +153 -0
- package/dist/models/auth.d.ts.map +1 -0
- package/dist/models/auth.js +217 -0
- package/dist/models/auth.js.map +1 -0
- package/dist/models/class.d.ts +439 -0
- package/dist/models/class.d.ts.map +1 -0
- package/dist/models/class.js +546 -0
- package/dist/models/class.js.map +1 -0
- package/dist/models/comment.d.ts +171 -0
- package/dist/models/comment.d.ts.map +1 -0
- package/dist/models/comment.js +138 -0
- package/dist/models/comment.js.map +1 -0
- package/dist/models/conversation.d.ts +164 -0
- package/dist/models/conversation.d.ts.map +1 -0
- package/dist/models/conversation.js +175 -0
- package/dist/models/conversation.js.map +1 -0
- package/dist/models/event.d.ts +295 -0
- package/dist/models/event.d.ts.map +1 -0
- package/dist/models/event.js +145 -0
- package/dist/models/event.js.map +1 -0
- package/dist/models/file.d.ts +536 -0
- package/dist/models/file.d.ts.map +1 -0
- package/dist/models/file.js +126 -0
- package/dist/models/file.js.map +1 -0
- package/dist/models/folder.d.ts +295 -0
- package/dist/models/folder.d.ts.map +1 -0
- package/dist/models/folder.js +202 -0
- package/dist/models/folder.js.map +1 -0
- package/dist/models/labChat.d.ts +243 -0
- package/dist/models/labChat.d.ts.map +1 -0
- package/dist/models/labChat.js +204 -0
- package/dist/models/labChat.js.map +1 -0
- package/dist/models/marketing.d.ts +72 -0
- package/dist/models/marketing.d.ts.map +1 -0
- package/dist/models/marketing.js +26 -0
- package/dist/models/marketing.js.map +1 -0
- package/dist/models/message.d.ts +100 -0
- package/dist/models/message.d.ts.map +1 -0
- package/dist/models/message.js +131 -0
- package/dist/models/message.js.map +1 -0
- package/dist/models/newtonChat.d.ts +72 -0
- package/dist/models/newtonChat.d.ts.map +1 -0
- package/dist/models/newtonChat.js +61 -0
- package/dist/models/newtonChat.js.map +1 -0
- package/dist/models/notification.d.ts +65 -0
- package/dist/models/notification.d.ts.map +1 -0
- package/dist/models/notification.js +46 -0
- package/dist/models/notification.js.map +1 -0
- package/dist/models/section.d.ts +102 -0
- package/dist/models/section.d.ts.map +1 -0
- package/dist/models/section.js +83 -0
- package/dist/models/section.js.map +1 -0
- package/dist/models/user.d.ts +39 -0
- package/dist/models/user.d.ts.map +1 -0
- package/dist/models/user.js +38 -0
- package/dist/models/user.js.map +1 -0
- package/dist/models/worksheet.d.ts +460 -0
- package/dist/models/worksheet.d.ts.map +1 -0
- package/dist/models/worksheet.js +200 -0
- package/dist/models/worksheet.js.map +1 -0
- package/dist/pipelines/aiLabChat.d.ts +21 -0
- package/dist/pipelines/aiLabChat.d.ts.map +1 -0
- package/dist/pipelines/aiLabChat.js +460 -0
- package/dist/pipelines/aiLabChat.js.map +1 -0
- package/dist/pipelines/aiNewtonChat.d.ts +30 -0
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -0
- package/dist/pipelines/aiNewtonChat.js +289 -0
- package/dist/pipelines/aiNewtonChat.js.map +1 -0
- package/dist/pipelines/gradeWorksheet.d.ts +30 -0
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -0
- package/dist/pipelines/gradeWorksheet.js +252 -0
- package/dist/pipelines/gradeWorksheet.js.map +1 -0
- package/dist/routers/_app.d.ts +6403 -3741
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +10 -0
- package/dist/routers/_app.js.map +1 -0
- package/dist/routers/agenda.d.ts +58 -6
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +6 -58
- package/dist/routers/agenda.js.map +1 -0
- package/dist/routers/announcement.d.ts +325 -6
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +547 -57
- package/dist/routers/announcement.js.map +1 -0
- package/dist/routers/assignment.d.ts +431 -318
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +104 -1559
- package/dist/routers/assignment.js.map +1 -0
- package/dist/routers/attendance.d.ts +20 -9
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +10 -263
- package/dist/routers/attendance.js.map +1 -0
- package/dist/routers/auth.d.ts +21 -1
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +37 -241
- package/dist/routers/auth.js.map +1 -0
- package/dist/routers/class.d.ts +198 -68
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +88 -909
- package/dist/routers/class.js.map +1 -0
- package/dist/routers/comment.d.ts +153 -0
- package/dist/routers/comment.d.ts.map +1 -0
- package/dist/routers/comment.js +58 -0
- package/dist/routers/comment.js.map +1 -0
- package/dist/routers/conversation.d.ts +73 -3
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +23 -265
- package/dist/routers/conversation.js.map +1 -0
- package/dist/routers/event.d.ts +46 -37
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/event.js +15 -431
- package/dist/routers/event.js.map +1 -0
- package/dist/routers/file.d.ts +4 -2
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +11 -295
- package/dist/routers/file.js.map +1 -0
- package/dist/routers/folder.d.ts +21 -14
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +36 -743
- package/dist/routers/folder.js.map +1 -0
- package/dist/routers/labChat.d.ts +12 -9
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +21 -877
- package/dist/routers/labChat.js.map +1 -0
- package/dist/routers/marketing.d.ts +2 -2
- package/dist/routers/marketing.d.ts.map +1 -1
- package/dist/routers/marketing.js +9 -54
- package/dist/routers/marketing.js.map +1 -0
- package/dist/routers/message.d.ts +2 -1
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +29 -519
- package/dist/routers/message.js.map +1 -0
- package/dist/routers/newtonChat.d.ts +55 -0
- package/dist/routers/newtonChat.d.ts.map +1 -0
- package/dist/routers/newtonChat.js +22 -0
- package/dist/routers/newtonChat.js.map +1 -0
- package/dist/routers/notifications.d.ts +8 -8
- package/dist/routers/notifications.d.ts.map +1 -1
- package/dist/routers/notifications.js +20 -81
- package/dist/routers/notifications.js.map +1 -0
- package/dist/routers/section.d.ts +37 -6
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +26 -167
- package/dist/routers/section.js.map +1 -0
- package/dist/routers/user.d.ts +1 -1
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +34 -204
- package/dist/routers/user.js.map +1 -0
- package/dist/routers/worksheet.d.ts +362 -0
- package/dist/routers/worksheet.d.ts.map +1 -0
- package/dist/routers/worksheet.js +153 -0
- package/dist/routers/worksheet.js.map +1 -0
- package/dist/seedDatabase.d.ts +2 -3
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +311 -289
- package/dist/seedDatabase.js.map +1 -0
- package/dist/server/pipelines/aiLabChat.d.ts +21 -0
- package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiLabChat.js +456 -0
- package/dist/server/pipelines/aiLabChat.js.map +1 -0
- package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
- package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiNewtonChat.js +285 -0
- package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts +30 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.js +248 -0
- package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
- package/dist/services/agenda.d.ts +100 -0
- package/dist/services/agenda.d.ts.map +1 -0
- package/dist/services/agenda.js +21 -0
- package/dist/services/agenda.js.map +1 -0
- package/dist/services/announcement.d.ts +135 -0
- package/dist/services/announcement.d.ts.map +1 -0
- package/dist/services/announcement.js +223 -0
- package/dist/services/announcement.js.map +1 -0
- package/dist/services/assignment.d.ts +1462 -0
- package/dist/services/assignment.d.ts.map +1 -0
- package/dist/services/assignment.js +898 -0
- package/dist/services/assignment.js.map +1 -0
- package/dist/services/attendance.d.ts +93 -0
- package/dist/services/attendance.d.ts.map +1 -0
- package/dist/services/attendance.js +61 -0
- package/dist/services/attendance.js.map +1 -0
- package/dist/services/auth.d.ts +68 -0
- package/dist/services/auth.d.ts.map +1 -0
- package/dist/services/auth.js +218 -0
- package/dist/services/auth.js.map +1 -0
- package/dist/services/class.d.ts +621 -0
- package/dist/services/class.d.ts.map +1 -0
- package/dist/services/class.js +474 -0
- package/dist/services/class.js.map +1 -0
- package/dist/services/comment.d.ts +100 -0
- package/dist/services/comment.d.ts.map +1 -0
- package/dist/services/comment.js +83 -0
- package/dist/services/comment.js.map +1 -0
- package/dist/services/conversation.d.ts +159 -0
- package/dist/services/conversation.d.ts.map +1 -0
- package/dist/services/conversation.js +138 -0
- package/dist/services/conversation.js.map +1 -0
- package/dist/services/event.d.ts +216 -0
- package/dist/services/event.d.ts.map +1 -0
- package/dist/services/event.js +168 -0
- package/dist/services/event.js.map +1 -0
- package/dist/services/file.d.ts +74 -0
- package/dist/services/file.d.ts.map +1 -0
- package/dist/services/file.js +133 -0
- package/dist/services/file.js.map +1 -0
- package/dist/services/folder.d.ts +239 -0
- package/dist/services/folder.d.ts.map +1 -0
- package/dist/services/folder.js +248 -0
- package/dist/services/folder.js.map +1 -0
- package/dist/services/labChat.d.ts +165 -0
- package/dist/services/labChat.d.ts.map +1 -0
- package/dist/services/labChat.js +289 -0
- package/dist/services/labChat.js.map +1 -0
- package/dist/services/marketing.d.ts +50 -0
- package/dist/services/marketing.d.ts.map +1 -0
- package/dist/services/marketing.js +32 -0
- package/dist/services/marketing.js.map +1 -0
- package/dist/services/message.d.ts +95 -0
- package/dist/services/message.d.ts.map +1 -0
- package/dist/services/message.js +350 -0
- package/dist/services/message.js.map +1 -0
- package/dist/services/newtonChat.d.ts +22 -0
- package/dist/services/newtonChat.d.ts.map +1 -0
- package/dist/services/newtonChat.js +174 -0
- package/dist/services/newtonChat.js.map +1 -0
- package/dist/services/notification.d.ts +65 -0
- package/dist/services/notification.d.ts.map +1 -0
- package/dist/services/notification.js +33 -0
- package/dist/services/notification.js.map +1 -0
- package/dist/services/section.d.ts +53 -0
- package/dist/services/section.d.ts.map +1 -0
- package/dist/services/section.js +199 -0
- package/dist/services/section.js.map +1 -0
- package/dist/services/user.d.ts +48 -0
- package/dist/services/user.d.ts.map +1 -0
- package/dist/services/user.js +141 -0
- package/dist/services/user.js.map +1 -0
- package/dist/services/worksheet.d.ts +239 -0
- package/dist/services/worksheet.d.ts.map +1 -0
- package/dist/services/worksheet.js +235 -0
- package/dist/services/worksheet.js.map +1 -0
- package/dist/socket/handlers.d.ts.map +1 -1
- package/dist/socket/handlers.js +4 -0
- package/dist/socket/handlers.js.map +1 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +4 -0
- package/dist/trpc.js.map +1 -0
- package/dist/types/trpc.d.ts.map +1 -1
- package/dist/types/trpc.js +4 -0
- package/dist/types/trpc.js.map +1 -0
- package/dist/utils/aiUser.d.ts +1 -3
- package/dist/utils/aiUser.d.ts.map +1 -1
- package/dist/utils/aiUser.js +8 -3
- package/dist/utils/aiUser.js.map +1 -0
- package/dist/utils/email.d.ts +12 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +26 -4
- package/dist/utils/email.js.map +1 -0
- package/dist/utils/generateInviteCode.d.ts +1 -2
- package/dist/utils/generateInviteCode.d.ts.map +1 -1
- package/dist/utils/generateInviteCode.js +5 -2
- package/dist/utils/generateInviteCode.js.map +1 -0
- package/dist/utils/inference.d.ts +8 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +78 -10
- package/dist/utils/inference.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +35 -3
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/prismaErrorHandler.d.ts.map +1 -1
- package/dist/utils/prismaErrorHandler.js +7 -0
- package/dist/utils/prismaErrorHandler.js.map +1 -0
- package/dist/utils/prismaWrapper.d.ts +1 -0
- package/dist/utils/prismaWrapper.d.ts.map +1 -1
- package/dist/utils/prismaWrapper.js +8 -0
- package/dist/utils/prismaWrapper.js.map +1 -0
- package/docker-compose.yml +19 -0
- package/package.json +21 -4
- package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +30 -0
- package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +35 -0
- package/prisma/schema.prisma +180 -12
- package/scripts/test-pre-push.ts +14 -0
- package/src/index.ts +247 -52
- package/src/instrument.ts +15 -0
- package/src/lib/config/env.ts +132 -0
- package/src/lib/fileUpload.ts +81 -16
- package/src/lib/googleCloudStorage.ts +42 -6
- package/src/lib/jsonConversion.ts +12 -14
- package/src/lib/prisma.ts +23 -2
- package/src/lib/pusher.ts +11 -6
- package/src/lib/redis.ts +56 -0
- package/src/lib/thumbnailGenerator.ts +170 -168
- package/src/middleware/auth.ts +86 -137
- package/src/middleware/security.ts +80 -0
- package/src/models/agenda.ts +46 -0
- package/src/models/announcement.ts +134 -0
- package/src/models/assignment.ts +322 -0
- package/src/models/attendance.ts +208 -0
- package/src/models/auth.ts +247 -0
- package/src/models/class.ts +598 -0
- package/src/models/comment.ts +152 -0
- package/src/models/conversation.ts +200 -0
- package/src/models/event.ts +177 -0
- package/src/models/file.ts +129 -0
- package/src/models/folder.ts +225 -0
- package/src/models/labChat.ts +213 -0
- package/src/models/marketing.ts +45 -0
- package/src/models/message.ts +153 -0
- package/src/models/newtonChat.ts +70 -0
- package/src/models/notification.ts +54 -0
- package/src/models/section.ts +98 -0
- package/src/models/user.ts +47 -0
- package/src/models/worksheet.ts +294 -0
- package/src/pipelines/aiLabChat.ts +511 -0
- package/src/pipelines/aiNewtonChat.ts +347 -0
- package/src/pipelines/gradeWorksheet.ts +286 -0
- package/src/routers/_app.ts +6 -0
- package/src/routers/agenda.ts +3 -61
- package/src/routers/announcement.ts +622 -57
- package/src/routers/assignment.ts +157 -1688
- package/src/routers/attendance.ts +16 -277
- package/src/routers/auth.ts +79 -313
- package/src/routers/class.ts +265 -1038
- package/src/routers/comment.ts +76 -0
- package/src/routers/conversation.ts +53 -284
- package/src/routers/event.ts +50 -481
- package/src/routers/file.ts +45 -341
- package/src/routers/folder.ts +107 -836
- package/src/routers/labChat.ts +29 -960
- package/src/routers/marketing.ts +35 -77
- package/src/routers/message.ts +45 -571
- package/src/routers/newtonChat.ts +36 -0
- package/src/routers/notifications.ts +32 -82
- package/src/routers/section.ts +58 -200
- package/src/routers/user.ts +49 -226
- package/src/routers/worksheet.ts +252 -0
- package/src/seedDatabase.ts +330 -290
- package/src/services/agenda.ts +21 -0
- package/src/services/announcement.ts +290 -0
- package/src/services/assignment.ts +1198 -0
- package/src/services/attendance.ts +85 -0
- package/src/services/auth.ts +277 -0
- package/src/services/class.ts +622 -0
- package/src/services/comment.ts +106 -0
- package/src/services/conversation.ts +213 -0
- package/src/services/event.ts +231 -0
- package/src/services/file.ts +167 -0
- package/src/services/folder.ts +316 -0
- package/src/services/labChat.ts +352 -0
- package/src/services/marketing.ts +57 -0
- package/src/services/message.ts +461 -0
- package/src/services/newtonChat.ts +222 -0
- package/src/services/notification.ts +50 -0
- package/src/services/section.ts +283 -0
- package/src/services/user.ts +172 -0
- package/src/services/worksheet.ts +358 -0
- package/src/trpc.ts +4 -0
- package/src/utils/aiUser.ts +4 -3
- package/src/utils/email.ts +33 -4
- package/src/utils/generateInviteCode.ts +1 -3
- package/src/utils/inference.ts +89 -10
- package/src/utils/logger.ts +33 -3
- package/src/utils/prismaErrorHandler.ts +3 -0
- package/src/utils/prismaWrapper.ts +4 -0
- package/tests/globalSetup.ts +62 -0
- package/tests/helpers.ts +22 -0
- package/tests/middleware/security.test.ts +42 -0
- package/tests/routers/agenda.test.ts +138 -0
- package/tests/routers/announcement.test.ts +490 -0
- package/tests/routers/assignment.test.ts +837 -0
- package/tests/routers/attendance.test.ts +160 -0
- package/tests/routers/auth.test.ts +171 -0
- package/tests/{class.test.ts → routers/class.test.ts} +163 -92
- package/tests/routers/comment.test.ts +126 -0
- package/tests/routers/conversation.test.ts +145 -0
- package/tests/routers/event.test.ts +289 -0
- package/tests/routers/folder.test.ts +178 -0
- package/tests/routers/labChat.test.ts +115 -0
- package/tests/routers/marketing.test.ts +59 -0
- package/tests/routers/message.test.ts +123 -0
- package/tests/routers/notification.test.ts +69 -0
- package/tests/routers/section.test.ts +208 -0
- package/tests/server/rateLimit.test.ts +73 -0
- package/tests/setup.ts +39 -65
- package/tests/user.test.ts +136 -0
- package/tests/utils/aiUser.test.ts +22 -0
- package/tests/utils/generateInviteCode.test.ts +24 -0
- package/tests/utils/logger.test.ts +74 -0
- package/tests/utils/prismaErrorHandler.test.ts +101 -0
- package/tests/utils/prismaWrapper.test.ts +82 -0
- package/tests/worksheet.test.ts +181 -0
- package/tsconfig.json +9 -2
- package/vitest.config.ts +30 -1
- package/vitest.unit.config.ts +21 -0
- package/API_SPECIFICATION.md +0 -1597
- package/BASE64_REMOVAL_SUMMARY.md +0 -164
- package/CHAT_API_SPEC.md +0 -579
- package/LAB_CHAT_API_SPEC.md +0 -518
- package/dist/routers/school.d.ts +0 -208
- package/dist/routers/school.d.ts.map +0 -1
- package/dist/routers/school.js +0 -481
- 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
|
-
|
|
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
|
-
|
|
66
|
+
preflightContinue: false, // Important: stop further handling of OPTIONS
|
|
67
|
+
optionsSuccessStatus: 204, // Recommended for modern browsers
|
|
68
|
+
|
|
34
69
|
}));
|
|
35
70
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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:
|
|
426
|
-
PORT:
|
|
427
|
-
NEXT_PUBLIC_APP_URL:
|
|
428
|
-
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
|
-
|
|
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>;
|
package/src/lib/fileUpload.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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:
|
|
207
|
-
uploadProgress:
|
|
252
|
+
uploadStatus: actualUploadSuccess ? 'COMPLETED' : 'FAILED',
|
|
253
|
+
uploadProgress: actualUploadSuccess ? 100 : 0,
|
|
208
254
|
};
|
|
209
255
|
|
|
210
|
-
if (!
|
|
211
|
-
updateData.uploadError =
|
|
256
|
+
if (!actualUploadSuccess && actualErrorMessage) {
|
|
257
|
+
updateData.uploadError = actualErrorMessage;
|
|
212
258
|
updateData.uploadRetryCount = { increment: 1 };
|
|
213
259
|
}
|
|
214
260
|
|
|
215
|
-
if (
|
|
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
|
-
|
|
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:
|
|
302
|
+
uploadProgress: clamped
|
|
247
303
|
}
|
|
248
304
|
});
|
|
249
305
|
} catch (error) {
|
|
250
|
-
|
|
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
|
-
|
|
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',
|