[SYNC-ERP-CORE] feat: Propagate middlewares, logger, DTOs and Swagger from erp-core
- Add logger utility (winston-based) - Add apiKeyAuth middleware for API key authentication - Add fieldPermissions middleware for field-level access control - Add Swagger/OpenAPI configuration and integration - Refactor DTOs with Zod validation schemas: - feature-flag.dto.ts - terminal.dto.ts - transaction.dto.ts - ai.dto.ts - Add error classes (AppError, UnauthorizedError, ForbiddenError, etc.) - Add AuthenticatedRequest type for erp-core compatibility - Add ESLint configuration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
11f8bc3ad0
commit
85043cbaff
27
.eslintrc.json
Normal file
27
.eslintrc.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"es2022": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"no-console": "off"
|
||||||
|
},
|
||||||
|
"ignorePatterns": ["dist/", "node_modules/"]
|
||||||
|
}
|
||||||
270
package-lock.json
generated
270
package-lock.json
generated
@ -23,6 +23,8 @@
|
|||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.0",
|
||||||
"typeorm": "^0.3.17",
|
"typeorm": "^0.3.17",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
@ -40,6 +42,8 @@
|
|||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/speakeasy": "^2.0.10",
|
"@types/speakeasy": "^2.0.10",
|
||||||
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
|
"@types/swagger-ui-express": "^4.1.6",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
"@typescript-eslint/parser": "^6.14.0",
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
@ -53,6 +57,50 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||||
|
"version": "9.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
|
||||||
|
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"@types/json-schema": "^7.0.6",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/openapi-schemas": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-methods": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||||
|
"@apidevtools/openapi-schemas": "^2.0.4",
|
||||||
|
"@apidevtools/swagger-methods": "^3.0.2",
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"z-schema": "^5.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"openapi-types": ">=7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@ -1317,6 +1365,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jsdevtools/ono": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -1365,6 +1419,13 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@scarf/scarf": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.27.8",
|
"version": "0.27.8",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||||
@ -1615,7 +1676,6 @@
|
|||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/jsonwebtoken": {
|
"node_modules/@types/jsonwebtoken": {
|
||||||
@ -1770,6 +1830,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/swagger-jsdoc": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/swagger-ui-express": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/serve-static": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/triple-beam": {
|
"node_modules/@types/triple-beam": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||||
@ -2182,7 +2260,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
@ -2638,6 +2715,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-me-maybe": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@ -2888,6 +2971,15 @@
|
|||||||
"node": ">=12.20"
|
"node": ">=12.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/compressible": {
|
"node_modules/compressible": {
|
||||||
"version": "2.0.18",
|
"version": "2.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||||
@ -2937,7 +3029,6 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
@ -3191,7 +3282,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esutils": "^2.0.2"
|
"esutils": "^2.0.2"
|
||||||
@ -3549,7 +3639,6 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@ -3921,7 +4010,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
@ -4372,7 +4460,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.3.0",
|
"once": "^1.3.0",
|
||||||
@ -5223,7 +5310,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@ -5408,6 +5494,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.get": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@ -5420,6 +5513,13 @@
|
|||||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.isinteger": {
|
"node_modules/lodash.isinteger": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
@ -5458,6 +5558,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.mergewith": {
|
||||||
|
"version": "4.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||||
|
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.once": {
|
"node_modules/lodash.once": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
@ -5846,7 +5952,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
@ -5877,6 +5982,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-types": {
|
||||||
|
"version": "12.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||||
|
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@ -5996,7 +6108,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@ -7366,6 +7477,105 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-jsdoc": {
|
||||||
|
"version": "6.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||||
|
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "6.2.0",
|
||||||
|
"doctrine": "3.0.0",
|
||||||
|
"glob": "7.1.6",
|
||||||
|
"lodash.mergewith": "^4.6.2",
|
||||||
|
"swagger-parser": "^10.0.3",
|
||||||
|
"yaml": "2.0.0-1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"swagger-jsdoc": "bin/swagger-jsdoc.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-jsdoc/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-jsdoc/node_modules/glob": {
|
||||||
|
"version": "7.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
|
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||||
|
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-jsdoc/node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/swagger-parser": "10.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-dist": {
|
||||||
|
"version": "5.31.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz",
|
||||||
|
"integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@scarf/scarf": "=1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-express": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"swagger-ui-dist": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= v0.10.32"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/test-exclude": {
|
"node_modules/test-exclude": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||||
@ -8201,7 +8411,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/write-file-atomic": {
|
"node_modules/write-file-atomic": {
|
||||||
@ -8243,6 +8452,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.0.0-1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||||
|
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
@ -8293,6 +8511,36 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/z-schema": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"validator": "^13.7.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"z-schema": "bin/z-schema"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"commander": "^9.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/z-schema/node_modules/commander": {
|
||||||
|
"version": "9.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||||
|
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
|
|||||||
@ -32,7 +32,9 @@
|
|||||||
"typeorm": "^0.3.17",
|
"typeorm": "^0.3.17",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
@ -47,6 +49,8 @@
|
|||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/speakeasy": "^2.0.10",
|
"@types/speakeasy": "^2.0.10",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
|
"@types/swagger-ui-express": "^4.1.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
"@typescript-eslint/parser": "^6.14.0",
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
|
|||||||
230
src/config/swagger.config.ts
Normal file
230
src/config/swagger.config.ts
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* Swagger/OpenAPI Configuration for ERP Mecanicas Diesel
|
||||||
|
* Propagated from erp-core with adaptations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import swaggerJSDoc from 'swagger-jsdoc';
|
||||||
|
import { Application } from 'express';
|
||||||
|
import swaggerUi from 'swagger-ui-express';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Swagger definition
|
||||||
|
const swaggerDefinition = {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'ERP Mecánicas Diesel - API',
|
||||||
|
version: '0.3.0',
|
||||||
|
description: `
|
||||||
|
API para el sistema ERP especializado en talleres mecánicos de motores diesel.
|
||||||
|
|
||||||
|
## Características principales
|
||||||
|
- Autenticación JWT y gestión de sesiones
|
||||||
|
- Multi-tenant con aislamiento de datos por taller
|
||||||
|
- Gestión de órdenes de servicio y diagnósticos
|
||||||
|
- Control de inventario de refacciones
|
||||||
|
- Gestión de vehículos y flotas
|
||||||
|
- Terminales de pago (MercadoPago, Clip)
|
||||||
|
- Sistema de despacho y técnicos
|
||||||
|
- Servicio en campo con soporte offline
|
||||||
|
|
||||||
|
## Autenticación
|
||||||
|
Todos los endpoints (excepto auth) requieren autenticación mediante Bearer Token (JWT).
|
||||||
|
El token debe incluirse en el header Authorization: Bearer <token>
|
||||||
|
|
||||||
|
## Multi-tenant
|
||||||
|
El sistema identifica automáticamente el taller (tenant) del usuario autenticado
|
||||||
|
y filtra todos los datos según el contexto del taller.
|
||||||
|
`,
|
||||||
|
contact: {
|
||||||
|
name: 'ERP Mecánicas Diesel Support',
|
||||||
|
email: 'support@mecanicas-diesel.com',
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
name: 'Proprietary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: 'http://localhost:3041/api/v1',
|
||||||
|
description: 'Desarrollo local',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.mecanicas-diesel.com/api/v1',
|
||||||
|
description: 'Producción',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tags: [
|
||||||
|
{ name: 'Auth', description: 'Autenticación y autorización (JWT, MFA)' },
|
||||||
|
{ name: 'Users', description: 'Gestión de usuarios y perfiles' },
|
||||||
|
{ name: 'Customers', description: 'Gestión de clientes del taller' },
|
||||||
|
{ name: 'Vehicles', description: 'Gestión de vehículos y flotas' },
|
||||||
|
{ name: 'Service Orders', description: 'Órdenes de servicio y diagnósticos' },
|
||||||
|
{ name: 'Quotes', description: 'Cotizaciones y presupuestos' },
|
||||||
|
{ name: 'Parts', description: 'Inventario de refacciones' },
|
||||||
|
{ name: 'Suppliers', description: 'Gestión de proveedores' },
|
||||||
|
{ name: 'GPS', description: 'Tracking GPS y geofencing' },
|
||||||
|
{ name: 'Assets', description: 'Gestión de activos del taller' },
|
||||||
|
{ name: 'Dispatch', description: 'Despacho de técnicos y turnos' },
|
||||||
|
{ name: 'Field Service', description: 'Servicio en campo y checklists' },
|
||||||
|
{ name: 'Payments', description: 'Terminales de pago y transacciones' },
|
||||||
|
{ name: 'Feature Flags', description: 'Configuración de features' },
|
||||||
|
{ name: 'Audit', description: 'Auditoría y logs del sistema' },
|
||||||
|
{ name: 'Health', description: 'Health checks y monitoreo' },
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
BearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
description: 'Token JWT obtenido del endpoint de login',
|
||||||
|
},
|
||||||
|
ApiKeyAuth: {
|
||||||
|
type: 'apiKey',
|
||||||
|
in: 'header',
|
||||||
|
name: 'X-API-Key',
|
||||||
|
description: 'API Key para integraciones externas',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schemas: {
|
||||||
|
ApiResponse: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PaginatedResponse: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: {
|
||||||
|
type: 'boolean',
|
||||||
|
example: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
type: 'integer',
|
||||||
|
example: 1,
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: 'integer',
|
||||||
|
example: 20,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: 'integer',
|
||||||
|
example: 100,
|
||||||
|
},
|
||||||
|
totalPages: {
|
||||||
|
type: 'integer',
|
||||||
|
example: 5,
|
||||||
|
},
|
||||||
|
hasNext: {
|
||||||
|
type: 'boolean',
|
||||||
|
example: true,
|
||||||
|
},
|
||||||
|
hasPrev: {
|
||||||
|
type: 'boolean',
|
||||||
|
example: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Error: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: {
|
||||||
|
type: 'boolean',
|
||||||
|
example: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'Error description',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'VALIDATION_ERROR',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
BearerAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Options for swagger-jsdoc
|
||||||
|
const options: swaggerJSDoc.Options = {
|
||||||
|
definition: swaggerDefinition,
|
||||||
|
// Path to the API routes for JSDoc comments
|
||||||
|
apis: [
|
||||||
|
path.resolve(process.cwd(), 'src/modules/**/*.controller.ts'),
|
||||||
|
path.resolve(process.cwd(), 'src/modules/**/*.routes.ts'),
|
||||||
|
path.resolve(process.cwd(), 'dist/modules/**/*.controller.js'),
|
||||||
|
path.resolve(process.cwd(), 'dist/modules/**/*.routes.js'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize swagger-jsdoc
|
||||||
|
const swaggerSpec = swaggerJSDoc(options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Swagger documentation for Express app
|
||||||
|
*/
|
||||||
|
export function setupSwagger(app: Application, prefix: string = '/api/v1') {
|
||||||
|
// Swagger UI options
|
||||||
|
const swaggerUiOptions = {
|
||||||
|
customCss: `
|
||||||
|
.swagger-ui .topbar { display: none }
|
||||||
|
.swagger-ui .info { margin: 50px 0; }
|
||||||
|
.swagger-ui .info .title { font-size: 36px; }
|
||||||
|
`,
|
||||||
|
customSiteTitle: 'ERP Mecánicas Diesel - API Documentation',
|
||||||
|
swaggerOptions: {
|
||||||
|
persistAuthorization: true,
|
||||||
|
displayRequestDuration: true,
|
||||||
|
filter: true,
|
||||||
|
tagsSorter: 'alpha',
|
||||||
|
operationsSorter: 'alpha',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Serve Swagger UI
|
||||||
|
app.use(`${prefix}/docs`, swaggerUi.serve);
|
||||||
|
app.get(`${prefix}/docs`, swaggerUi.setup(swaggerSpec, swaggerUiOptions));
|
||||||
|
|
||||||
|
// Serve OpenAPI spec as JSON
|
||||||
|
app.get(`${prefix}/docs.json`, (_req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.send(swaggerSpec);
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3041;
|
||||||
|
console.log(`📚 Swagger docs available at: http://localhost:${port}${prefix}/docs`);
|
||||||
|
console.log(`📄 OpenAPI spec JSON at: http://localhost:${port}${prefix}/docs.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { swaggerSpec };
|
||||||
@ -60,6 +60,9 @@ import { featureFlagsMiddleware } from './modules/feature-flags/middleware/featu
|
|||||||
// Payment Terminals Module
|
// Payment Terminals Module
|
||||||
import { PaymentTerminalsModule } from './modules/payment-terminals';
|
import { PaymentTerminalsModule } from './modules/payment-terminals';
|
||||||
|
|
||||||
|
// Swagger Documentation
|
||||||
|
import { setupSwagger } from './config/swagger.config';
|
||||||
|
|
||||||
// Entities - Auth
|
// Entities - Auth
|
||||||
import { User } from './modules/auth/entities/user.entity';
|
import { User } from './modules/auth/entities/user.entity';
|
||||||
import { RefreshToken } from './modules/auth/entities/refresh-token.entity';
|
import { RefreshToken } from './modules/auth/entities/refresh-token.entity';
|
||||||
@ -249,7 +252,10 @@ async function bootstrap() {
|
|||||||
try {
|
try {
|
||||||
// Initialize database connection
|
// Initialize database connection
|
||||||
await AppDataSource.initialize();
|
await AppDataSource.initialize();
|
||||||
console.log('📦 Database connection established');
|
console.log('📦 Database connection established');
|
||||||
|
|
||||||
|
// Setup Swagger documentation
|
||||||
|
setupSwagger(app);
|
||||||
|
|
||||||
// Register API routes
|
// Register API routes
|
||||||
app.use('/api/v1/auth', createAuthController(AppDataSource));
|
app.use('/api/v1/auth', createAuthController(AppDataSource));
|
||||||
|
|||||||
@ -1,110 +1,156 @@
|
|||||||
/**
|
/**
|
||||||
* AI DTOs
|
* AI DTOs
|
||||||
* Using plain TypeScript interfaces (Zod for runtime validation)
|
* Version: 2.0.0 - With Zod Validation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// PROMPT DTOs
|
// ZOD SCHEMAS - PROMPT
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export interface CreatePromptDto {
|
export const CreatePromptSchema = z.object({
|
||||||
code: string;
|
code: z.string().min(1, 'Code is required').max(50),
|
||||||
name: string;
|
name: z.string().min(1, 'Name is required').max(100),
|
||||||
description?: string;
|
description: z.string().max(500).optional(),
|
||||||
category?: string;
|
category: z.string().max(50).optional(),
|
||||||
systemPrompt: string;
|
systemPrompt: z.string().min(1, 'System prompt is required').max(10000),
|
||||||
userPromptTemplate?: string;
|
userPromptTemplate: z.string().max(5000).optional(),
|
||||||
variables?: string[];
|
variables: z.array(z.string()).optional(),
|
||||||
temperature?: number;
|
temperature: z.number().min(0).max(2).optional(),
|
||||||
maxTokens?: number;
|
maxTokens: z.number().int().positive().max(100000).optional(),
|
||||||
stopSequences?: string[];
|
stopSequences: z.array(z.string()).optional(),
|
||||||
modelParameters?: Record<string, any>;
|
modelParameters: z.record(z.any()).optional(),
|
||||||
allowedModels?: string[];
|
allowedModels: z.array(z.string()).optional(),
|
||||||
metadata?: Record<string, any>;
|
metadata: z.record(z.any()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdatePromptSchema = z.object({
|
||||||
|
name: z.string().min(1).max(100).optional(),
|
||||||
|
description: z.string().max(500).optional(),
|
||||||
|
category: z.string().max(50).optional(),
|
||||||
|
systemPrompt: z.string().min(1).max(10000).optional(),
|
||||||
|
userPromptTemplate: z.string().max(5000).optional(),
|
||||||
|
variables: z.array(z.string()).optional(),
|
||||||
|
temperature: z.number().min(0).max(2).optional(),
|
||||||
|
maxTokens: z.number().int().positive().max(100000).optional(),
|
||||||
|
stopSequences: z.array(z.string()).optional(),
|
||||||
|
modelParameters: z.record(z.any()).optional(),
|
||||||
|
isActive: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ZOD SCHEMAS - CONVERSATION
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const CreateConversationSchema = z.object({
|
||||||
|
modelId: z.string().uuid().optional(),
|
||||||
|
promptId: z.string().uuid().optional(),
|
||||||
|
title: z.string().max(200).optional(),
|
||||||
|
systemPrompt: z.string().max(10000).optional(),
|
||||||
|
temperature: z.number().min(0).max(2).optional(),
|
||||||
|
maxTokens: z.number().int().positive().max(100000).optional(),
|
||||||
|
context: z.record(z.any()).optional(),
|
||||||
|
metadata: z.record(z.any()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateConversationSchema = z.object({
|
||||||
|
title: z.string().max(200).optional(),
|
||||||
|
systemPrompt: z.string().max(10000).optional(),
|
||||||
|
temperature: z.number().min(0).max(2).optional(),
|
||||||
|
maxTokens: z.number().int().positive().max(100000).optional(),
|
||||||
|
context: z.record(z.any()).optional(),
|
||||||
|
metadata: z.record(z.any()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ZOD SCHEMAS - MESSAGE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const AddMessageSchema = z.object({
|
||||||
|
role: z.enum(['user', 'assistant', 'system']),
|
||||||
|
content: z.string().min(1, 'Content is required').max(100000),
|
||||||
|
modelCode: z.string().max(100).optional(),
|
||||||
|
promptTokens: z.number().int().nonnegative().optional(),
|
||||||
|
completionTokens: z.number().int().nonnegative().optional(),
|
||||||
|
totalTokens: z.number().int().nonnegative().optional(),
|
||||||
|
finishReason: z.string().max(50).optional(),
|
||||||
|
latencyMs: z.number().int().nonnegative().optional(),
|
||||||
|
metadata: z.record(z.any()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ZOD SCHEMAS - USAGE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const LogUsageSchema = z.object({
|
||||||
|
userId: z.string().uuid().optional(),
|
||||||
|
conversationId: z.string().uuid().optional(),
|
||||||
|
modelId: z.string().uuid('Model ID is required'),
|
||||||
|
usageType: z.string().min(1, 'Usage type is required').max(50),
|
||||||
|
promptTokens: z.number().int().nonnegative(),
|
||||||
|
completionTokens: z.number().int().nonnegative(),
|
||||||
|
costUsd: z.number().nonnegative().optional(),
|
||||||
|
latencyMs: z.number().int().nonnegative().optional(),
|
||||||
|
wasSuccessful: z.boolean().default(true),
|
||||||
|
errorMessage: z.string().max(500).optional(),
|
||||||
|
metadata: z.record(z.any()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ZOD SCHEMAS - QUOTA
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const UpdateQuotaSchema = z.object({
|
||||||
|
maxRequestsPerMonth: z.number().int().positive().optional(),
|
||||||
|
maxTokensPerMonth: z.number().int().positive().optional(),
|
||||||
|
maxSpendPerMonth: z.number().positive().optional(),
|
||||||
|
maxRequestsPerDay: z.number().int().positive().optional(),
|
||||||
|
maxTokensPerDay: z.number().int().positive().optional(),
|
||||||
|
allowedModels: z.array(z.string()).optional(),
|
||||||
|
blockedModels: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TYPE DEFINITIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export type CreatePromptDto = z.infer<typeof CreatePromptSchema>;
|
||||||
|
export type UpdatePromptDto = z.infer<typeof UpdatePromptSchema>;
|
||||||
|
export type CreateConversationDto = z.infer<typeof CreateConversationSchema>;
|
||||||
|
export type UpdateConversationDto = z.infer<typeof UpdateConversationSchema>;
|
||||||
|
export type AddMessageDto = z.infer<typeof AddMessageSchema>;
|
||||||
|
export type LogUsageDto = z.infer<typeof LogUsageSchema>;
|
||||||
|
export type UpdateQuotaDto = z.infer<typeof UpdateQuotaSchema>;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// VALIDATION HELPERS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export function validateCreatePrompt(data: unknown): CreatePromptDto {
|
||||||
|
return CreatePromptSchema.parse(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdatePromptDto {
|
export function validateUpdatePrompt(data: unknown): UpdatePromptDto {
|
||||||
name?: string;
|
return UpdatePromptSchema.parse(data);
|
||||||
description?: string;
|
|
||||||
category?: string;
|
|
||||||
systemPrompt?: string;
|
|
||||||
userPromptTemplate?: string;
|
|
||||||
variables?: string[];
|
|
||||||
temperature?: number;
|
|
||||||
maxTokens?: number;
|
|
||||||
stopSequences?: string[];
|
|
||||||
modelParameters?: Record<string, any>;
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
export function validateCreateConversation(data: unknown): CreateConversationDto {
|
||||||
// CONVERSATION DTOs
|
return CreateConversationSchema.parse(data);
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export interface CreateConversationDto {
|
|
||||||
modelId?: string;
|
|
||||||
promptId?: string;
|
|
||||||
title?: string;
|
|
||||||
systemPrompt?: string;
|
|
||||||
temperature?: number;
|
|
||||||
maxTokens?: number;
|
|
||||||
context?: Record<string, any>;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateConversationDto {
|
export function validateUpdateConversation(data: unknown): UpdateConversationDto {
|
||||||
title?: string;
|
return UpdateConversationSchema.parse(data);
|
||||||
systemPrompt?: string;
|
|
||||||
temperature?: number;
|
|
||||||
maxTokens?: number;
|
|
||||||
context?: Record<string, any>;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
export function validateAddMessage(data: unknown): AddMessageDto {
|
||||||
// MESSAGE DTOs
|
return AddMessageSchema.parse(data);
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export interface AddMessageDto {
|
|
||||||
role: string;
|
|
||||||
content: string;
|
|
||||||
modelCode?: string;
|
|
||||||
promptTokens?: number;
|
|
||||||
completionTokens?: number;
|
|
||||||
totalTokens?: number;
|
|
||||||
finishReason?: string;
|
|
||||||
latencyMs?: number;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
export function validateLogUsage(data: unknown): LogUsageDto {
|
||||||
// USAGE DTOs
|
return LogUsageSchema.parse(data);
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export interface LogUsageDto {
|
|
||||||
userId?: string;
|
|
||||||
conversationId?: string;
|
|
||||||
modelId: string;
|
|
||||||
usageType: string;
|
|
||||||
promptTokens: number;
|
|
||||||
completionTokens: number;
|
|
||||||
costUsd?: number;
|
|
||||||
latencyMs?: number;
|
|
||||||
wasSuccessful?: boolean;
|
|
||||||
errorMessage?: string;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
export function validateUpdateQuota(data: unknown): UpdateQuotaDto {
|
||||||
// QUOTA DTOs
|
return UpdateQuotaSchema.parse(data);
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export interface UpdateQuotaDto {
|
|
||||||
maxRequestsPerMonth?: number;
|
|
||||||
maxTokensPerMonth?: number;
|
|
||||||
maxSpendPerMonth?: number;
|
|
||||||
maxRequestsPerDay?: number;
|
|
||||||
maxTokensPerDay?: number;
|
|
||||||
allowedModels?: string[];
|
|
||||||
blockedModels?: string[];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +1,102 @@
|
|||||||
// =====================================================
|
// =====================================================
|
||||||
// DTOs: Feature Flags
|
// DTOs: Feature Flags
|
||||||
// Modulo: MGN-019
|
// Modulo: MGN-019
|
||||||
// Version: 1.0.0
|
// Version: 2.0.0 - With Zod Validation
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
export interface CreateFlagDto {
|
import { z } from 'zod';
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
rolloutPercentage?: number;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateFlagDto {
|
// ============================================
|
||||||
name?: string;
|
// ZOD SCHEMAS
|
||||||
description?: string;
|
// ============================================
|
||||||
enabled?: boolean;
|
|
||||||
rolloutPercentage?: number;
|
|
||||||
tags?: string[];
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateTenantOverrideDto {
|
export const CreateFlagSchema = z.object({
|
||||||
flagId: string;
|
code: z.string().min(1, 'Code is required').max(50, 'Code max 50 chars'),
|
||||||
tenantId: string;
|
name: z.string().min(1, 'Name is required').max(100, 'Name max 100 chars'),
|
||||||
enabled: boolean;
|
description: z.string().max(500).optional(),
|
||||||
reason?: string;
|
enabled: z.boolean().default(false),
|
||||||
expiresAt?: Date;
|
rolloutPercentage: z.number().min(0).max(100).optional(),
|
||||||
}
|
tags: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export interface UpdateTenantOverrideDto {
|
export const UpdateFlagSchema = z.object({
|
||||||
enabled?: boolean;
|
name: z.string().min(1).max(100).optional(),
|
||||||
reason?: string;
|
description: z.string().max(500).optional(),
|
||||||
expiresAt?: Date | null;
|
enabled: z.boolean().optional(),
|
||||||
}
|
rolloutPercentage: z.number().min(0).max(100).optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
isActive: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export interface EvaluateFlagDto {
|
export const CreateTenantOverrideSchema = z.object({
|
||||||
flagCode: string;
|
flagId: z.string().uuid('Invalid flag ID'),
|
||||||
tenantId: string;
|
tenantId: z.string().uuid('Invalid tenant ID'),
|
||||||
}
|
enabled: z.boolean(),
|
||||||
|
reason: z.string().max(255).optional(),
|
||||||
|
expiresAt: z.coerce.date().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export interface EvaluateFlagsDto {
|
export const UpdateTenantOverrideSchema = z.object({
|
||||||
flagCodes: string[];
|
enabled: z.boolean().optional(),
|
||||||
tenantId: string;
|
reason: z.string().max(255).optional(),
|
||||||
}
|
expiresAt: z.coerce.date().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const EvaluateFlagSchema = z.object({
|
||||||
|
flagCode: z.string().min(1, 'Flag code is required'),
|
||||||
|
tenantId: z.string().uuid('Invalid tenant ID'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const EvaluateFlagsSchema = z.object({
|
||||||
|
flagCodes: z.array(z.string()).min(1, 'At least one flag code required'),
|
||||||
|
tenantId: z.string().uuid('Invalid tenant ID'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TYPE DEFINITIONS (inferred from schemas)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export type CreateFlagDto = z.infer<typeof CreateFlagSchema>;
|
||||||
|
export type UpdateFlagDto = z.infer<typeof UpdateFlagSchema>;
|
||||||
|
export type CreateTenantOverrideDto = z.infer<typeof CreateTenantOverrideSchema>;
|
||||||
|
export type UpdateTenantOverrideDto = z.infer<typeof UpdateTenantOverrideSchema>;
|
||||||
|
export type EvaluateFlagDto = z.infer<typeof EvaluateFlagSchema>;
|
||||||
|
export type EvaluateFlagsDto = z.infer<typeof EvaluateFlagsSchema>;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// RESPONSE TYPES (no validation needed)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
export interface FlagEvaluationResult {
|
export interface FlagEvaluationResult {
|
||||||
code: string;
|
code: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
source: 'override' | 'global' | 'rollout' | 'default';
|
source: 'override' | 'global' | 'rollout' | 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// VALIDATION HELPERS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export function validateCreateFlag(data: unknown): CreateFlagDto {
|
||||||
|
return CreateFlagSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUpdateFlag(data: unknown): UpdateFlagDto {
|
||||||
|
return UpdateFlagSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateCreateTenantOverride(data: unknown): CreateTenantOverrideDto {
|
||||||
|
return CreateTenantOverrideSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUpdateTenantOverride(data: unknown): UpdateTenantOverrideDto {
|
||||||
|
return UpdateTenantOverrideSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateEvaluateFlag(data: unknown): EvaluateFlagDto {
|
||||||
|
return EvaluateFlagSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateEvaluateFlags(data: unknown): EvaluateFlagsDto {
|
||||||
|
return EvaluateFlagsSchema.parse(data);
|
||||||
|
}
|
||||||
|
|||||||
@ -47,13 +47,13 @@ export class TransactionsController {
|
|||||||
*/
|
*/
|
||||||
private async getStats(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
private async getStats(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const filter: TransactionFilterDto = {
|
const filter: Partial<TransactionFilterDto> = {
|
||||||
branchId: req.query.branchId as string,
|
branchId: req.query.branchId as string,
|
||||||
startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined,
|
startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined,
|
||||||
endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined,
|
endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const stats = await this.service.getStats(req.tenantId!, filter);
|
const stats = await this.service.getStats(req.tenantId!, filter as TransactionFilterDto);
|
||||||
res.json({ data: stats });
|
res.json({ data: stats });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@ -112,8 +112,8 @@ export class TransactionsController {
|
|||||||
terminalProvider: req.query.terminalProvider as string,
|
terminalProvider: req.query.terminalProvider as string,
|
||||||
startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined,
|
startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined,
|
||||||
endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined,
|
endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined,
|
||||||
limit: req.query.limit ? parseInt(req.query.limit as string) : undefined,
|
limit: req.query.limit ? parseInt(req.query.limit as string) : 20,
|
||||||
offset: req.query.offset ? parseInt(req.query.offset as string) : undefined,
|
offset: req.query.offset ? parseInt(req.query.offset as string) : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.service.findAll(req.tenantId!, filter);
|
const result = await this.service.findAll(req.tenantId!, filter);
|
||||||
|
|||||||
@ -1,39 +1,64 @@
|
|||||||
/**
|
/**
|
||||||
* Terminal DTOs
|
* Terminal DTOs
|
||||||
|
* Version: 2.0.0 - With Zod Validation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Local type definitions (based on tenant-terminal-config.entity.ts)
|
import { z } from 'zod';
|
||||||
export type TerminalProvider = 'mercadopago' | 'clip' | 'stripe_terminal';
|
|
||||||
export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';
|
|
||||||
|
|
||||||
export class CreateTerminalDto {
|
// ============================================
|
||||||
branchId: string;
|
// ENUMS
|
||||||
terminalProvider: TerminalProvider;
|
// ============================================
|
||||||
terminalId: string;
|
|
||||||
terminalName?: string;
|
|
||||||
credentials?: Record<string, any>;
|
|
||||||
isPrimary?: boolean;
|
|
||||||
dailyLimit?: number;
|
|
||||||
transactionLimit?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdateTerminalDto {
|
export const TerminalProviderEnum = z.enum(['mercadopago', 'clip', 'stripe_terminal']);
|
||||||
terminalName?: string;
|
export const HealthStatusEnum = z.enum(['healthy', 'degraded', 'unhealthy', 'unknown']);
|
||||||
credentials?: Record<string, any>;
|
|
||||||
isPrimary?: boolean;
|
|
||||||
isActive?: boolean;
|
|
||||||
dailyLimit?: number;
|
|
||||||
transactionLimit?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TerminalHealthCheckDto {
|
export type TerminalProvider = z.infer<typeof TerminalProviderEnum>;
|
||||||
terminalId: string;
|
export type HealthStatus = z.infer<typeof HealthStatusEnum>;
|
||||||
status: HealthStatus;
|
|
||||||
message?: string;
|
|
||||||
responseTime?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TerminalResponseDto {
|
// ============================================
|
||||||
|
// ZOD SCHEMAS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const CreateTerminalSchema = z.object({
|
||||||
|
branchId: z.string().uuid('Invalid branch ID'),
|
||||||
|
terminalProvider: TerminalProviderEnum,
|
||||||
|
terminalId: z.string().min(1, 'Terminal ID is required').max(100),
|
||||||
|
terminalName: z.string().max(100).optional(),
|
||||||
|
credentials: z.record(z.any()).optional(),
|
||||||
|
isPrimary: z.boolean().default(false),
|
||||||
|
dailyLimit: z.number().positive().optional(),
|
||||||
|
transactionLimit: z.number().positive().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateTerminalSchema = z.object({
|
||||||
|
terminalName: z.string().max(100).optional(),
|
||||||
|
credentials: z.record(z.any()).optional(),
|
||||||
|
isPrimary: z.boolean().optional(),
|
||||||
|
isActive: z.boolean().optional(),
|
||||||
|
dailyLimit: z.number().positive().nullable().optional(),
|
||||||
|
transactionLimit: z.number().positive().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TerminalHealthCheckSchema = z.object({
|
||||||
|
terminalId: z.string().min(1, 'Terminal ID is required'),
|
||||||
|
status: HealthStatusEnum,
|
||||||
|
message: z.string().max(500).optional(),
|
||||||
|
responseTime: z.number().nonnegative().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TYPE DEFINITIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export type CreateTerminalDto = z.infer<typeof CreateTerminalSchema>;
|
||||||
|
export type UpdateTerminalDto = z.infer<typeof UpdateTerminalSchema>;
|
||||||
|
export type TerminalHealthCheckDto = z.infer<typeof TerminalHealthCheckSchema>;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// RESPONSE TYPES (no validation needed)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface TerminalResponseDto {
|
||||||
id: string;
|
id: string;
|
||||||
branchId: string;
|
branchId: string;
|
||||||
terminalProvider: TerminalProvider;
|
terminalProvider: TerminalProvider;
|
||||||
@ -47,3 +72,19 @@ export class TerminalResponseDto {
|
|||||||
lastTransactionAt: Date | null;
|
lastTransactionAt: Date | null;
|
||||||
lastHealthCheckAt: Date | null;
|
lastHealthCheckAt: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// VALIDATION HELPERS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export function validateCreateTerminal(data: unknown): CreateTerminalDto {
|
||||||
|
return CreateTerminalSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUpdateTerminal(data: unknown): UpdateTerminalDto {
|
||||||
|
return UpdateTerminalSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTerminalHealthCheck(data: unknown): TerminalHealthCheckDto {
|
||||||
|
return TerminalHealthCheckSchema.parse(data);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,37 +1,92 @@
|
|||||||
/**
|
/**
|
||||||
* Transaction DTOs
|
* Transaction DTOs
|
||||||
|
* Version: 2.0.0 - With Zod Validation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Local type definitions (based on terminal-payment.entity.ts)
|
import { z } from 'zod';
|
||||||
export type PaymentSourceType = 'sale' | 'service_order' | 'fiado_payment' | 'manual';
|
|
||||||
export type PaymentMethod = 'card' | 'qr' | 'link' | 'cash' | 'bank_transfer';
|
|
||||||
export type PaymentStatus =
|
|
||||||
| 'pending'
|
|
||||||
| 'processing'
|
|
||||||
| 'approved'
|
|
||||||
| 'authorized'
|
|
||||||
| 'in_process'
|
|
||||||
| 'rejected'
|
|
||||||
| 'refunded'
|
|
||||||
| 'partially_refunded'
|
|
||||||
| 'cancelled'
|
|
||||||
| 'charged_back'
|
|
||||||
| 'completed'
|
|
||||||
| 'failed';
|
|
||||||
|
|
||||||
export class ProcessPaymentDto {
|
// ============================================
|
||||||
terminalId: string;
|
// ENUMS
|
||||||
amount: number;
|
// ============================================
|
||||||
currency?: string;
|
|
||||||
tipAmount?: number;
|
|
||||||
sourceType: PaymentSourceType;
|
|
||||||
sourceId: string;
|
|
||||||
description?: string;
|
|
||||||
customerEmail?: string;
|
|
||||||
customerPhone?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PaymentResultDto {
|
export const PaymentSourceTypeEnum = z.enum(['sale', 'service_order', 'fiado_payment', 'manual']);
|
||||||
|
export const PaymentMethodEnum = z.enum(['card', 'qr', 'link', 'cash', 'bank_transfer']);
|
||||||
|
export const PaymentStatusEnum = z.enum([
|
||||||
|
'pending',
|
||||||
|
'processing',
|
||||||
|
'approved',
|
||||||
|
'authorized',
|
||||||
|
'in_process',
|
||||||
|
'rejected',
|
||||||
|
'refunded',
|
||||||
|
'partially_refunded',
|
||||||
|
'cancelled',
|
||||||
|
'charged_back',
|
||||||
|
'completed',
|
||||||
|
'failed',
|
||||||
|
]);
|
||||||
|
export const RefundStatusEnum = z.enum(['pending', 'completed', 'failed']);
|
||||||
|
|
||||||
|
export type PaymentSourceType = z.infer<typeof PaymentSourceTypeEnum>;
|
||||||
|
export type PaymentMethod = z.infer<typeof PaymentMethodEnum>;
|
||||||
|
export type PaymentStatus = z.infer<typeof PaymentStatusEnum>;
|
||||||
|
export type RefundStatus = z.infer<typeof RefundStatusEnum>;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ZOD SCHEMAS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const ProcessPaymentSchema = z.object({
|
||||||
|
terminalId: z.string().uuid('Invalid terminal ID'),
|
||||||
|
amount: z.number().positive('Amount must be positive'),
|
||||||
|
currency: z.string().length(3, 'Currency must be 3 characters (ISO 4217)').default('MXN'),
|
||||||
|
tipAmount: z.number().nonnegative().default(0),
|
||||||
|
sourceType: PaymentSourceTypeEnum,
|
||||||
|
sourceId: z.string().uuid('Invalid source ID'),
|
||||||
|
description: z.string().max(255).optional(),
|
||||||
|
customerEmail: z.string().email('Invalid email').optional(),
|
||||||
|
customerPhone: z.string().max(20).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ProcessRefundSchema = z.object({
|
||||||
|
transactionId: z.string().uuid('Invalid transaction ID'),
|
||||||
|
amount: z.number().positive('Refund amount must be positive').optional(),
|
||||||
|
reason: z.string().max(255).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SendReceiptSchema = z.object({
|
||||||
|
email: z.string().email('Invalid email').optional(),
|
||||||
|
phone: z.string().max(20).optional(),
|
||||||
|
}).refine(data => data.email || data.phone, {
|
||||||
|
message: 'Either email or phone must be provided',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TransactionFilterSchema = z.object({
|
||||||
|
branchId: z.string().uuid().optional(),
|
||||||
|
userId: z.string().uuid().optional(),
|
||||||
|
status: PaymentStatusEnum.optional(),
|
||||||
|
startDate: z.coerce.date().optional(),
|
||||||
|
endDate: z.coerce.date().optional(),
|
||||||
|
sourceType: PaymentSourceTypeEnum.optional(),
|
||||||
|
terminalProvider: z.string().optional(),
|
||||||
|
limit: z.number().int().positive().max(100).default(20),
|
||||||
|
offset: z.number().int().nonnegative().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TYPE DEFINITIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export type ProcessPaymentDto = z.infer<typeof ProcessPaymentSchema>;
|
||||||
|
export type ProcessRefundDto = z.infer<typeof ProcessRefundSchema>;
|
||||||
|
export type SendReceiptDto = z.infer<typeof SendReceiptSchema>;
|
||||||
|
export type TransactionFilterDto = z.infer<typeof TransactionFilterSchema>;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// RESPONSE TYPES (no validation needed)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface PaymentResultDto {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
transactionId?: string;
|
transactionId?: string;
|
||||||
externalTransactionId?: string;
|
externalTransactionId?: string;
|
||||||
@ -48,38 +103,15 @@ export class PaymentResultDto {
|
|||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProcessRefundDto {
|
export interface RefundResultDto {
|
||||||
transactionId: string;
|
|
||||||
amount?: number; // Partial refund if provided
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RefundResultDto {
|
|
||||||
success: boolean;
|
success: boolean;
|
||||||
refundId?: string;
|
refundId?: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
status: 'pending' | 'completed' | 'failed';
|
status: RefundStatus;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SendReceiptDto {
|
export interface TransactionStatsDto {
|
||||||
email?: string;
|
|
||||||
phone?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TransactionFilterDto {
|
|
||||||
branchId?: string;
|
|
||||||
userId?: string;
|
|
||||||
status?: PaymentStatus;
|
|
||||||
startDate?: Date;
|
|
||||||
endDate?: Date;
|
|
||||||
sourceType?: PaymentSourceType;
|
|
||||||
terminalProvider?: string;
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TransactionStatsDto {
|
|
||||||
total: number;
|
total: number;
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
byStatus: Record<PaymentStatus, number>;
|
byStatus: Record<PaymentStatus, number>;
|
||||||
@ -88,3 +120,23 @@ export class TransactionStatsDto {
|
|||||||
averageAmount: number;
|
averageAmount: number;
|
||||||
successRate: number;
|
successRate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// VALIDATION HELPERS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export function validateProcessPayment(data: unknown): ProcessPaymentDto {
|
||||||
|
return ProcessPaymentSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateProcessRefund(data: unknown): ProcessRefundDto {
|
||||||
|
return ProcessRefundSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateSendReceipt(data: unknown): SendReceiptDto {
|
||||||
|
return SendReceiptSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTransactionFilter(data: unknown): TransactionFilterDto {
|
||||||
|
return TransactionFilterSchema.parse(data);
|
||||||
|
}
|
||||||
|
|||||||
330
src/shared/middleware/apiKeyAuth.middleware.ts
Normal file
330
src/shared/middleware/apiKeyAuth.middleware.ts
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
/**
|
||||||
|
* API Key Authentication Middleware
|
||||||
|
* Propagated from erp-core with adaptations for mecanicas-diesel
|
||||||
|
*/
|
||||||
|
import { Response, NextFunction } from 'express';
|
||||||
|
import { AuthenticatedRequest, UnauthorizedError, ForbiddenError } from '../types/index';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import { query } from '../../config/database';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API KEY AUTHENTICATION MIDDLEWARE
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header name for API Key authentication
|
||||||
|
* Supports both X-API-Key and Authorization: ApiKey xxx
|
||||||
|
*/
|
||||||
|
const API_KEY_HEADER = 'x-api-key';
|
||||||
|
const API_KEY_AUTH_PREFIX = 'ApiKey ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Key validation result
|
||||||
|
*/
|
||||||
|
interface ApiKeyValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
error?: string;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
email: string;
|
||||||
|
roles: string[];
|
||||||
|
};
|
||||||
|
apiKey?: {
|
||||||
|
id: string;
|
||||||
|
scope: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract API key from request headers
|
||||||
|
*/
|
||||||
|
function extractApiKey(req: AuthenticatedRequest): string | null {
|
||||||
|
// Check X-API-Key header first
|
||||||
|
const xApiKey = req.headers[API_KEY_HEADER] as string;
|
||||||
|
if (xApiKey) {
|
||||||
|
return xApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Authorization header with ApiKey prefix
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (authHeader && authHeader.startsWith(API_KEY_AUTH_PREFIX)) {
|
||||||
|
return authHeader.substring(API_KEY_AUTH_PREFIX.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client IP address from request
|
||||||
|
*/
|
||||||
|
function getClientIp(req: AuthenticatedRequest): string | undefined {
|
||||||
|
// Check X-Forwarded-For header (for proxies/load balancers)
|
||||||
|
const forwardedFor = req.headers['x-forwarded-for'];
|
||||||
|
if (forwardedFor) {
|
||||||
|
const ips = (forwardedFor as string).split(',');
|
||||||
|
return ips[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check X-Real-IP header
|
||||||
|
const realIp = req.headers['x-real-ip'] as string;
|
||||||
|
if (realIp) {
|
||||||
|
return realIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to socket remote address
|
||||||
|
return req.socket.remoteAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate API key against database
|
||||||
|
* Note: Requires auth.api_keys table to exist
|
||||||
|
*/
|
||||||
|
async function validateApiKey(apiKey: string, clientIp?: string): Promise<ApiKeyValidationResult> {
|
||||||
|
try {
|
||||||
|
// Check if api_keys table exists and validate key
|
||||||
|
const result = await query<{
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
key_hash: string;
|
||||||
|
scope: string | null;
|
||||||
|
allowed_ips: string[] | null;
|
||||||
|
expires_at: Date | null;
|
||||||
|
is_active: boolean;
|
||||||
|
}>(
|
||||||
|
`SELECT ak.id, ak.user_id, ak.key_hash, ak.scope, ak.allowed_ips, ak.expires_at, ak.is_active
|
||||||
|
FROM auth.api_keys ak
|
||||||
|
WHERE ak.key_hash = crypt($1, ak.key_hash)
|
||||||
|
AND ak.is_active = true
|
||||||
|
AND (ak.expires_at IS NULL OR ak.expires_at > NOW())
|
||||||
|
LIMIT 1`,
|
||||||
|
[apiKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return { valid: false, error: 'API key inválida o expirada' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyRecord = result[0];
|
||||||
|
|
||||||
|
// Check IP restrictions
|
||||||
|
if (keyRecord.allowed_ips && keyRecord.allowed_ips.length > 0 && clientIp) {
|
||||||
|
if (!keyRecord.allowed_ips.includes(clientIp)) {
|
||||||
|
return { valid: false, error: 'IP no autorizada para esta API key' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user info
|
||||||
|
const userResult = await query<{
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
email: string;
|
||||||
|
}>(
|
||||||
|
`SELECT id, tenant_id, email FROM auth.users WHERE id = $1 AND status = 'active'`,
|
||||||
|
[keyRecord.user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userResult.length === 0) {
|
||||||
|
return { valid: false, error: 'Usuario asociado a API key no encontrado o inactivo' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userResult[0];
|
||||||
|
|
||||||
|
// Get user roles
|
||||||
|
const rolesResult = await query<{ code: string }>(
|
||||||
|
`SELECT r.code
|
||||||
|
FROM auth.roles r
|
||||||
|
JOIN auth.user_roles ur ON r.id = ur.role_id
|
||||||
|
WHERE ur.user_id = $1`,
|
||||||
|
[user.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const roles = rolesResult.map(r => r.code);
|
||||||
|
|
||||||
|
// Update last_used_at
|
||||||
|
await query(
|
||||||
|
`UPDATE auth.api_keys SET last_used_at = NOW(), last_used_ip = $1 WHERE id = $2`,
|
||||||
|
[clientIp || null, keyRecord.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
tenant_id: user.tenant_id,
|
||||||
|
email: user.email,
|
||||||
|
roles,
|
||||||
|
},
|
||||||
|
apiKey: {
|
||||||
|
id: keyRecord.id,
|
||||||
|
scope: keyRecord.scope,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error validating API key', { error });
|
||||||
|
// If table doesn't exist, return graceful error
|
||||||
|
return { valid: false, error: 'Sistema de API keys no configurado' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate request using API Key
|
||||||
|
* Use this middleware for API endpoints that should accept API Key authentication
|
||||||
|
*/
|
||||||
|
export function authenticateApiKey(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
_res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): void {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const apiKey = extractApiKey(req);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new UnauthorizedError('API key requerida');
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientIp = getClientIp(req);
|
||||||
|
const result = await validateApiKey(apiKey, clientIp);
|
||||||
|
|
||||||
|
if (!result.valid || !result.user) {
|
||||||
|
logger.warn('API key validation failed', {
|
||||||
|
error: result.error,
|
||||||
|
clientIp,
|
||||||
|
});
|
||||||
|
throw new UnauthorizedError(result.error || 'API key inválida');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set user info on request (same format as JWT auth)
|
||||||
|
req.user = {
|
||||||
|
userId: result.user.id,
|
||||||
|
tenantId: result.user.tenant_id,
|
||||||
|
email: result.user.email,
|
||||||
|
role: result.user.roles[0] || 'user',
|
||||||
|
roles: result.user.roles,
|
||||||
|
};
|
||||||
|
req.tenantId = result.user.tenant_id;
|
||||||
|
|
||||||
|
// Mark request as authenticated via API Key (for logging/audit)
|
||||||
|
(req as any).authMethod = 'api_key';
|
||||||
|
(req as any).apiKeyId = result.apiKey?.id;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate request using either JWT or API Key
|
||||||
|
* Use this for endpoints that should accept both authentication methods
|
||||||
|
*/
|
||||||
|
export function authenticateJwtOrApiKey(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): void {
|
||||||
|
const apiKey = extractApiKey(req);
|
||||||
|
const jwtToken = req.headers.authorization?.startsWith('Bearer ');
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
// Use API Key authentication
|
||||||
|
authenticateApiKey(req, res, next);
|
||||||
|
} else if (jwtToken) {
|
||||||
|
// Use JWT authentication - import dynamically to avoid circular deps
|
||||||
|
import('./auth.middleware').then(({ authenticate }) => {
|
||||||
|
authenticate(req, res, next);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next(new UnauthorizedError('Autenticación requerida (JWT o API Key)'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require specific API key scope
|
||||||
|
* Use after authenticateApiKey to enforce scope restrictions
|
||||||
|
*/
|
||||||
|
export function requireApiKeyScope(requiredScope: string) {
|
||||||
|
return (req: AuthenticatedRequest, _res: Response, next: NextFunction): void => {
|
||||||
|
try {
|
||||||
|
const apiKeyId = (req as any).apiKeyId;
|
||||||
|
const authMethod = (req as any).authMethod;
|
||||||
|
|
||||||
|
// Only check scope for API Key auth
|
||||||
|
if (authMethod !== 'api_key') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-validate to get scope (in production, cache this)
|
||||||
|
(async () => {
|
||||||
|
const apiKey = extractApiKey(req);
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new ForbiddenError('API key no encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await validateApiKey(apiKey);
|
||||||
|
if (!result.valid || !result.apiKey) {
|
||||||
|
throw new ForbiddenError('API key inválida');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null scope means full access
|
||||||
|
if (result.apiKey.scope === null) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if scope matches
|
||||||
|
if (result.apiKey.scope !== requiredScope) {
|
||||||
|
logger.warn('API key scope mismatch', {
|
||||||
|
apiKeyId,
|
||||||
|
requiredScope,
|
||||||
|
actualScope: result.apiKey.scope,
|
||||||
|
});
|
||||||
|
throw new ForbiddenError(`API key no tiene el scope requerido: ${requiredScope}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
})();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limiting for API Key requests
|
||||||
|
* Simple in-memory rate limiter - use Redis in production
|
||||||
|
*/
|
||||||
|
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
||||||
|
|
||||||
|
export function apiKeyRateLimit(maxRequests: number = 1000, windowMs: number = 60000) {
|
||||||
|
return (req: AuthenticatedRequest, _res: Response, next: NextFunction): void => {
|
||||||
|
try {
|
||||||
|
const apiKeyId = (req as any).apiKeyId;
|
||||||
|
if (!apiKeyId) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const record = rateLimitStore.get(apiKeyId);
|
||||||
|
|
||||||
|
if (!record || now > record.resetTime) {
|
||||||
|
rateLimitStore.set(apiKeyId, {
|
||||||
|
count: 1,
|
||||||
|
resetTime: now + windowMs,
|
||||||
|
});
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.count >= maxRequests) {
|
||||||
|
logger.warn('API key rate limit exceeded', { apiKeyId, count: record.count });
|
||||||
|
throw new ForbiddenError('Rate limit excedido. Intente más tarde.');
|
||||||
|
}
|
||||||
|
|
||||||
|
record.count++;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -55,3 +55,8 @@ export function authMiddleware(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for authMiddleware (erp-core compatibility)
|
||||||
|
*/
|
||||||
|
export const authenticate = authMiddleware;
|
||||||
|
|||||||
355
src/shared/middleware/fieldPermissions.middleware.ts
Normal file
355
src/shared/middleware/fieldPermissions.middleware.ts
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
/**
|
||||||
|
* Field Permissions Middleware
|
||||||
|
* Propagated from erp-core with adaptations for mecanicas-diesel
|
||||||
|
* Provides field-level access control for API responses and requests
|
||||||
|
*/
|
||||||
|
import { Response, NextFunction } from 'express';
|
||||||
|
import { query } from '../../config/database';
|
||||||
|
import { AuthenticatedRequest } from '../types/index';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface FieldPermission {
|
||||||
|
field_name: string;
|
||||||
|
can_read: boolean;
|
||||||
|
can_write: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelFieldPermissions {
|
||||||
|
model_name: string;
|
||||||
|
fields: Map<string, FieldPermission>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for field permissions per user/model
|
||||||
|
const permissionsCache = new Map<string, { permissions: ModelFieldPermissions; expires: number }>();
|
||||||
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for user/model combination
|
||||||
|
*/
|
||||||
|
function getCacheKey(userId: string, tenantId: string, modelName: string): string {
|
||||||
|
return `${tenantId}:${userId}:${modelName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load field permissions for a user on a specific model
|
||||||
|
* Note: Requires auth.model_fields, auth.field_permissions, auth.user_groups tables
|
||||||
|
*/
|
||||||
|
async function loadFieldPermissions(
|
||||||
|
userId: string,
|
||||||
|
tenantId: string,
|
||||||
|
modelName: string
|
||||||
|
): Promise<ModelFieldPermissions | null> {
|
||||||
|
// Check cache first
|
||||||
|
const cacheKey = getCacheKey(userId, tenantId, modelName);
|
||||||
|
const cached = permissionsCache.get(cacheKey);
|
||||||
|
|
||||||
|
if (cached && cached.expires > Date.now()) {
|
||||||
|
return cached.permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load from database
|
||||||
|
const result = await query<{
|
||||||
|
field_name: string;
|
||||||
|
can_read: boolean;
|
||||||
|
can_write: boolean;
|
||||||
|
}>(
|
||||||
|
`SELECT
|
||||||
|
mf.name as field_name,
|
||||||
|
COALESCE(fp.can_read, true) as can_read,
|
||||||
|
COALESCE(fp.can_write, true) as can_write
|
||||||
|
FROM auth.model_fields mf
|
||||||
|
JOIN auth.models m ON mf.model_id = m.id
|
||||||
|
LEFT JOIN auth.field_permissions fp ON mf.id = fp.field_id
|
||||||
|
LEFT JOIN auth.user_groups ug ON fp.group_id = ug.group_id
|
||||||
|
WHERE m.model = $1
|
||||||
|
AND m.tenant_id = $2
|
||||||
|
AND (ug.user_id = $3 OR fp.group_id IS NULL)
|
||||||
|
GROUP BY mf.name, fp.can_read, fp.can_write`,
|
||||||
|
[modelName, tenantId, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
// No permissions defined = allow all
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions: ModelFieldPermissions = {
|
||||||
|
model_name: modelName,
|
||||||
|
fields: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const row of result) {
|
||||||
|
permissions.fields.set(row.field_name, {
|
||||||
|
field_name: row.field_name,
|
||||||
|
can_read: row.can_read,
|
||||||
|
can_write: row.can_write,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
permissionsCache.set(cacheKey, {
|
||||||
|
permissions,
|
||||||
|
expires: Date.now() + CACHE_TTL,
|
||||||
|
});
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
} catch (error) {
|
||||||
|
// If tables don't exist, allow all (graceful degradation)
|
||||||
|
logger.debug('Field permissions tables not configured, allowing all fields');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter object fields based on read permissions
|
||||||
|
*/
|
||||||
|
function filterReadFields<T extends Record<string, any>>(
|
||||||
|
data: T,
|
||||||
|
permissions: ModelFieldPermissions | null
|
||||||
|
): Partial<T> {
|
||||||
|
// No permissions defined = return all fields
|
||||||
|
if (!permissions || permissions.fields.size === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
const fieldPerm = permissions.fields.get(key);
|
||||||
|
|
||||||
|
// If no permission defined for field, allow it
|
||||||
|
// If permission exists and can_read is true, allow it
|
||||||
|
if (!fieldPerm || fieldPerm.can_read) {
|
||||||
|
filtered[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered as Partial<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter array of objects
|
||||||
|
*/
|
||||||
|
function filterReadFieldsArray<T extends Record<string, any>>(
|
||||||
|
data: T[],
|
||||||
|
permissions: ModelFieldPermissions | null
|
||||||
|
): Partial<T>[] {
|
||||||
|
return data.map(item => filterReadFields(item, permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate write permissions for incoming data
|
||||||
|
*/
|
||||||
|
function validateWriteFields<T extends Record<string, any>>(
|
||||||
|
data: T,
|
||||||
|
permissions: ModelFieldPermissions | null
|
||||||
|
): { valid: boolean; forbiddenFields: string[] } {
|
||||||
|
// No permissions defined = allow all writes
|
||||||
|
if (!permissions || permissions.fields.size === 0) {
|
||||||
|
return { valid: true, forbiddenFields: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const forbiddenFields: string[] = [];
|
||||||
|
|
||||||
|
for (const key of Object.keys(data)) {
|
||||||
|
const fieldPerm = permissions.fields.get(key);
|
||||||
|
|
||||||
|
// If permission exists and can_write is false, it's forbidden
|
||||||
|
if (fieldPerm && !fieldPerm.can_write) {
|
||||||
|
forbiddenFields.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: forbiddenFields.length === 0,
|
||||||
|
forbiddenFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MIDDLEWARE FACTORIES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to filter response fields based on read permissions
|
||||||
|
* Use this on GET endpoints
|
||||||
|
*/
|
||||||
|
export function filterResponseFields(modelName: string) {
|
||||||
|
return async (req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
// Store original json method
|
||||||
|
const originalJson = res.json.bind(res);
|
||||||
|
|
||||||
|
// Override json method to filter fields
|
||||||
|
res.json = function(body: any) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// Only filter for authenticated requests
|
||||||
|
if (!req.user) {
|
||||||
|
return originalJson(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load permissions
|
||||||
|
const permissions = await loadFieldPermissions(
|
||||||
|
req.user.userId,
|
||||||
|
req.user.tenantId,
|
||||||
|
modelName
|
||||||
|
);
|
||||||
|
|
||||||
|
// If no permissions defined or super_admin, return original
|
||||||
|
if (!permissions || req.user.roles?.includes('super_admin')) {
|
||||||
|
return originalJson(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the response
|
||||||
|
if (body && typeof body === 'object') {
|
||||||
|
if (body.data) {
|
||||||
|
if (Array.isArray(body.data)) {
|
||||||
|
body.data = filterReadFieldsArray(body.data, permissions);
|
||||||
|
} else if (typeof body.data === 'object') {
|
||||||
|
body.data = filterReadFields(body.data, permissions);
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(body)) {
|
||||||
|
body = filterReadFieldsArray(body, permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalJson(body);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error filtering response fields', { error, modelName });
|
||||||
|
return originalJson(body);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} as typeof res.json;
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to validate write permissions on incoming data
|
||||||
|
* Use this on POST/PUT/PATCH endpoints
|
||||||
|
*/
|
||||||
|
export function validateWritePermissions(modelName: string) {
|
||||||
|
return async (req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// Skip for unauthenticated requests (they'll fail auth anyway)
|
||||||
|
if (!req.user) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Super admins bypass field permission checks
|
||||||
|
if (req.user.roles?.includes('super_admin')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load permissions
|
||||||
|
const permissions = await loadFieldPermissions(
|
||||||
|
req.user.userId,
|
||||||
|
req.user.tenantId,
|
||||||
|
modelName
|
||||||
|
);
|
||||||
|
|
||||||
|
// No permissions defined = allow all
|
||||||
|
if (!permissions) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate write fields in request body
|
||||||
|
if (req.body && typeof req.body === 'object') {
|
||||||
|
const { valid, forbiddenFields } = validateWriteFields(req.body, permissions);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
logger.warn('Write permission denied for fields', {
|
||||||
|
userId: req.user.userId,
|
||||||
|
modelName,
|
||||||
|
forbiddenFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: `No tiene permisos para modificar los campos: ${forbiddenFields.join(', ')}`,
|
||||||
|
forbiddenFields,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error validating write permissions', { error, modelName });
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combined middleware for both read and write validation
|
||||||
|
*/
|
||||||
|
export function fieldPermissions(modelName: string) {
|
||||||
|
const readFilter = filterResponseFields(modelName);
|
||||||
|
const writeValidator = validateWritePermissions(modelName);
|
||||||
|
|
||||||
|
return async (req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
// For write operations, validate first
|
||||||
|
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
||||||
|
await writeValidator(req, res, () => {
|
||||||
|
// If write validation passed, apply read filter for response
|
||||||
|
readFilter(req, res, next);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For read operations, just apply read filter
|
||||||
|
await readFilter(req, res, next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear permissions cache for a user (call after permission changes)
|
||||||
|
*/
|
||||||
|
export function clearPermissionsCache(userId?: string, tenantId?: string): void {
|
||||||
|
if (userId && tenantId) {
|
||||||
|
// Clear specific user's cache
|
||||||
|
const prefix = `${tenantId}:${userId}:`;
|
||||||
|
for (const key of permissionsCache.keys()) {
|
||||||
|
if (key.startsWith(prefix)) {
|
||||||
|
permissionsCache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear all cache
|
||||||
|
permissionsCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of restricted fields for a user on a model
|
||||||
|
* Useful for frontend to know which fields to hide/disable
|
||||||
|
*/
|
||||||
|
export async function getRestrictedFields(
|
||||||
|
userId: string,
|
||||||
|
tenantId: string,
|
||||||
|
modelName: string
|
||||||
|
): Promise<{ readRestricted: string[]; writeRestricted: string[] }> {
|
||||||
|
const permissions = await loadFieldPermissions(userId, tenantId, modelName);
|
||||||
|
|
||||||
|
const readRestricted: string[] = [];
|
||||||
|
const writeRestricted: string[] = [];
|
||||||
|
|
||||||
|
if (permissions) {
|
||||||
|
for (const [fieldName, perm] of permissions.fields) {
|
||||||
|
if (!perm.can_read) readRestricted.push(fieldName);
|
||||||
|
if (!perm.can_write) writeRestricted.push(fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { readRestricted, writeRestricted };
|
||||||
|
}
|
||||||
@ -5,3 +5,5 @@
|
|||||||
|
|
||||||
export * from './auth.middleware';
|
export * from './auth.middleware';
|
||||||
export * from './rbac.middleware';
|
export * from './rbac.middleware';
|
||||||
|
export * from './apiKeyAuth.middleware';
|
||||||
|
export * from './fieldPermissions.middleware';
|
||||||
|
|||||||
@ -13,18 +13,96 @@ export interface JwtPayload {
|
|||||||
email: string;
|
email: string;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
roles?: string[];
|
||||||
iat?: number;
|
iat?: number;
|
||||||
exp?: number;
|
exp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticated Request
|
* Authenticated Request (legacy alias)
|
||||||
*/
|
*/
|
||||||
export interface AuthRequest extends Request {
|
export interface AuthRequest extends Request {
|
||||||
user?: JwtPayload;
|
user?: JwtPayload;
|
||||||
tenantId?: string;
|
tenantId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticated Request (erp-core compatible)
|
||||||
|
*/
|
||||||
|
export interface AuthenticatedRequest extends Request {
|
||||||
|
user?: JwtPayload;
|
||||||
|
tenantId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ERROR CLASSES
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Application Error
|
||||||
|
*/
|
||||||
|
export class AppError extends Error {
|
||||||
|
constructor(
|
||||||
|
public message: string,
|
||||||
|
public statusCode: number = 500,
|
||||||
|
public code?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'AppError';
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation Error (400)
|
||||||
|
*/
|
||||||
|
export class ValidationError extends AppError {
|
||||||
|
constructor(message: string, public details?: any[]) {
|
||||||
|
super(message, 400, 'VALIDATION_ERROR');
|
||||||
|
this.name = 'ValidationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unauthorized Error (401)
|
||||||
|
*/
|
||||||
|
export class UnauthorizedError extends AppError {
|
||||||
|
constructor(message: string = 'No autorizado') {
|
||||||
|
super(message, 401, 'UNAUTHORIZED');
|
||||||
|
this.name = 'UnauthorizedError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forbidden Error (403)
|
||||||
|
*/
|
||||||
|
export class ForbiddenError extends AppError {
|
||||||
|
constructor(message: string = 'Acceso denegado') {
|
||||||
|
super(message, 403, 'FORBIDDEN');
|
||||||
|
this.name = 'ForbiddenError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not Found Error (404)
|
||||||
|
*/
|
||||||
|
export class NotFoundError extends AppError {
|
||||||
|
constructor(message: string = 'Recurso no encontrado') {
|
||||||
|
super(message, 404, 'NOT_FOUND');
|
||||||
|
this.name = 'NotFoundError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conflict Error (409)
|
||||||
|
*/
|
||||||
|
export class ConflictError extends AppError {
|
||||||
|
constructor(message: string = 'Conflicto con recurso existente') {
|
||||||
|
super(message, 409, 'CONFLICT');
|
||||||
|
this.name = 'ConflictError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard API Response format
|
* Standard API Response format
|
||||||
*/
|
*/
|
||||||
|
|||||||
47
src/shared/utils/logger.ts
Normal file
47
src/shared/utils/logger.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Logger Utility
|
||||||
|
* Winston-based logging for ERP Mecanicas Diesel
|
||||||
|
* Propagated from erp-core
|
||||||
|
*/
|
||||||
|
import winston from 'winston';
|
||||||
|
|
||||||
|
const { combine, timestamp, printf, colorize, errors } = winston.format;
|
||||||
|
|
||||||
|
const logLevel = process.env.LOG_LEVEL || 'info';
|
||||||
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
|
const logFormat = printf(({ level, message, timestamp, ...metadata }) => {
|
||||||
|
let msg = `${timestamp} [${level}]: ${message}`;
|
||||||
|
if (Object.keys(metadata).length > 0) {
|
||||||
|
msg += ` ${JSON.stringify(metadata)}`;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const logger = winston.createLogger({
|
||||||
|
level: logLevel,
|
||||||
|
format: combine(
|
||||||
|
errors({ stack: true }),
|
||||||
|
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
|
logFormat
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: combine(
|
||||||
|
colorize(),
|
||||||
|
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
|
logFormat
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add file transport in production
|
||||||
|
if (nodeEnv === 'production') {
|
||||||
|
logger.add(
|
||||||
|
new winston.transports.File({ filename: 'logs/error.log', level: 'error' })
|
||||||
|
);
|
||||||
|
logger.add(
|
||||||
|
new winston.transports.File({ filename: 'logs/combined.log' })
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user