@studious-lms/server 1.2.45 → 1.2.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +45 -0
- package/.env.test.example +37 -0
- package/README.md +34 -7
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +12110 -0
- package/coverage/coverage-final.json +44 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +221 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/server/index.html +116 -0
- package/coverage/server/src/exportType.ts.html +109 -0
- package/coverage/server/src/index.html +161 -0
- package/coverage/server/src/index.ts.html +1702 -0
- package/coverage/server/src/instrument.ts.html +130 -0
- package/coverage/server/src/lib/config/env.ts.html +448 -0
- package/coverage/server/src/lib/config/index.html +116 -0
- package/coverage/server/src/lib/fileUpload.ts.html +1138 -0
- package/coverage/server/src/lib/googleCloudStorage.ts.html +334 -0
- package/coverage/server/src/lib/index.html +206 -0
- package/coverage/server/src/lib/jsonConversion.ts.html +2323 -0
- package/coverage/server/src/lib/jsonStyles.ts.html +193 -0
- package/coverage/server/src/lib/notificationHandler.ts.html +193 -0
- package/coverage/server/src/lib/pusher.ts.html +121 -0
- package/coverage/server/src/lib/thumbnailGenerator.ts.html +592 -0
- package/coverage/server/src/middleware/auth.ts.html +646 -0
- package/coverage/server/src/middleware/index.html +146 -0
- package/coverage/server/src/middleware/logging.ts.html +244 -0
- package/coverage/server/src/middleware/security.ts.html +271 -0
- package/coverage/server/src/routers/_app.ts.html +232 -0
- package/coverage/server/src/routers/agenda.ts.html +319 -0
- package/coverage/server/src/routers/announcement.ts.html +3481 -0
- package/coverage/server/src/routers/assignment.ts.html +7633 -0
- package/coverage/server/src/routers/attendance.ts.html +1030 -0
- package/coverage/server/src/routers/auth.ts.html +1081 -0
- package/coverage/server/src/routers/class.ts.html +3535 -0
- package/coverage/server/src/routers/comment.ts.html +991 -0
- package/coverage/server/src/routers/conversation.ts.html +982 -0
- package/coverage/server/src/routers/event.ts.html +1609 -0
- package/coverage/server/src/routers/file.ts.html +1144 -0
- package/coverage/server/src/routers/folder.ts.html +2797 -0
- package/coverage/server/src/routers/index.html +386 -0
- package/coverage/server/src/routers/labChat.ts.html +3073 -0
- package/coverage/server/src/routers/marketing.ts.html +340 -0
- package/coverage/server/src/routers/message.ts.html +1912 -0
- package/coverage/server/src/routers/notifications.ts.html +364 -0
- package/coverage/server/src/routers/section.ts.html +1120 -0
- package/coverage/server/src/routers/user.ts.html +862 -0
- package/coverage/server/src/routers/worksheet.ts.html +1729 -0
- package/coverage/server/src/trpc.ts.html +397 -0
- package/coverage/server/src/types/index.html +116 -0
- package/coverage/server/src/types/trpc.ts.html +127 -0
- package/coverage/server/src/utils/aiUser.ts.html +280 -0
- package/coverage/server/src/utils/email.ts.html +121 -0
- package/coverage/server/src/utils/generateInviteCode.ts.html +106 -0
- package/coverage/server/src/utils/index.html +206 -0
- package/coverage/server/src/utils/inference.ts.html +709 -0
- package/coverage/server/src/utils/logger.ts.html +664 -0
- package/coverage/server/src/utils/prismaErrorHandler.ts.html +907 -0
- package/coverage/server/src/utils/prismaWrapper.ts.html +355 -0
- package/coverage/server/vitest.config.ts.html +196 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +83 -52
- package/dist/index.js.map +1 -1
- package/dist/instrument.js +15 -8
- package/dist/instrument.js.map +1 -1
- package/dist/lib/config/env.d.ts +169 -0
- package/dist/lib/config/env.d.ts.map +1 -0
- package/dist/lib/config/env.js +115 -0
- package/dist/lib/config/env.js.map +1 -0
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +5 -4
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +7 -8
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/jsonConversion.d.ts.map +1 -1
- package/dist/lib/jsonConversion.js +14 -16
- package/dist/lib/jsonConversion.js.map +1 -1
- package/dist/lib/notificationHandler.d.ts +2 -2
- package/dist/lib/prisma.d.ts +2 -2
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +22 -3
- package/dist/lib/prisma.js.map +1 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +8 -7
- package/dist/lib/pusher.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +6 -5
- package/dist/middleware/auth.js.map +1 -1
- 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/routers/_app.d.ts +294 -98
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +12 -9
- package/dist/routers/agenda.js.map +1 -1
- package/dist/routers/announcement.d.ts +8 -0
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +6 -4
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +7 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +35 -18
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +1 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +4 -4
- package/dist/routers/attendance.js.map +1 -1
- package/dist/routers/auth.d.ts +20 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +132 -15
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +10 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +49 -5
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/comment.d.ts +2 -0
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/conversation.d.ts +1 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +46 -31
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +30 -7
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/labChat.d.ts +1 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +2 -3
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/marketing.d.ts +1 -1
- package/dist/routers/newtonChat.d.ts +55 -0
- package/dist/routers/newtonChat.d.ts.map +1 -0
- package/dist/routers/newtonChat.js +438 -0
- package/dist/routers/newtonChat.js.map +1 -0
- package/dist/routers/notifications.d.ts +4 -4
- package/dist/routers/section.d.ts +9 -4
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +8 -8
- package/dist/routers/section.js.map +1 -1
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +5 -4
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +30 -36
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +11 -33
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/seedDatabase.d.ts +1 -1
- package/dist/seedDatabase.js +275 -284
- package/dist/seedDatabase.js.map +1 -1
- package/dist/server/pipelines/aiLabChat.d.ts +10 -0
- package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiLabChat.js +83 -0
- package/dist/server/pipelines/aiLabChat.js.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts +2 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.js +138 -0
- package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +2 -2
- package/dist/trpc.js.map +1 -1
- package/dist/utils/email.d.ts +9 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +20 -5
- package/dist/utils/email.js.map +1 -1
- package/dist/utils/inference.d.ts +3 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +41 -7
- package/dist/utils/inference.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +3 -3
- package/dist/utils/logger.js.map +1 -1
- package/docker-compose.yml +14 -0
- package/package.json +13 -4
- package/prisma/schema.prisma +32 -5
- package/scripts/test-pre-push.ts +14 -0
- package/src/index.ts +98 -54
- package/src/instrument.ts +13 -6
- package/src/lib/config/env.ts +126 -0
- package/src/lib/fileUpload.ts +3 -2
- package/src/lib/googleCloudStorage.ts +6 -6
- package/src/lib/jsonConversion.ts +12 -14
- package/src/lib/prisma.ts +23 -2
- package/src/lib/pusher.ts +6 -5
- package/src/middleware/auth.ts +4 -3
- package/src/middleware/security.ts +80 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/agenda.ts +10 -7
- package/src/routers/announcement.ts +4 -2
- package/src/routers/assignment.ts +58 -40
- package/src/routers/attendance.ts +2 -2
- package/src/routers/auth.ts +143 -14
- package/src/routers/class.ts +52 -3
- package/src/routers/conversation.ts +49 -29
- package/src/routers/file.ts +29 -5
- package/src/routers/labChat.ts +0 -1
- package/src/routers/newtonChat.ts +520 -0
- package/src/routers/section.ts +6 -6
- package/src/routers/user.ts +3 -2
- package/src/routers/worksheet.ts +9 -37
- package/src/seedDatabase.ts +290 -283
- package/src/server/pipelines/aiLabChat.ts +92 -0
- package/src/server/pipelines/gradeWorksheet.ts +152 -0
- package/src/trpc.ts +2 -0
- package/src/utils/email.ts +30 -3
- package/src/utils/inference.ts +50 -5
- package/src/utils/logger.ts +2 -1
- package/tests/announcement.test.ts +164 -0
- package/tests/assignment.test.ts +296 -0
- package/tests/attendance.test.ts +168 -0
- package/tests/auth.test.ts +33 -10
- package/tests/class.test.ts +34 -9
- package/tests/event.test.ts +228 -0
- package/tests/section.test.ts +216 -0
- package/tests/setup.ts +70 -16
- package/tests/user.test.ts +158 -0
- package/vitest.config.ts +26 -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 -483
package/dist/utils/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sources":["utils/logger.ts"],"sourceRoot":"/","sourcesContent":["
|
|
1
|
+
{"version":3,"file":"logger.js","sources":["utils/logger.ts"],"sourceRoot":"/","sourcesContent":["\nexport enum LogLevel {\n INFO = 'info',\n WARN = 'warn',\n ERROR = 'error',\n DEBUG = 'debug'\n}\n\ntype LogMode = 'silent' | 'minimal' | 'normal' | 'verbose';\n\ninterface LogMessage {\n level: LogLevel;\n message: string;\n timestamp: string;\n context?: Record<string, any>;\n}\n\n// ANSI color codes\nconst colors = {\n reset: '\\x1b[0m',\n bright: '\\x1b[1m',\n dim: '\\x1b[2m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n blue: '\\x1b[34m',\n magenta: '\\x1b[35m',\n cyan: '\\x1b[36m',\n white: '\\x1b[37m',\n gray: '\\x1b[90m',\n // Background colors\n bgRed: '\\x1b[41m',\n bgGreen: '\\x1b[42m',\n bgYellow: '\\x1b[43m',\n bgBlue: '\\x1b[44m',\n bgMagenta: '\\x1b[45m',\n bgCyan: '\\x1b[46m',\n bgWhite: '\\x1b[47m',\n bgGray: '\\x1b[100m',\n // Bright background colors\n bgBrightRed: '\\x1b[101m',\n bgBrightGreen: '\\x1b[102m',\n bgBrightYellow: '\\x1b[103m',\n bgBrightBlue: '\\x1b[104m',\n bgBrightMagenta: '\\x1b[105m',\n bgBrightCyan: '\\x1b[106m',\n bgBrightWhite: '\\x1b[107m'\n};\n\nclass Logger {\n private static instance: Logger;\n private isDevelopment: boolean;\n private mode: LogMode;\n private levelColors: Record<LogLevel, string>;\n private levelBgColors: Record<LogLevel, string>;\n private levelEmojis: Record<LogLevel, string>;\n\n private constructor() {\n\n // this.isDevelopment = process.env.NODE_ENV === 'development';\n this.isDevelopment = true;\n\n this.mode = (process.env.NODE_ENV === 'test' ? 'silent' : ((process.env.LOG_MODE as LogMode) || 'normal'));\n \n\n this.levelColors = {\n [LogLevel.INFO]: colors.blue,\n [LogLevel.WARN]: colors.yellow,\n [LogLevel.ERROR]: colors.red,\n [LogLevel.DEBUG]: colors.magenta\n };\n\n this.levelBgColors = {\n [LogLevel.INFO]: colors.bgBlue,\n [LogLevel.WARN]: colors.bgYellow,\n [LogLevel.ERROR]: colors.bgRed,\n [LogLevel.DEBUG]: colors.bgMagenta\n };\n\n this.levelEmojis = {\n [LogLevel.INFO]: 'ℹ️',\n [LogLevel.WARN]: '⚠️',\n [LogLevel.ERROR]: '❌',\n [LogLevel.DEBUG]: '🔍'\n };\n }\n\n public static getInstance(): Logger {\n if (!Logger.instance) {\n Logger.instance = new Logger();\n }\n return Logger.instance;\n }\n\n public setMode(mode: LogMode) {\n this.mode = mode;\n }\n\n private shouldLog(level: LogLevel): boolean {\n const silent = [\n LogLevel.ERROR,\n ];\n\n const minimal = [\n LogLevel.ERROR,\n LogLevel.WARN,\n ];\n\n const normal = [\n LogLevel.ERROR,\n LogLevel.WARN,\n LogLevel.INFO,\n ];\n\n if (this.mode === 'silent') return silent.includes(level);\n if (this.mode === 'minimal') return minimal.includes(level);\n if (this.mode === 'normal') return normal.includes(level);\n return true; // verbose mode\n }\n\n private formatMessage(logMessage: LogMessage): string {\n const { level, message, timestamp, context } = logMessage;\n const color = this.levelColors[level];\n const bgColor = this.levelBgColors[level];\n const emoji = this.levelEmojis[level];\n \n const timestampStr = colors.gray + `[${timestamp}]` + colors.reset;\n // Use background color for level badge like Vitest\n const levelStr = colors.white + bgColor + ` ${level.toUpperCase()} ` + colors.reset;\n const emojiStr = emoji + ' ';\n const messageStr = colors.bright + message + colors.reset;\n \n const contextStr = context \n ? '\\n' + colors.dim + 'Context: ' + JSON.stringify(context, null, 2) + colors.reset\n : '';\n\n return `${timestampStr} ${levelStr} ${emojiStr}${messageStr}${contextStr}`;\n }\n\n private log(level: LogLevel, message: string, context?: Record<string, any>) {\n if (!this.shouldLog(level)) \n return; \n\n if (level == LogLevel.WARN || level == LogLevel.ERROR) {\n if (level == LogLevel.ERROR) {\n // alert me\n }\n // store in database\n }\n \n const logMessage: LogMessage = {\n level,\n message,\n timestamp: new Date().toISOString(),\n context\n };\n\n const formattedMessage = this.formatMessage(logMessage);\n\n switch (level) {\n case LogLevel.ERROR:\n console.error(formattedMessage);\n break;\n case LogLevel.WARN:\n console.warn(formattedMessage);\n break;\n case LogLevel.DEBUG:\n if (this.isDevelopment) {\n console.debug(formattedMessage);\n }\n break;\n default:\n console.log(formattedMessage);\n }\n }\n\n public info(message: string, context?: Record<string, any>) {\n this.log(LogLevel.INFO, message, context);\n }\n\n public warn(message: string, context?: Record<string, any>) {\n this.log(LogLevel.WARN, message, context);\n }\n\n public error(message: string, context?: Record<string, any>) {\n this.log(LogLevel.ERROR, message, context);\n }\n\n public debug(message: string, context?: Record<string, any>) {\n this.log(LogLevel.DEBUG, message, context);\n }\n}\n\nexport const logger = Logger.getInstance(); "],"names":[],"mappings":"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","debug_id":"3ad19480-6fd4-5009-abf4-d8f8caeb9a0e"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studious-lms/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.46",
|
|
4
4
|
"description": "Backend server for Studious application",
|
|
5
5
|
"main": "dist/exportType.js",
|
|
6
6
|
"types": "dist/exportType.d.ts",
|
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
18
|
"generate": "npx prisma generate",
|
|
19
19
|
"prepublishOnly": "npm run generate && npm run build",
|
|
20
|
-
"test": "
|
|
21
|
-
"
|
|
20
|
+
"test:pre": "NODE_ENV=test tsx scripts/test-pre-push.ts",
|
|
21
|
+
"test": "npm run test:pre && NODE_ENV=test vitest",
|
|
22
|
+
"test:run": "NODE_ENV=test vitest run",
|
|
23
|
+
"test:watch": "NODE_ENV=test vitest watch",
|
|
24
|
+
"test:coverage": "NODE_ENV=test vitest run --coverage",
|
|
25
|
+
"db:seed": "tsx src/seedDatabase.ts",
|
|
22
26
|
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org studious-fp --project server ./dist && sentry-cli sourcemaps upload --org studious-fp --project server ./dist"
|
|
23
27
|
},
|
|
24
28
|
"dependencies": {
|
|
@@ -28,11 +32,15 @@
|
|
|
28
32
|
"@sentry/cli": "^2.58.2",
|
|
29
33
|
"@sentry/node": "^10.26.0",
|
|
30
34
|
"@trpc/server": "^11.4.3",
|
|
35
|
+
"@unkey/ratelimit": "^2.1.3",
|
|
31
36
|
"bcryptjs": "^3.0.2",
|
|
37
|
+
"compression": "^1.8.1",
|
|
32
38
|
"cors": "^2.8.5",
|
|
33
39
|
"date-fns": "^4.1.0",
|
|
34
40
|
"dotenv": "^16.5.0",
|
|
35
41
|
"express": "^4.18.3",
|
|
42
|
+
"express-rate-limit": "^8.2.1",
|
|
43
|
+
"helmet": "^8.1.0",
|
|
36
44
|
"nodemailer": "^7.0.4",
|
|
37
45
|
"openai": "^5.23.0",
|
|
38
46
|
"pdf-lib": "^1.17.1",
|
|
@@ -41,17 +49,18 @@
|
|
|
41
49
|
"sharp": "^0.34.2",
|
|
42
50
|
"socket.io": "^4.8.1",
|
|
43
51
|
"superjson": "^2.2.2",
|
|
44
|
-
"trpc-panel": "^1.3.4",
|
|
45
52
|
"uuid": "^11.1.0",
|
|
46
53
|
"zod": "^3.22.4"
|
|
47
54
|
},
|
|
48
55
|
"devDependencies": {
|
|
56
|
+
"@types/compression": "^1.8.1",
|
|
49
57
|
"@types/cors": "^2.8.17",
|
|
50
58
|
"@types/express": "^4.17.21",
|
|
51
59
|
"@types/node": "^20.11.24",
|
|
52
60
|
"@types/nodemailer": "^6.4.17",
|
|
53
61
|
"@types/supertest": "^6.0.3",
|
|
54
62
|
"@types/uuid": "^10.0.0",
|
|
63
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
55
64
|
"supertest": "^7.1.4",
|
|
56
65
|
"trpc-ui": "^1.0.15",
|
|
57
66
|
"ts-node": "^10.9.2",
|
package/prisma/schema.prisma
CHANGED
|
@@ -9,6 +9,11 @@ generator client {
|
|
|
9
9
|
// output = "../generated/prisma"
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// @todo: add type safety to json fields.
|
|
13
|
+
// generator json {
|
|
14
|
+
// provider = "prisma-json-types-generator"
|
|
15
|
+
// }
|
|
16
|
+
|
|
12
17
|
datasource db {
|
|
13
18
|
provider = "postgresql"
|
|
14
19
|
url = env("DATABASE_URL")
|
|
@@ -42,6 +47,15 @@ enum UploadStatus {
|
|
|
42
47
|
CANCELLED
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
enum GenerationStatus {
|
|
51
|
+
NOT_STARTED
|
|
52
|
+
PENDING
|
|
53
|
+
GENERATING
|
|
54
|
+
COMPLETED
|
|
55
|
+
FAILED
|
|
56
|
+
CANCELLED
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
enum WorksheetQuestionType {
|
|
46
60
|
MULTIPLE_CHOICE
|
|
47
61
|
TRUE_FALSE
|
|
@@ -305,6 +319,7 @@ model Comment {
|
|
|
305
319
|
announcementId String?
|
|
306
320
|
parentComment Comment? @relation("CommentReplies", fields: [parentCommentId], references: [id], onDelete: Cascade)
|
|
307
321
|
parentCommentId String?
|
|
322
|
+
status GenerationStatus?
|
|
308
323
|
replies Comment[] @relation("CommentReplies")
|
|
309
324
|
reactions Reaction[]
|
|
310
325
|
createdAt DateTime @default(now())
|
|
@@ -345,7 +360,6 @@ model Submission {
|
|
|
345
360
|
annotations File[] @relation("SubmissionAnnotations")
|
|
346
361
|
worksheetResponses StudentWorksheetResponse[]
|
|
347
362
|
extendedResponse String?
|
|
348
|
-
|
|
349
363
|
gradeReceived Int?
|
|
350
364
|
|
|
351
365
|
rubricState String?
|
|
@@ -354,6 +368,18 @@ model Submission {
|
|
|
354
368
|
submittedAt DateTime?
|
|
355
369
|
submitted Boolean? @default(false)
|
|
356
370
|
returned Boolean? @default(false)
|
|
371
|
+
newtonTutors NewtonChat[]
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
model NewtonChat {
|
|
375
|
+
id String @id @default(uuid())
|
|
376
|
+
submissionId String
|
|
377
|
+
submission Submission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
|
|
378
|
+
conversationId String @unique
|
|
379
|
+
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
380
|
+
createdAt DateTime @default(now())
|
|
381
|
+
updatedAt DateTime @updatedAt
|
|
382
|
+
title String @default("Session with Newton Tutor")
|
|
357
383
|
}
|
|
358
384
|
|
|
359
385
|
model Section {
|
|
@@ -441,6 +467,7 @@ model Conversation {
|
|
|
441
467
|
members ConversationMember[]
|
|
442
468
|
messages Message[]
|
|
443
469
|
labChat LabChat?
|
|
470
|
+
newtonChat NewtonChat?
|
|
444
471
|
}
|
|
445
472
|
|
|
446
473
|
model LabChat {
|
|
@@ -451,8 +478,7 @@ model LabChat {
|
|
|
451
478
|
conversationId String @unique
|
|
452
479
|
createdById String // Teacher who created the lab
|
|
453
480
|
createdAt DateTime @default(now())
|
|
454
|
-
updatedAt DateTime @updatedAt
|
|
455
|
-
|
|
481
|
+
updatedAt DateTime @updatedAt
|
|
456
482
|
class Class @relation("ClassLabChats", fields: [classId], references: [id], onDelete: Cascade)
|
|
457
483
|
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
458
484
|
createdBy User @relation("CreatedLabChats", fields: [createdById], references: [id], onDelete: NoAction)
|
|
@@ -479,7 +505,7 @@ model Message {
|
|
|
479
505
|
senderId String
|
|
480
506
|
conversationId String
|
|
481
507
|
createdAt DateTime @default(now())
|
|
482
|
-
|
|
508
|
+
status GenerationStatus? @default(NOT_STARTED)
|
|
483
509
|
sender User @relation("SentMessages", fields: [senderId], references: [id], onDelete: Cascade)
|
|
484
510
|
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
485
511
|
mentions Mention[]
|
|
@@ -557,6 +583,7 @@ model StudentQuestionProgress {
|
|
|
557
583
|
updatedAt DateTime? @updatedAt
|
|
558
584
|
studentWorksheetResponseId String?
|
|
559
585
|
studentWorksheetResponse StudentWorksheetResponse? @relation(fields: [studentWorksheetResponseId], references: [id], onDelete: Cascade)
|
|
586
|
+
status GenerationStatus? @default(NOT_STARTED)
|
|
560
587
|
|
|
561
588
|
@@index([studentId, questionId])
|
|
562
589
|
}
|
|
@@ -582,7 +609,7 @@ model SchoolDevelopementProgram {
|
|
|
582
609
|
submittedAt DateTime? @default(now())
|
|
583
610
|
reviewedAt DateTime?
|
|
584
611
|
|
|
585
|
-
status String @default("
|
|
612
|
+
status String @default("NOT_STARTED") // NOT_STARTED, PENDING, APPROVED, REJECTED, REFERRED
|
|
586
613
|
|
|
587
614
|
supportingDocumentation File[] @relation("SchoolDevelopementProgramSupportingDocumentation")
|
|
588
615
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { logger } from '../src/utils/logger';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
|
|
6
|
+
dotenv.config({ path: resolve(process.cwd(), '.env.test') });
|
|
7
|
+
|
|
8
|
+
logger.info("Syncing Prisma schema to test database...");
|
|
9
|
+
|
|
10
|
+
execSync("npx prisma db push --force-reset --skip-generate", {
|
|
11
|
+
stdio: 'inherit',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
logger.info("Test database is ready!");
|
package/src/index.ts
CHANGED
|
@@ -11,58 +11,57 @@ import { logger } from './utils/logger.js';
|
|
|
11
11
|
import { setupSocketHandlers } from './socket/handlers.js';
|
|
12
12
|
import { bucket } from './lib/googleCloudStorage.js';
|
|
13
13
|
import { prisma } from './lib/prisma.js';
|
|
14
|
+
|
|
15
|
+
import { authLimiter, generalLimiter, helmetConfig, uploadLimiter } from './middleware/security.js';
|
|
16
|
+
|
|
14
17
|
import * as Sentry from "@sentry/node";
|
|
18
|
+
import { env } from './lib/config/env.js';
|
|
19
|
+
import compression from 'compression';
|
|
20
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
15
21
|
|
|
16
|
-
import { renderTrpcPanel } from "trpc-panel";
|
|
17
22
|
|
|
18
23
|
import "./instrument.js";
|
|
19
|
-
|
|
20
|
-
dotenv.config();
|
|
24
|
+
import { openAIClient } from './utils/inference.js';
|
|
21
25
|
|
|
22
26
|
const app = express();
|
|
23
27
|
|
|
28
|
+
app.use(helmetConfig);
|
|
29
|
+
app.use(compression());
|
|
30
|
+
|
|
31
|
+
app.use((req, res, next) => {
|
|
32
|
+
const requestId = uuidv4();
|
|
33
|
+
res.setHeader('X-Request-ID', requestId);
|
|
34
|
+
next();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.use(generalLimiter);
|
|
38
|
+
|
|
39
|
+
const allowedOrigins = env.NODE_ENV === 'production'
|
|
40
|
+
? [
|
|
41
|
+
'https://www.studious.sh',
|
|
42
|
+
'https://studious.sh',
|
|
43
|
+
env.NEXT_PUBLIC_APP_URL,
|
|
44
|
+
'http://localhost:3000',
|
|
45
|
+
|
|
46
|
+
].filter(Boolean)
|
|
47
|
+
: [
|
|
48
|
+
'http://localhost:3000',
|
|
49
|
+
'http://localhost:3001',
|
|
50
|
+
'http://127.0.0.1:3000',
|
|
51
|
+
'http://127.0.0.1:3001',
|
|
52
|
+
|
|
53
|
+
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
54
|
+
];
|
|
55
|
+
|
|
24
56
|
// CORS middleware
|
|
25
57
|
app.use(cors({
|
|
26
|
-
origin:
|
|
27
|
-
'http://localhost:3000', // Frontend development server
|
|
28
|
-
'http://localhost:3001', // Server port
|
|
29
|
-
'http://127.0.0.1:3000', // Alternative localhost
|
|
30
|
-
'http://127.0.0.1:3001', // Alternative localhost
|
|
31
|
-
'https://www.studious.sh', // Production frontend
|
|
32
|
-
'https://studious.sh', // Production frontend (without www)
|
|
33
|
-
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
34
|
-
],
|
|
58
|
+
origin: allowedOrigins,
|
|
35
59
|
credentials: true,
|
|
36
60
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
37
61
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-user'],
|
|
38
62
|
optionsSuccessStatus: 200
|
|
39
63
|
}));
|
|
40
64
|
|
|
41
|
-
// Handle preflight OPTIONS requests
|
|
42
|
-
app.options('*', (req, res) => {
|
|
43
|
-
const allowedOrigins = [
|
|
44
|
-
'http://localhost:3000',
|
|
45
|
-
'http://localhost:3001',
|
|
46
|
-
'http://127.0.0.1:3000',
|
|
47
|
-
'http://127.0.0.1:3001',
|
|
48
|
-
'https://www.studious.sh', // Production frontend
|
|
49
|
-
'https://studious.sh', // Production frontend (without www)
|
|
50
|
-
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
const origin = req.headers.origin;
|
|
54
|
-
if (origin && allowedOrigins.includes(origin)) {
|
|
55
|
-
res.header('Access-Control-Allow-Origin', origin);
|
|
56
|
-
} else {
|
|
57
|
-
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
61
|
-
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, x-user');
|
|
62
|
-
res.header('Access-Control-Allow-Credentials', 'true');
|
|
63
|
-
res.sendStatus(200);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
65
|
// CORS debugging middleware
|
|
67
66
|
app.use((req, res, next) => {
|
|
68
67
|
if (req.method === 'OPTIONS' || req.path.includes('trpc')) {
|
|
@@ -92,7 +91,7 @@ app.use((req, res, next) => {
|
|
|
92
91
|
});
|
|
93
92
|
|
|
94
93
|
app.use("/panel", async (_, res) => {
|
|
95
|
-
if (
|
|
94
|
+
if (env.NODE_ENV !== "development") {
|
|
96
95
|
return res.status(404).send("Not Found");
|
|
97
96
|
}
|
|
98
97
|
|
|
@@ -115,12 +114,25 @@ app.use("/panel", async (_, res) => {
|
|
|
115
114
|
// Create HTTP server
|
|
116
115
|
const httpServer = createServer(app);
|
|
117
116
|
|
|
118
|
-
app.get('/health', (req, res) => {
|
|
119
|
-
res.status(200).json({ message: 'OK' });
|
|
120
|
-
});
|
|
117
|
+
app.get('/health', async (req, res) => {
|
|
121
118
|
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
try {
|
|
120
|
+
// Check database connectivity
|
|
121
|
+
await prisma.$queryRaw`SELECT 1`;
|
|
122
|
+
|
|
123
|
+
res.status(200).json({
|
|
124
|
+
status: 'OK',
|
|
125
|
+
timestamp: new Date().toISOString(),
|
|
126
|
+
uptime: process.uptime(),
|
|
127
|
+
database: 'connected'
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
res.status(503).json({
|
|
131
|
+
status: 'ERROR',
|
|
132
|
+
database: 'disconnected',
|
|
133
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
124
136
|
});
|
|
125
137
|
|
|
126
138
|
// Setup Socket.IO
|
|
@@ -133,7 +145,7 @@ const io = new Server(httpServer, {
|
|
|
133
145
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
134
146
|
'https://www.studious.sh', // Production frontend
|
|
135
147
|
'https://studious.sh', // Production frontend (without www)
|
|
136
|
-
|
|
148
|
+
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
137
149
|
],
|
|
138
150
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
139
151
|
credentials: true,
|
|
@@ -356,12 +368,15 @@ app.get('/api/files/:fileId', async (req, res) => {
|
|
|
356
368
|
}
|
|
357
369
|
});
|
|
358
370
|
|
|
371
|
+
app.use('/trpc/auth.login', authLimiter);
|
|
372
|
+
app.use('/trpc/auth.register', authLimiter);
|
|
373
|
+
|
|
359
374
|
// File upload endpoint for secure file uploads (supports both POST and PUT)
|
|
360
|
-
app.post('/api/upload/:filePath', async (req, res) => {
|
|
375
|
+
app.post('/api/upload/:filePath', uploadLimiter, async (req, res) => {
|
|
361
376
|
handleFileUpload(req, res);
|
|
362
377
|
});
|
|
363
378
|
|
|
364
|
-
app.put('/api/upload/:filePath', async (req, res) => {
|
|
379
|
+
app.put('/api/upload/:filePath', uploadLimiter, async (req, res) => {
|
|
365
380
|
handleFileUpload(req, res);
|
|
366
381
|
});
|
|
367
382
|
|
|
@@ -378,7 +393,7 @@ function handleFileUpload(req: any, res: any) {
|
|
|
378
393
|
'http://127.0.0.1:3001',
|
|
379
394
|
'https://www.studious.sh', // Production frontend
|
|
380
395
|
'https://studious.sh', // Production frontend (without www)
|
|
381
|
-
|
|
396
|
+
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
382
397
|
];
|
|
383
398
|
|
|
384
399
|
if (origin && allowedOrigins.includes(origin)) {
|
|
@@ -453,7 +468,7 @@ Sentry.setupExpressErrorHandler(app);
|
|
|
453
468
|
// });
|
|
454
469
|
|
|
455
470
|
|
|
456
|
-
const PORT =
|
|
471
|
+
const PORT = env.PORT || 3001;
|
|
457
472
|
|
|
458
473
|
httpServer.listen(PORT, () => {
|
|
459
474
|
logger.info(`Server running on port ${PORT}`, {
|
|
@@ -464,10 +479,10 @@ httpServer.listen(PORT, () => {
|
|
|
464
479
|
|
|
465
480
|
// log all env variables
|
|
466
481
|
logger.info('Configurations', {
|
|
467
|
-
NODE_ENV:
|
|
468
|
-
PORT:
|
|
469
|
-
NEXT_PUBLIC_APP_URL:
|
|
470
|
-
LOG_MODE:
|
|
482
|
+
NODE_ENV: env.NODE_ENV,
|
|
483
|
+
PORT: env.PORT,
|
|
484
|
+
NEXT_PUBLIC_APP_URL: env.NEXT_PUBLIC_APP_URL,
|
|
485
|
+
LOG_MODE: env.LOG_MODE,
|
|
471
486
|
});
|
|
472
487
|
|
|
473
488
|
// Log CORS configuration
|
|
@@ -477,6 +492,35 @@ logger.info('CORS Configuration', {
|
|
|
477
492
|
'http://localhost:3001',
|
|
478
493
|
'http://127.0.0.1:3000',
|
|
479
494
|
'http://127.0.0.1:3001',
|
|
480
|
-
|
|
495
|
+
env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
481
496
|
]
|
|
482
|
-
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const gracefulShutdown = (signal: string) => {
|
|
500
|
+
logger.info(`Received ${signal}, shutting down gracefully`);
|
|
501
|
+
|
|
502
|
+
httpServer.close(() => {
|
|
503
|
+
logger.info('HTTP server closed');
|
|
504
|
+
|
|
505
|
+
io.close(() => {
|
|
506
|
+
logger.info('Socket.IO server closed');
|
|
507
|
+
|
|
508
|
+
prisma.$disconnect().then(() => {
|
|
509
|
+
logger.info('Database connections closed');
|
|
510
|
+
process.exit(0);
|
|
511
|
+
}).catch((err) => {
|
|
512
|
+
logger.error('Error disconnecting from database', { error: err });
|
|
513
|
+
process.exit(1);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Force shutdown after 10 seconds
|
|
519
|
+
setTimeout(() => {
|
|
520
|
+
logger.error('Forced shutdown after timeout');
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}, 10000);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
526
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
package/src/instrument.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/node";
|
|
2
|
+
import { env } from "./lib/config/env.js";
|
|
2
3
|
|
|
3
|
-
Sentry
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
// Only initialize Sentry in non-test environments
|
|
5
|
+
if (env.NODE_ENV !== 'test') {
|
|
6
|
+
Sentry.init({
|
|
7
|
+
dsn: env.SENTRY_DSN,
|
|
8
|
+
environment: env.NODE_ENV || 'development',
|
|
9
|
+
// Setting this option to true will send default PII data to Sentry.
|
|
10
|
+
// For example, automatic IP address collection on events
|
|
11
|
+
sendDefaultPii: true,
|
|
12
|
+
// @todo: disable in test environment
|
|
13
|
+
enabled: true, // Explicitly disable in test environment
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
// Determine which env file to load based on NODE_ENV
|
|
7
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
8
|
+
const envFileMap: Record<string, string> = {
|
|
9
|
+
test: '.env.test',
|
|
10
|
+
development: '.env.development',
|
|
11
|
+
production: '.env.production',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Load the appropriate env file
|
|
15
|
+
const envFile = envFileMap[nodeEnv] || '.env';
|
|
16
|
+
const envPath = resolve(process.cwd(), envFile);
|
|
17
|
+
|
|
18
|
+
// Load environment variables from the correct file
|
|
19
|
+
// First load .env (base), then override with environment-specific file
|
|
20
|
+
dotenv.config(); // Load .env first (base config)
|
|
21
|
+
dotenv.config({ path: envPath, override: true }); // Override with env-specific
|
|
22
|
+
|
|
23
|
+
const isTest = nodeEnv === 'test';
|
|
24
|
+
const isProduction = nodeEnv === 'production';
|
|
25
|
+
|
|
26
|
+
// Base schema with required vars for all environments
|
|
27
|
+
const baseSchema = z.object({
|
|
28
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
29
|
+
PORT: z.string().transform(Number).default('3001'),
|
|
30
|
+
DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Production/development schema with all required vars
|
|
34
|
+
const fullSchema = baseSchema.extend({
|
|
35
|
+
NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
|
|
36
|
+
BACKEND_URL: z.string().url().default('http://localhost:3001'),
|
|
37
|
+
SENTRY_DSN: z.string().url().optional(),
|
|
38
|
+
EMAIL_HOST: z.string().min(1, 'EMAIL_HOST is required'),
|
|
39
|
+
EMAIL_USER: z.string().email('EMAIL_USER must be a valid email'),
|
|
40
|
+
EMAIL_PASS: z.string().min(1, 'EMAIL_PASS is required'),
|
|
41
|
+
EMAIL_DRY_RUN: z.string().optional().default('false'),
|
|
42
|
+
GOOGLE_CLOUD_PROJECT_ID: z.string().min(1, 'GOOGLE_CLOUD_PROJECT_ID is required'),
|
|
43
|
+
GOOGLE_CLOUD_CLIENT_EMAIL: z.string().email('GOOGLE_CLOUD_CLIENT_EMAIL must be a valid email'),
|
|
44
|
+
GOOGLE_CLOUD_PRIVATE_KEY: z.string().min(1, 'GOOGLE_CLOUD_PRIVATE_KEY is required'),
|
|
45
|
+
GOOGLE_CLOUD_BUCKET_NAME: z.string().min(1, 'GOOGLE_CLOUD_BUCKET_NAME is required'),
|
|
46
|
+
PUSHER_APP_ID: z.string().min(1, 'PUSHER_APP_ID is required'),
|
|
47
|
+
PUSHER_KEY: z.string().min(1, 'PUSHER_KEY is required'),
|
|
48
|
+
PUSHER_SECRET: z.string().min(1, 'PUSHER_SECRET is required'),
|
|
49
|
+
PUSHER_CLUSTER: z.string().min(1, 'PUSHER_CLUSTER is required'),
|
|
50
|
+
INFERENCE_API_KEY: z.string().optional(),
|
|
51
|
+
INFERENCE_API_BASE_URL: z.string().url().optional(),
|
|
52
|
+
OPENAI_API_KEY: z.string().optional(),
|
|
53
|
+
LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('normal'),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Test schema - only require what's needed for tests
|
|
57
|
+
const testSchema = baseSchema.extend({
|
|
58
|
+
NEXT_PUBLIC_APP_URL: z.string().url().optional().default('http://localhost:3000'),
|
|
59
|
+
BACKEND_URL: z.string().url().optional().default('http://localhost:3001'),
|
|
60
|
+
SENTRY_DSN: z.string().url().optional(),
|
|
61
|
+
EMAIL_HOST: z.string().optional().default('smtp.test.com'),
|
|
62
|
+
EMAIL_USER: z.string().email().optional().default('test@test.com'),
|
|
63
|
+
EMAIL_PASS: z.string().optional().default('test'),
|
|
64
|
+
EMAIL_DRY_RUN: z.string().optional().default('false'),
|
|
65
|
+
GOOGLE_CLOUD_PROJECT_ID: z.string().optional().default('test-project'),
|
|
66
|
+
GOOGLE_CLOUD_CLIENT_EMAIL: z.string().email().optional().default('test@test.iam.gserviceaccount.com'),
|
|
67
|
+
GOOGLE_CLOUD_PRIVATE_KEY: z.string().optional().default('test-key'),
|
|
68
|
+
GOOGLE_CLOUD_BUCKET_NAME: z.string().optional().default('test-bucket'),
|
|
69
|
+
PUSHER_APP_ID: z.string().optional().default('test-app-id'),
|
|
70
|
+
PUSHER_KEY: z.string().optional().default('test-key'),
|
|
71
|
+
PUSHER_SECRET: z.string().optional().default('test-secret'),
|
|
72
|
+
PUSHER_CLUSTER: z.string().optional().default('us2'),
|
|
73
|
+
INFERENCE_API_KEY: z.string().optional(),
|
|
74
|
+
OPENAI_API_KEY: z.string().optional(),
|
|
75
|
+
INFERENCE_API_BASE_URL: z.string().url().optional(),
|
|
76
|
+
LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('quiet'),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Use test schema in test mode, full schema otherwise
|
|
80
|
+
const envSchema = isTest ? testSchema : fullSchema;
|
|
81
|
+
|
|
82
|
+
// Validate environment variables
|
|
83
|
+
function validateEnv() {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = envSchema.parse(process.env);
|
|
86
|
+
|
|
87
|
+
// Only exit on validation failure in production
|
|
88
|
+
if (isProduction && !parsed.DATABASE_URL) {
|
|
89
|
+
logger.error('DATABASE_URL is required in production');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return parsed;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof z.ZodError) {
|
|
96
|
+
const missingVars = error.errors.map(err => ({
|
|
97
|
+
path: err.path.join('.'),
|
|
98
|
+
message: err.message,
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
logger.error('Environment variable validation failed', {
|
|
102
|
+
envFile,
|
|
103
|
+
missingVars,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Only exit in production - in test/dev, log warning but continue
|
|
107
|
+
if (isProduction) {
|
|
108
|
+
logger.error(`Please check your ${envFile} file and ensure all required variables are set.`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
} else {
|
|
111
|
+
logger.warn('Continuing with defaults - some features may not work correctly', {
|
|
112
|
+
envFile,
|
|
113
|
+
});
|
|
114
|
+
// Return parsed with defaults for non-production
|
|
115
|
+
return envSchema.parse({ ...process.env });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Export validated environment variables
|
|
123
|
+
export const env = validateEnv();
|
|
124
|
+
|
|
125
|
+
// Type-safe environment access
|
|
126
|
+
export type Env = z.infer<typeof envSchema>;
|
package/src/lib/fileUpload.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getSignedUrl, objectExists } from "./googleCloudStorage.js";
|
|
|
4
4
|
import { generateMediaThumbnail } from "./thumbnailGenerator.js";
|
|
5
5
|
import { prisma } from "./prisma.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { env } from "./config/env.js";
|
|
7
8
|
|
|
8
9
|
export interface FileData {
|
|
9
10
|
name: string;
|
|
@@ -136,7 +137,7 @@ export async function createDirectUploadFile(
|
|
|
136
137
|
const uploadSessionId = uuidv4();
|
|
137
138
|
|
|
138
139
|
// Generate backend proxy upload URL (not direct GCS)
|
|
139
|
-
const baseUrl =
|
|
140
|
+
const baseUrl = env.BACKEND_URL || 'http://localhost:3001';
|
|
140
141
|
const uploadUrl = `${baseUrl}/api/upload/${encodeURIComponent(filePath)}`;
|
|
141
142
|
const uploadExpiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes from now
|
|
142
143
|
|
|
@@ -231,7 +232,7 @@ export async function confirmDirectUpload(
|
|
|
231
232
|
// If uploadSuccess is true, verify the object actually exists in GCS
|
|
232
233
|
if (uploadSuccess) {
|
|
233
234
|
try {
|
|
234
|
-
const exists = await objectExists(
|
|
235
|
+
const exists = await objectExists(env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
|
|
235
236
|
if (!exists) {
|
|
236
237
|
actualUploadSuccess = false;
|
|
237
238
|
actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
dotenv.config();
|
|
1
|
+
|
|
3
2
|
import { Storage } from '@google-cloud/storage';
|
|
4
3
|
import { TRPCError } from '@trpc/server';
|
|
4
|
+
import { env } from './config/env.js';
|
|
5
5
|
|
|
6
6
|
const storage = new Storage({
|
|
7
|
-
projectId:
|
|
7
|
+
projectId: env.GOOGLE_CLOUD_PROJECT_ID,
|
|
8
8
|
credentials: {
|
|
9
|
-
client_email:
|
|
10
|
-
private_key:
|
|
9
|
+
client_email: env.GOOGLE_CLOUD_CLIENT_EMAIL,
|
|
10
|
+
private_key: env.GOOGLE_CLOUD_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
|
11
11
|
},
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
export const bucket = storage.bucket(
|
|
14
|
+
export const bucket = storage.bucket(env.GOOGLE_CLOUD_BUCKET_NAME!);
|
|
15
15
|
|
|
16
16
|
// Short expiration time for signed URLs (5 minutes)
|
|
17
17
|
const SIGNED_URL_EXPIRATION = 5 * 60 * 1000;
|