fix: harden launch server runtime and public config
This commit is contained in:
@@ -8,6 +8,12 @@ module.exports = {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
max_memory_restart: '512M',
|
||||
kill_timeout: 10000,
|
||||
max_restarts: 10,
|
||||
min_uptime: '10s',
|
||||
listen_timeout: 10000,
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
||||
merge_logs: true,
|
||||
wait_ready: false,
|
||||
}],
|
||||
};
|
||||
|
||||
Generated
+259
-19
@@ -17,6 +17,8 @@
|
||||
"express-rate-limit": "^8.4.1",
|
||||
"helmet": "^8.2.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mammoth": "^1.12.0",
|
||||
"nodemailer": "^8.0.10",
|
||||
"pg": "^8.13.0",
|
||||
"wechatpay-node-v3": "^2.2.1"
|
||||
}
|
||||
@@ -64,6 +66,15 @@
|
||||
"@noble/hashes": "^1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.13",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -97,6 +108,15 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@@ -115,6 +135,26 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||
@@ -130,10 +170,16 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
|
||||
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"version": "1.20.5",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
||||
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
@@ -144,7 +190,7 @@
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.14.0",
|
||||
"qs": "~6.15.1",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
@@ -302,6 +348,12 @@
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||
@@ -372,6 +424,12 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/dingbat-to-unicode": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
|
||||
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/dot-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||
@@ -394,6 +452,15 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/duck": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
|
||||
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
|
||||
"license": "BSD",
|
||||
"dependencies": {
|
||||
"underscore": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -493,14 +560,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.3",
|
||||
"body-parser": "~1.20.5",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
@@ -519,7 +586,7 @@
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.14.0",
|
||||
"qs": "~6.15.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
@@ -539,12 +606,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
|
||||
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.1.0"
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -794,6 +861,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -801,9 +874,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
@@ -827,6 +900,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
@@ -855,6 +934,18 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
@@ -876,6 +967,15 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -918,6 +1018,17 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lop": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
|
||||
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"duck": "^0.1.12",
|
||||
"option": "~0.2.1",
|
||||
"underscore": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lower-case": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
||||
@@ -927,6 +1038,30 @@
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/mammoth": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
|
||||
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.6",
|
||||
"argparse": "~1.0.3",
|
||||
"base64-js": "^1.5.1",
|
||||
"bluebird": "~3.4.0",
|
||||
"dingbat-to-unicode": "^1.0.1",
|
||||
"jszip": "^3.7.1",
|
||||
"lop": "^0.4.2",
|
||||
"path-is-absolute": "^1.0.0",
|
||||
"underscore": "^1.13.1",
|
||||
"xmlbuilder": "^10.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"mammoth": "bin/mammoth"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/map-obj": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
|
||||
@@ -1042,6 +1177,15 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.10",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.10.tgz",
|
||||
"integrity": "sha512-BLFuSth7QtHOkBzyqTehWWyub0NTRDuK2Q2SQfnGLsrJnzyU+Yeh4WpV1eZGuARFj1xQJHIdnTuJZLP+b9R1GQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -1084,6 +1228,18 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/option": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
|
||||
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -1093,6 +1249,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
@@ -1239,6 +1404,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -1253,9 +1424,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
@@ -1303,6 +1474,27 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -1398,6 +1590,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -1521,6 +1719,12 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sse-decoder": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sse-decoder/-/sse-decoder-1.0.0.tgz",
|
||||
@@ -1547,6 +1751,21 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "8.0.6",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz",
|
||||
@@ -1644,6 +1863,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.8",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
||||
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
|
||||
@@ -1704,6 +1929,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utility": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/utility/-/utility-2.5.0.tgz",
|
||||
@@ -1752,6 +1983,15 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"express-rate-limit": "^8.4.1",
|
||||
"helmet": "^8.2.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mammoth": "^1.12.0",
|
||||
"nodemailer": "^8.0.10",
|
||||
"pg": "^8.13.0",
|
||||
"wechatpay-node-v3": "^2.2.1"
|
||||
}
|
||||
|
||||
@@ -958,6 +958,7 @@ async function ensureSchema() {
|
||||
await runMigration("029_task_poll_heartbeat", migrateTaskPollHeartbeat);
|
||||
await runMigration("030_generation_tasks_user_status_index", migrateGenerationTasksUserStatusIndex);
|
||||
await runMigration("031_generation_tasks_billing_columns", migrateGenerationTasksBillingColumns);
|
||||
await runMigration("032_ecommerce_video_history", migrateEcommerceVideoHistorySchema);
|
||||
await ensureModelPriceSeed();
|
||||
}
|
||||
|
||||
@@ -1105,3 +1106,22 @@ module.exports = {
|
||||
hasColumn,
|
||||
addColumnIfMissing,
|
||||
};
|
||||
|
||||
async function migrateEcommerceVideoHistorySchema(client) {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS ecommerce_video_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(200) NOT NULL DEFAULT '',
|
||||
config_json TEXT NOT NULL DEFAULT '{}',
|
||||
plan_json TEXT NOT NULL DEFAULT '{}',
|
||||
scenes_json TEXT NOT NULL DEFAULT '[]',
|
||||
source_image_urls TEXT NOT NULL DEFAULT '[]',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'completed',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ecommerce_video_history_user
|
||||
ON ecommerce_video_history(user_id, created_at DESC);
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ const ENTERPRISE_VIDEO_ALLOWED_MODELS = new Set([
|
||||
"kling-3.0-dashscope",
|
||||
"kling-v3-omni-dashscope",
|
||||
"kling/kling-v3-omni-video-generation",
|
||||
"vidu-q3-turbo",
|
||||
"vidu-q3-turbo-t2v",
|
||||
"vidu-q3-turbo-i2v",
|
||||
"pixverse-c1",
|
||||
"pixverse-c1-t2v",
|
||||
"pixverse-c1-i2v",
|
||||
]);
|
||||
|
||||
function normalizeModel(value) {
|
||||
@@ -43,6 +49,8 @@ function isEnterpriseVideoModelAllowed(providerConfig, model) {
|
||||
if (protocol === "wan-animate-mix") return true;
|
||||
if (protocol === "wan-s2v") return true;
|
||||
if (protocol === "kling-dashscope") return true;
|
||||
if (protocol === "vidu") return true;
|
||||
if (protocol === "pixverse") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -78,6 +86,14 @@ function getEnterpriseVideoCreditRate(input) {
|
||||
return resolution === "720P" ? 0.9 : 1.2;
|
||||
}
|
||||
|
||||
if (model.includes("vidu")) {
|
||||
return resolution === "720P" ? 0.6 : 1.0;
|
||||
}
|
||||
|
||||
if (model.includes("pixverse")) {
|
||||
return resolution === "720P" ? 0.6 : 1.0;
|
||||
}
|
||||
|
||||
const error = new Error(`Unsupported enterprise video model: ${input.model || input.requestedModel}`);
|
||||
error.status = 403;
|
||||
error.code = "ENTERPRISE_VIDEO_MODEL_NOT_ALLOWED";
|
||||
@@ -194,6 +210,21 @@ async function markEnterpriseVideoCreditsAccepted(clientOrPool, creditLedgerId)
|
||||
return rowCount > 0;
|
||||
}
|
||||
|
||||
async function refundEnterpriseVideoCredits(clientOrPool, billing, reason) {
|
||||
if (!billing || !billing.creditLedgerId) return false;
|
||||
const { rowCount } = await clientOrPool.query(
|
||||
"UPDATE credit_ledger SET status = 'refunded', refund_reason = $1, updated_at = NOW() WHERE id = $2 AND status = 'reserved'",
|
||||
[reason || null, billing.creditLedgerId],
|
||||
);
|
||||
if (rowCount > 0 && billing.amountCents > 0 && billing.enterpriseId) {
|
||||
await clientOrPool.query(
|
||||
"UPDATE enterprises SET balance_cents = balance_cents + $1, updated_at = NOW() WHERE id = $2",
|
||||
[billing.amountCents, billing.enterpriseId],
|
||||
);
|
||||
}
|
||||
return rowCount > 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ENTERPRISE_VIDEO_ALLOWED_MODELS,
|
||||
assertEnterpriseVideoModelAllowed,
|
||||
@@ -206,5 +237,6 @@ module.exports = {
|
||||
normalizeEnterpriseVideoDuration,
|
||||
normalizeEnterpriseVideoResolution,
|
||||
prepareEnterpriseVideoBilling,
|
||||
refundEnterpriseVideoCredits,
|
||||
reserveEnterpriseVideoCredits,
|
||||
};
|
||||
|
||||
+39
-1
@@ -16,6 +16,7 @@ const routes = require('./routes')
|
||||
const PORT = Number(process.env.PORT) || 3600
|
||||
const HOST = process.env.HOST || '0.0.0.0'
|
||||
const IS_PRODUCTION = process.env.NODE_ENV === 'production'
|
||||
let server = null
|
||||
|
||||
// CORS: in production, require explicit allowlist; in dev, allow all with credentials
|
||||
function buildCorsOptions() {
|
||||
@@ -103,6 +104,7 @@ async function main() {
|
||||
|
||||
// Skip JSON body-parser for binary upload routes (busboy handles multipart parsing)
|
||||
app.use('/api/oss/upload-binary', (req, res, next) => { req._body = true; next(); })
|
||||
app.use("/api/files/extract-text", (req, res, next) => { req._body = true; next(); })
|
||||
// JSON body limit: 5MB globally (upload routes override locally)
|
||||
app.use('/api/oss/upload', express.json({ limit: '200mb' }))
|
||||
app.use(express.json({ limit: process.env.JSON_BODY_LIMIT || '5mb' }))
|
||||
@@ -145,7 +147,7 @@ async function main() {
|
||||
const { startStaleTaskCleanup } = require('./aiTaskWorker')
|
||||
startStaleTaskCleanup()
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
server = app.listen(PORT, HOST, () => {
|
||||
console.log(`OmniAI Key Server running at http://${HOST}:${PORT}`)
|
||||
console.log(`Health check: http://${HOST}:${PORT}/api/health`)
|
||||
})
|
||||
@@ -158,9 +160,45 @@ main().catch((err) => {
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('[fatal] Unhandled promise rejection:', reason)
|
||||
process.exitCode = 1
|
||||
setTimeout(() => process.exit(1), 5000).unref()
|
||||
})
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[fatal] Uncaught exception:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
|
||||
// ── Graceful shutdown ───────────────────────────────────────────────────
|
||||
let shuttingDown = false
|
||||
|
||||
function gracefulShutdown(signal) {
|
||||
if (shuttingDown) return
|
||||
shuttingDown = true
|
||||
console.log('[shutdown] Received ' + signal + ', draining connections...')
|
||||
|
||||
if (server && server.listening) {
|
||||
server.close(() => {
|
||||
console.log('[shutdown] Server closed, cleaning up...')
|
||||
const { stopProviderHealthMonitor } = require('./providerHealthMonitor')
|
||||
stopProviderHealthMonitor()
|
||||
const { pool } = require('./db')
|
||||
pool.end().then(() => {
|
||||
console.log('[shutdown] Database pool closed')
|
||||
process.exit(0)
|
||||
}).catch(() => process.exit(0))
|
||||
})
|
||||
|
||||
// Force exit after timeout
|
||||
setTimeout(() => {
|
||||
console.error('[shutdown] Forced exit after timeout')
|
||||
process.exit(1)
|
||||
}, 15000).unref()
|
||||
} else {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
|
||||
|
||||
+21
-1
@@ -111,4 +111,24 @@ async function putObject(objectKey, body, contentType = "application/octet-strea
|
||||
return { ossKey: objectKey, url };
|
||||
}
|
||||
|
||||
module.exports = { getObject, putObject, isOssConfigured, createSignedReadUrl };
|
||||
module.exports = { getObject, putObject, deleteObject, isOssConfigured, createSignedReadUrl };
|
||||
|
||||
async function deleteObject(objectKey) {
|
||||
if (!isOssConfigured()) {
|
||||
throw new Error("OSS is not configured");
|
||||
}
|
||||
|
||||
const date = new Date().toUTCString();
|
||||
const authorization = signOssRequest("DELETE", objectKey, date);
|
||||
const url = `${getOssEndpoint()}/${encodeURIComponent(objectKey).replace(/%2F/g, "/")}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: { Date: date, Authorization: authorization },
|
||||
});
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
const text = await response.text().catch(() => "");
|
||||
throw new Error(`OSS DELETE failed (${response.status}): ${text.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -1495,7 +1495,7 @@ function registerAiRoutes(router) {
|
||||
res.flushHeaders();
|
||||
|
||||
const abortController = new AbortController();
|
||||
const streamTimer = setTimeout(() => abortController.abort(), 60000);
|
||||
const streamTimer = setTimeout(() => abortController.abort(), 120000);
|
||||
req.on("close", () => { clearTimeout(streamTimer); abortController.abort(); });
|
||||
|
||||
try {
|
||||
@@ -1554,7 +1554,7 @@ function registerAiRoutes(router) {
|
||||
}
|
||||
} else {
|
||||
const nonStreamAbort = new AbortController();
|
||||
const nonStreamTimer = setTimeout(() => nonStreamAbort.abort(), 60000);
|
||||
const nonStreamTimer = setTimeout(() => nonStreamAbort.abort(), 120000);
|
||||
const upstream = await fetch(url, { method: "POST", headers: reqHeaders, body: reqBody, signal: nonStreamAbort.signal });
|
||||
clearTimeout(nonStreamTimer);
|
||||
const text = await upstream.text().catch(() => "");
|
||||
@@ -1578,7 +1578,7 @@ function registerAiRoutes(router) {
|
||||
const fallbackHeaders = { "Content-Type": "application/json", Authorization: "Bearer " + slotResult.apiKey };
|
||||
const fallbackBody = JSON.stringify({ model: fallbackConfig.model, messages, stream: false, temperature: temperature || 0.7, max_tokens: 4096 });
|
||||
const fbAbort = new AbortController();
|
||||
const fbTimer = setTimeout(() => fbAbort.abort(), 60000);
|
||||
const fbTimer = setTimeout(() => fbAbort.abort(), 90000);
|
||||
const fbUpstream = await fetch(fallbackUrl, { method: "POST", headers: fallbackHeaders, body: fallbackBody, signal: fbAbort.signal });
|
||||
clearTimeout(fbTimer);
|
||||
const fbText = await fbUpstream.text().catch(() => "");
|
||||
@@ -1609,7 +1609,7 @@ function registerAiRoutes(router) {
|
||||
} catch (err) {
|
||||
releaseLease(slotResult);
|
||||
console.error("[ai/chat] error:", err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1690,7 +1690,7 @@ function registerAiRoutes(router) {
|
||||
|
||||
res.json({ taskId: String(rows[0].id), conversationId: rows[0].conversation_id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1714,7 +1714,7 @@ function registerAiRoutes(router) {
|
||||
|
||||
res.json(formatAiTaskRow(rows[0]));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1761,7 +1761,7 @@ function registerAiRoutes(router) {
|
||||
taskEvents.off(`task:${taskId}`, onUpdate);
|
||||
});
|
||||
} catch (err) {
|
||||
if (!res.headersSent) res.status(500).json({ error: err.message });
|
||||
if (!res.headersSent) res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1826,7 +1826,7 @@ function registerAiRoutes(router) {
|
||||
res.end(buffer);
|
||||
} catch (err) {
|
||||
console.error("[ai/tasks/download] failed:", err.message);
|
||||
if (!res.headersSent) res.status(500).json({ error: err.message });
|
||||
if (!res.headersSent) res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1854,7 +1854,7 @@ function registerAiRoutes(router) {
|
||||
res.end(buffer);
|
||||
} catch (err) {
|
||||
console.error("[ai/proxy-download] failed:", err.message);
|
||||
if (!res.headersSent) res.status(500).json({ error: err.message });
|
||||
if (!res.headersSent) res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@ const {
|
||||
buildOssPublicUrl,
|
||||
normalizeAvatarOssKey,
|
||||
normalizeProfileMediaUrl,
|
||||
EMAIL_PURPOSES,
|
||||
EMAIL_CODE_TTL_MINUTES,
|
||||
EMAIL_CODE_COOLDOWN_SECONDS,
|
||||
EMAIL_CODE_MAX_ATTEMPTS,
|
||||
hashEmailCode,
|
||||
sendEmailCode,
|
||||
consumeEmailCode,
|
||||
} = require("./context");
|
||||
const {
|
||||
checkBetaInviteCodeForRegistration,
|
||||
@@ -208,15 +215,20 @@ function registerAuthRoutes(router) {
|
||||
const email = normalizeEmail(req.body?.email);
|
||||
const usernameInput = String(req.body?.username || "").trim();
|
||||
const password = String(req.body?.password || "");
|
||||
const code = String(req.body?.code || "").trim();
|
||||
|
||||
const emailError = validateEmail(email);
|
||||
if (emailError) return res.status(400).json({ error: emailError });
|
||||
if (!code) return res.status(400).json({ error: "缺少验证码" });
|
||||
const passwordError = validatePassword(password);
|
||||
if (passwordError) return res.status(400).json({ error: passwordError });
|
||||
const registrationInvite = await ensureRegistrationInvite(req, res);
|
||||
if (!registrationInvite) return;
|
||||
|
||||
try {
|
||||
const verified = await consumeEmailCode(email, code, "register");
|
||||
if (!verified) return res.status(400).json({ error: "验证码错误或已过期" });
|
||||
|
||||
const { rows: existingEmail } = await pool.query(
|
||||
"SELECT id FROM users WHERE LOWER(email) = LOWER($1) LIMIT 1",
|
||||
[email],
|
||||
@@ -751,6 +763,140 @@ function registerAuthRoutes(router) {
|
||||
res.status(500).json({ error: "更新个人资料失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Email verification routes
|
||||
// ============================================================
|
||||
|
||||
router.post("/auth/email/send-code", async (req, res) => {
|
||||
const email = normalizeEmail(req.body?.email);
|
||||
const purpose = String(req.body?.purpose || "register");
|
||||
const emailError = validateEmail(email);
|
||||
if (emailError) return res.status(400).json({ error: emailError });
|
||||
if (!EMAIL_PURPOSES.has(purpose)) return res.status(400).json({ error: "验证码用途无效" });
|
||||
|
||||
if (purpose === "register") {
|
||||
const inviteOk = await ensureBetaInviteCode(req, res);
|
||||
if (!inviteOk) return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows: recentCodes } = await pool.query(
|
||||
"SELECT created_at FROM email_verification_codes WHERE email = $1 AND purpose = $2 AND created_at > NOW() - ($3::text || ' seconds')::interval ORDER BY created_at DESC LIMIT 1",
|
||||
[email, purpose, EMAIL_CODE_COOLDOWN_SECONDS]
|
||||
);
|
||||
if (recentCodes.length > 0) {
|
||||
return res.status(429).json({ error: "验证码发送太频繁,请 " + EMAIL_CODE_COOLDOWN_SECONDS + " 秒后再试" });
|
||||
}
|
||||
|
||||
if (purpose === "register") {
|
||||
const { rows: existing } = await pool.query("SELECT id FROM users WHERE LOWER(email) = LOWER($1) LIMIT 1", [email]);
|
||||
if (existing.length > 0) return res.status(409).json({ error: "该邮箱已注册" });
|
||||
}
|
||||
|
||||
if (purpose === "login" || purpose === "reset") {
|
||||
const { rows: existing } = await pool.query("SELECT id FROM users WHERE LOWER(email) = LOWER($1) AND enabled = 1 LIMIT 1", [email]);
|
||||
if (existing.length === 0) return res.status(404).json({ error: "该邮箱尚未注册" });
|
||||
}
|
||||
|
||||
const code = generateSmsCode();
|
||||
const codeHash = hashEmailCode(email, code);
|
||||
await pool.query(
|
||||
"INSERT INTO email_verification_codes (email, purpose, code_hash, expires_at) VALUES ($1, $2, $3, NOW() + ($4::text || ' minutes')::interval)",
|
||||
[email, purpose, codeHash, EMAIL_CODE_TTL_MINUTES]
|
||||
);
|
||||
|
||||
const sendResult = await sendEmailCode(email, code, purpose);
|
||||
res.json({
|
||||
success: true,
|
||||
provider: sendResult.provider,
|
||||
ttlSeconds: EMAIL_CODE_TTL_MINUTES * 60,
|
||||
cooldownSeconds: EMAIL_CODE_COOLDOWN_SECONDS,
|
||||
...(sendResult.devCode ? { devCode: sendResult.devCode } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[auth/email/send-code] failed", error);
|
||||
res.status(500).json({ error: "验证码发送失败" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/auth/email/verify", async (req, res) => {
|
||||
const email = normalizeEmail(req.body?.email);
|
||||
const code = String(req.body?.code || "").trim();
|
||||
const purpose = String(req.body?.purpose || "register");
|
||||
const emailError = validateEmail(email);
|
||||
if (emailError) return res.status(400).json({ error: emailError });
|
||||
if (!code) return res.status(400).json({ error: "缺少验证码" });
|
||||
if (!EMAIL_PURPOSES.has(purpose)) return res.status(400).json({ error: "验证码用途无效" });
|
||||
|
||||
try {
|
||||
const verified = await consumeEmailCode(email, code, purpose);
|
||||
if (!verified) return res.status(400).json({ error: "验证码错误或已过期" });
|
||||
if (purpose === "register" || purpose === "login") {
|
||||
await pool.query("UPDATE users SET email_verified = 1 WHERE LOWER(email) = LOWER($1)", [email]);
|
||||
}
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("[auth/email/verify] failed", error);
|
||||
res.status(500).json({ error: "验证失败" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/auth/forgot-password", async (req, res) => {
|
||||
const email = normalizeEmail(req.body?.email);
|
||||
const emailError = validateEmail(email);
|
||||
if (emailError) return res.status(400).json({ error: emailError });
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query("SELECT id FROM users WHERE LOWER(email) = LOWER($1) AND enabled = 1 LIMIT 1", [email]);
|
||||
if (rows.length === 0) {
|
||||
return res.json({ success: true, message: "如果该邮箱已注册,重置链接已发送" });
|
||||
}
|
||||
|
||||
const { rows: recentCodes } = await pool.query(
|
||||
"SELECT created_at FROM email_verification_codes WHERE email = $1 AND purpose = 'reset' AND created_at > NOW() - ($2::text || ' seconds')::interval ORDER BY created_at DESC LIMIT 1",
|
||||
[email, EMAIL_CODE_COOLDOWN_SECONDS]
|
||||
);
|
||||
if (recentCodes.length > 0) {
|
||||
return res.status(429).json({ error: "发送太频繁,请 " + EMAIL_CODE_COOLDOWN_SECONDS + " 秒后再试" });
|
||||
}
|
||||
|
||||
const code = generateSmsCode();
|
||||
const codeHash = hashEmailCode(email, code);
|
||||
await pool.query(
|
||||
"INSERT INTO email_verification_codes (email, purpose, code_hash, expires_at) VALUES ($1, 'reset', $2, NOW() + ($3::text || ' minutes')::interval)",
|
||||
[email, codeHash, EMAIL_CODE_TTL_MINUTES]
|
||||
);
|
||||
await sendEmailCode(email, code, "reset");
|
||||
res.json({ success: true, message: "重置验证码已发送到您的邮箱" });
|
||||
} catch (error) {
|
||||
console.error("[auth/forgot-password] failed", error);
|
||||
res.status(500).json({ error: "发送失败" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/auth/reset-password", async (req, res) => {
|
||||
const email = normalizeEmail(req.body?.email);
|
||||
const code = String(req.body?.code || "").trim();
|
||||
const newPassword = String(req.body?.newPassword || "");
|
||||
const emailError = validateEmail(email);
|
||||
if (emailError) return res.status(400).json({ error: emailError });
|
||||
if (!code) return res.status(400).json({ error: "缺少验证码" });
|
||||
const passwordError = validatePassword(newPassword);
|
||||
if (passwordError) return res.status(400).json({ error: passwordError });
|
||||
|
||||
try {
|
||||
const verified = await consumeEmailCode(email, code, "reset");
|
||||
if (!verified) return res.status(400).json({ error: "验证码错误或已过期" });
|
||||
const hash = await bcrypt.hash(newPassword, 10);
|
||||
await pool.query("UPDATE users SET password_hash = $1 WHERE LOWER(email) = LOWER($2)", [hash, email]);
|
||||
res.json({ success: true, message: "密码重置成功,请重新登录" });
|
||||
} catch (error) {
|
||||
console.error("[auth/reset-password] failed", error);
|
||||
res.status(500).json({ error: "密码重置失败" });
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -53,6 +53,10 @@ const SMS_PURPOSES = new Set(["register", "login"]);
|
||||
const SMS_CODE_TTL_MINUTES = Math.max(1, Number(process.env.SMS_CODE_TTL_MINUTES) || 10);
|
||||
const SMS_CODE_COOLDOWN_SECONDS = Math.max(10, Number(process.env.SMS_CODE_COOLDOWN_SECONDS) || 60);
|
||||
const SMS_CODE_MAX_ATTEMPTS = Math.max(1, Number(process.env.SMS_CODE_MAX_ATTEMPTS) || 5);
|
||||
const EMAIL_PURPOSES = new Set(["register", "login", "reset"]);
|
||||
const EMAIL_CODE_TTL_MINUTES = Math.max(1, Number(process.env.EMAIL_CODE_TTL_MINUTES) || 10);
|
||||
const EMAIL_CODE_COOLDOWN_SECONDS = Math.max(10, Number(process.env.EMAIL_CODE_COOLDOWN_SECONDS) || 60);
|
||||
const EMAIL_CODE_MAX_ATTEMPTS = Math.max(1, Number(process.env.EMAIL_CODE_MAX_ATTEMPTS) || 5);
|
||||
|
||||
function validateUsername(username) {
|
||||
if (!username) return "缺少用户名";
|
||||
@@ -201,6 +205,66 @@ async function consumeSmsCode(phone, code, purpose) {
|
||||
await pool.query("UPDATE sms_verification_codes SET consumed_at = NOW() WHERE id = $1", [row.id]);
|
||||
return true;
|
||||
}
|
||||
function hashEmailCode(email, code) {
|
||||
const secret = process.env.EMAIL_CODE_SECRET || process.env.JWT_SECRET || "omniai-dev-email-secret";
|
||||
return crypto.createHash("sha256").update(email + ":" + code + ":" + secret).digest("hex");
|
||||
}
|
||||
|
||||
async function sendEmailCode(email, code, purpose) {
|
||||
const provider = String(process.env.EMAIL_PROVIDER || "mock").trim().toLowerCase();
|
||||
|
||||
if (provider === "smtp") {
|
||||
const nodemailer = require("nodemailer");
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT) || 587,
|
||||
secure: process.env.SMTP_SECURE === "1",
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
const purposeText = purpose === "register" ? "注册" : purpose === "login" ? "登录" : "重置密码";
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM || process.env.SMTP_USER,
|
||||
to: email,
|
||||
subject: "[OmniAI] \u90ae\u7bb1\u9a8c\u8bc1\u7801",
|
||||
text: "\u60a8\u7684\u9a8c\u8bc1\u7801\u662f\uff1a" + code + "\n\u7528\u9014\uff1a" + purposeText + "\n\u6709\u6548\u671f\uff1a" + String(process.env.EMAIL_CODE_TTL_MINUTES || 10) + " \u5206\u949f\n\u5982\u679c\u4e0d\u662f\u60a8\u672c\u4eba\u64cd\u4f5c\uff0c\u8bf7\u5ffd\u7565\u6b64\u90ae\u4ef6\u3002",
|
||||
html: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><h2 style=\"color:#333\">OmniAI \u90ae\u7bb1\u9a8c\u8bc1</h2><p style=\"font-size:16px;color:#555\">\u60a8\u7684\u9a8c\u8bc1\u7801\u662f\uff1a</p><p style=\"font-size:32px;font-weight:bold;letter-spacing:6px;color:#1677ff;margin:16px 0\">" + code + "</p><p style=\"color:#888\">\u7528\u9014\uff1a" + purposeText + "</p><p style=\"color:#888\">\u6709\u6548\u671f\uff1a" + String(process.env.EMAIL_CODE_TTL_MINUTES || 10) + " \u5206\u949f</p><hr style=\"border:none;border-top:1px solid #eee;margin:24px 0\"><p style=\"color:#aaa;font-size:13px\">\u5982\u679c\u4e0d\u662f\u60a8\u672c\u4eba\u64cd\u4f5c\uff0c\u8bf7\u5ffd\u7565\u6b64\u90ae\u4ef6\u3002</p></div>",
|
||||
});
|
||||
return { provider: "smtp" };
|
||||
}
|
||||
|
||||
console.log("[email:" + purpose + "] " + email + " verification code: " + code + " (mock provider)");
|
||||
return {
|
||||
provider: "mock",
|
||||
devCode: process.env.EMAIL_DEV_RETURN_CODE === "1" ? code : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function consumeEmailCode(email, code, purpose) {
|
||||
const { rows } = await pool.query(
|
||||
"SELECT id, code_hash, attempts FROM email_verification_codes WHERE email = $1 AND purpose = $2 AND consumed_at IS NULL AND expires_at > NOW() ORDER BY created_at DESC LIMIT 1",
|
||||
[email, purpose]
|
||||
);
|
||||
|
||||
const row = rows[0];
|
||||
if (!row) return false;
|
||||
|
||||
if (Number(row.attempts || 0) >= EMAIL_CODE_MAX_ATTEMPTS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedHash = hashEmailCode(email, String(code || "").trim());
|
||||
if (row.code_hash !== expectedHash) {
|
||||
await pool.query("UPDATE email_verification_codes SET attempts = attempts + 1 WHERE id = $1", [row.id]);
|
||||
return false;
|
||||
}
|
||||
|
||||
await pool.query("UPDATE email_verification_codes SET consumed_at = NOW() WHERE id = $1", [row.id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getWechatLoginConfig() {
|
||||
const appId = process.env.WECHAT_LOGIN_APP_ID || process.env.WECHAT_APP_ID || "";
|
||||
@@ -742,6 +806,10 @@ module.exports = {
|
||||
PRICE_TYPES,
|
||||
PHONE_PATTERN,
|
||||
EMAIL_PATTERN,
|
||||
EMAIL_PURPOSES,
|
||||
EMAIL_CODE_TTL_MINUTES,
|
||||
EMAIL_CODE_COOLDOWN_SECONDS,
|
||||
EMAIL_CODE_MAX_ATTEMPTS,
|
||||
SMS_PURPOSES,
|
||||
SMS_CODE_TTL_MINUTES,
|
||||
SMS_CODE_COOLDOWN_SECONDS,
|
||||
@@ -755,6 +823,9 @@ module.exports = {
|
||||
hashSmsCode,
|
||||
generateSmsCode,
|
||||
sendSmsCode,
|
||||
hashEmailCode,
|
||||
sendEmailCode,
|
||||
consumeEmailCode,
|
||||
createLoginResultForUserId,
|
||||
sanitizeUsernameSeed,
|
||||
generateUniqueUsername,
|
||||
|
||||
@@ -131,3 +131,172 @@ function registerEcommerceRoutes(router) {
|
||||
}
|
||||
|
||||
module.exports = { registerEcommerceRoutes };
|
||||
|
||||
function registerEcommerceHistoryRoutes(router) {
|
||||
router.post("/ai/ecommerce/video-history", requireAuth, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const { title, config, plan, scenes, sourceImageUrls } = req.body;
|
||||
|
||||
if (!scenes || !Array.isArray(scenes) || scenes.length === 0) {
|
||||
return res.status(400).json({ error: "Missing scenes data" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO ecommerce_video_history (user_id, title, config_json, plan_json, scenes_json, source_image_urls)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, created_at`,
|
||||
[
|
||||
userId,
|
||||
title || "",
|
||||
JSON.stringify(config || {}),
|
||||
JSON.stringify(plan || {}),
|
||||
JSON.stringify(scenes),
|
||||
JSON.stringify(sourceImageUrls || []),
|
||||
]
|
||||
);
|
||||
res.json({ id: rows[0].id, createdAt: rows[0].created_at });
|
||||
} catch (err) {
|
||||
console.error("[ecommerce/video-history] save error:", err.message);
|
||||
res.status(500).json({ error: "保存失败" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/ai/ecommerce/video-history", requireAuth, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const limit = Math.min(Math.max(Number(req.query.limit) || 20, 1), 100);
|
||||
const offset = Math.max(Number(req.query.offset) || 0, 0);
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, title, config_json, scenes_json, source_image_urls, created_at
|
||||
FROM ecommerce_video_history
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3`,
|
||||
[userId, limit, offset]
|
||||
);
|
||||
|
||||
const { rows: countRows } = await pool.query(
|
||||
"SELECT COUNT(*)::int AS total FROM ecommerce_video_history WHERE user_id = $1",
|
||||
[userId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
items: rows.map(r => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
config: JSON.parse(r.config_json || "{}"),
|
||||
scenes: JSON.parse(r.scenes_json || "[]"),
|
||||
sourceImageUrls: JSON.parse(r.source_image_urls || "[]"),
|
||||
createdAt: r.created_at,
|
||||
})),
|
||||
total: countRows[0].total,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[ecommerce/video-history] list error:", err.message);
|
||||
res.status(500).json({ error: "查询失败" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/ai/ecommerce/video-history/:id", requireAuth, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const id = Number(req.params.id);
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, title, config_json, plan_json, scenes_json, source_image_urls, created_at
|
||||
FROM ecommerce_video_history
|
||||
WHERE id = $1 AND user_id = $2`,
|
||||
[id, userId]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: "记录不存在" });
|
||||
}
|
||||
|
||||
const r = rows[0];
|
||||
res.json({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
config: JSON.parse(r.config_json || "{}"),
|
||||
plan: JSON.parse(r.plan_json || "{}"),
|
||||
scenes: JSON.parse(r.scenes_json || "[]"),
|
||||
sourceImageUrls: JSON.parse(r.source_image_urls || "[]"),
|
||||
createdAt: r.created_at,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[ecommerce/video-history] get error:", err.message);
|
||||
res.status(500).json({ error: "查询失败" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/ai/ecommerce/video-history/:id", requireAuth, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const id = Number(req.params.id);
|
||||
|
||||
try {
|
||||
// Fetch record first to get OSS URLs before deletion
|
||||
const { rows } = await pool.query(
|
||||
"SELECT scenes_json, source_image_urls FROM ecommerce_video_history WHERE id = $1 AND user_id = $2",
|
||||
[id, userId]
|
||||
);
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: "记录不存在" });
|
||||
}
|
||||
|
||||
// Extract all OSS URLs from scenes and source images
|
||||
const scenes = JSON.parse(rows[0].scenes_json || "[]");
|
||||
const sourceUrls = JSON.parse(rows[0].source_image_urls || "[]");
|
||||
const ossUrlsToDelete = [];
|
||||
for (const scene of scenes) {
|
||||
if (scene.imageUrl) ossUrlsToDelete.push(scene.imageUrl);
|
||||
if (scene.videoUrl) ossUrlsToDelete.push(scene.videoUrl);
|
||||
}
|
||||
for (const url of sourceUrls) {
|
||||
if (url) ossUrlsToDelete.push(url);
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await pool.query(
|
||||
"DELETE FROM ecommerce_video_history WHERE id = $1 AND user_id = $2",
|
||||
[id, userId]
|
||||
);
|
||||
|
||||
// Delete OSS files in background (best-effort)
|
||||
const { deleteObject, isOssConfigured } = require("../ossClient");
|
||||
if (isOssConfigured() && ossUrlsToDelete.length > 0) {
|
||||
const bucket = String(process.env.OSS_BUCKET || "").trim();
|
||||
const region = String(process.env.OSS_REGION || "").trim().replace(/^oss-/, "");
|
||||
const ossHost = bucket + ".oss-" + region + ".aliyuncs.com";
|
||||
const publicBase = String(process.env.OSS_PUBLIC_BASE_URL || "").trim().replace(/\/+$/, "");
|
||||
|
||||
for (const url of ossUrlsToDelete) {
|
||||
try {
|
||||
let ossKey = "";
|
||||
if (publicBase && url.startsWith(publicBase)) {
|
||||
ossKey = url.slice(publicBase.length + 1);
|
||||
} else if (url.includes(ossHost)) {
|
||||
const parsed = new URL(url);
|
||||
ossKey = decodeURIComponent(parsed.pathname.slice(1));
|
||||
}
|
||||
if (ossKey) {
|
||||
await deleteObject(ossKey);
|
||||
}
|
||||
} catch (delErr) {
|
||||
console.warn("[ecommerce/video-history] OSS delete failed for:", url, delErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("[ecommerce/video-history] delete error:", err.message);
|
||||
res.status(500).json({ error: "删除失败" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.registerEcommerceHistoryRoutes = registerEcommerceHistoryRoutes;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"use strict";
|
||||
|
||||
const mammoth = require("mammoth");
|
||||
const { requireAuth } = require("./context");
|
||||
|
||||
function registerFileExtractRoutes(router) {
|
||||
router.post("/files/extract-text", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const body = Buffer.concat(chunks);
|
||||
|
||||
const contentType = req.headers["content-type"] || "";
|
||||
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;\s]+))/);
|
||||
if (!boundaryMatch) {
|
||||
return res.status(400).json({ error: "Missing multipart boundary" });
|
||||
}
|
||||
const boundary = boundaryMatch[1] || boundaryMatch[2];
|
||||
const parts = parseMultipart(body, boundary);
|
||||
const filePart = parts.find(p => p.name === "file");
|
||||
if (!filePart || !filePart.data || filePart.data.length === 0) {
|
||||
return res.status(400).json({ error: "No file uploaded" });
|
||||
}
|
||||
|
||||
const filename = filePart.filename || "unknown";
|
||||
const ext = filename.lastIndexOf(".") >= 0
|
||||
? filename.slice(filename.lastIndexOf(".")).toLowerCase()
|
||||
: "";
|
||||
|
||||
let text = "";
|
||||
if (ext === ".docx") {
|
||||
const result = await mammoth.extractRawText({ buffer: filePart.data });
|
||||
text = result.value || "";
|
||||
} else if (ext === ".doc") {
|
||||
text = filePart.data
|
||||
.toString("utf-8")
|
||||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
||||
.replace(/\s{3,}/g, "\n\n")
|
||||
.trim();
|
||||
} else {
|
||||
text = filePart.data.toString("utf-8");
|
||||
}
|
||||
|
||||
if (!text || text.trim().length === 0) {
|
||||
return res.status(422).json({ error: "Unable to extract text" });
|
||||
}
|
||||
|
||||
res.json({ text: text.trim(), filename });
|
||||
} catch (err) {
|
||||
console.error("[files/extract-text] error:", err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseMultipart(body, boundary) {
|
||||
const parts = [];
|
||||
const boundaryBuf = Buffer.from("--" + boundary);
|
||||
const crlf = Buffer.from("\r\n");
|
||||
const doubleCrlf = Buffer.from("\r\n\r\n");
|
||||
|
||||
let start = bufIndexOf(body, boundaryBuf, 0);
|
||||
if (start === -1) return parts;
|
||||
start += boundaryBuf.length + crlf.length;
|
||||
|
||||
while (true) {
|
||||
const end = bufIndexOf(body, boundaryBuf, start);
|
||||
if (end === -1) break;
|
||||
|
||||
const partData = body.slice(start, end - crlf.length);
|
||||
const headerEnd = bufIndexOf(partData, doubleCrlf, 0);
|
||||
if (headerEnd === -1) {
|
||||
start = end + boundaryBuf.length + crlf.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
const headerStr = partData.slice(0, headerEnd).toString("utf-8");
|
||||
const content = partData.slice(headerEnd + doubleCrlf.length);
|
||||
|
||||
const nameMatch = headerStr.match(/name="([^"]+)"/);
|
||||
const filenameMatch = headerStr.match(/filename="([^"]*)"/);
|
||||
|
||||
parts.push({
|
||||
name: nameMatch ? nameMatch[1] : "",
|
||||
filename: filenameMatch ? filenameMatch[1] : null,
|
||||
data: content,
|
||||
});
|
||||
|
||||
const afterBoundary = body.slice(end + boundaryBuf.length, end + boundaryBuf.length + 2);
|
||||
if (afterBoundary.toString() === "--") break;
|
||||
start = end + boundaryBuf.length + crlf.length;
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
function bufIndexOf(buf, search, from) {
|
||||
for (let i = from; i <= buf.length - search.length; i++) {
|
||||
let found = true;
|
||||
for (let j = 0; j < search.length; j++) {
|
||||
if (buf[i + j] !== search[j]) { found = false; break; }
|
||||
}
|
||||
if (found) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
module.exports = { registerFileExtractRoutes };
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
const express = require('express')
|
||||
const { registerAuthRoutes } = require('./auth')
|
||||
const { registerPriceRoutes, registerPackageRoutes, registerHealthRoutes } = require('./public')
|
||||
const { registerPriceRoutes, registerPackageRoutes, registerPublicConfigRoutes, registerHealthRoutes } = require('./public')
|
||||
const { registerKeyRoutes } = require('./keys')
|
||||
const { registerAdminRoutes, registerAdminInvoiceRoutes } = require('./admin')
|
||||
const { registerEnterpriseRoutes } = require('./enterprise')
|
||||
@@ -12,18 +12,20 @@ const { registerProjectRoutes } = require('./projects')
|
||||
const { registerOssRoutes } = require('./oss')
|
||||
const { registerCommunityRoutes, registerAdminCommunityRoutes } = require('./community')
|
||||
const { registerAiRoutes } = require('./ai')
|
||||
const { registerEcommerceRoutes } = require("./ecommerce")
|
||||
const { registerEcommerceRoutes, registerEcommerceHistoryRoutes } = require("./ecommerce")
|
||||
const { registerConversationRoutes } = require('./conversations')
|
||||
const { registerReportRoutes } = require('./reports')
|
||||
const { registerAssetRoutes } = require('./assets')
|
||||
const { registerNotificationRoutes } = require('./notifications')
|
||||
const { registerDraftRoutes } = require('./drafts');
|
||||
const { registerFileExtractRoutes } = require('./fileExtract');
|
||||
const mountClientErrorRoutes = require('./clientErrors')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
registerAuthRoutes(router)
|
||||
registerPriceRoutes(router)
|
||||
registerPublicConfigRoutes(router)
|
||||
registerKeyRoutes(router)
|
||||
registerAdminRoutes(router)
|
||||
registerPackageRoutes(router)
|
||||
@@ -41,11 +43,13 @@ registerCommunityRoutes(router)
|
||||
registerAdminCommunityRoutes(router)
|
||||
registerAiRoutes(router)
|
||||
registerEcommerceRoutes(router)
|
||||
registerEcommerceHistoryRoutes(router)
|
||||
registerConversationRoutes(router)
|
||||
registerReportRoutes(router)
|
||||
registerAssetRoutes(router)
|
||||
registerNotificationRoutes(router)
|
||||
registerDraftRoutes(router)
|
||||
registerFileExtractRoutes(router)
|
||||
registerHealthRoutes(router)
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -36,14 +36,50 @@ function registerPackageRoutes(router) {
|
||||
function registerHealthRoutes(router) {
|
||||
// ── Health ───────────────────────────────────────────────────────────
|
||||
|
||||
// Public health: minimal response, no provider/key details exposed
|
||||
router.get("/health", async (_req, res) => {
|
||||
res.json({ status: "ok", uptime: process.uptime() });
|
||||
});
|
||||
|
||||
// Admin-only health: full provider status (requires auth via admin middleware)
|
||||
router.get("/admin/providers/status", async (_req, res) => {
|
||||
const status = await keyManager.getAllStatus();
|
||||
res.json({ status: "ok", uptime: process.uptime(), providers: status });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const PUBLIC_CONFIG_PROFILES = new Set(["web-public-config", "web-model-capabilities"]);
|
||||
|
||||
function createPublicConfigFallback(name) {
|
||||
if (name === "web-model-capabilities") {
|
||||
return {
|
||||
imageModels: [],
|
||||
videoModels: [],
|
||||
chatModels: [],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function registerPublicConfigRoutes(router) {
|
||||
router.get("/public/config/profile", async (req, res) => {
|
||||
const name = String(req.query.name || "web-public-config").trim() || "web-public-config";
|
||||
if (!PUBLIC_CONFIG_PROFILES.has(name)) return res.status(404).json({ error: "Public config profile not found" });
|
||||
const { rows: [row] } = await pool.query(
|
||||
"SELECT config_json, description, updated_at FROM config_profiles WHERE name = $1",
|
||||
[name],
|
||||
);
|
||||
if (!row) return res.json({ name, config: createPublicConfigFallback(name), description: "", updatedAt: null });
|
||||
let config = {};
|
||||
try { config = JSON.parse(row.config_json || "{}"); } catch {}
|
||||
return res.json({ name, config, description: row.description || "", updatedAt: row.updated_at });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
registerPriceRoutes,
|
||||
registerPackageRoutes,
|
||||
registerPublicConfigRoutes,
|
||||
registerHealthRoutes,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user