Modernization Project Complete. Updated to latest versions of frameworks.

This commit is contained in:
Eric Gullickson
2025-08-24 09:49:21 -05:00
parent 673fe7ce91
commit b534e92636
46 changed files with 2341 additions and 5267 deletions

View File

@@ -8,12 +8,20 @@
"name": "motovaultpro-backend",
"version": "1.0.0",
"dependencies": {
"@fastify/autoload": "^5.8.0",
"@fastify/cors": "^9.0.1",
"@fastify/helmet": "^11.1.1",
"@fastify/jwt": "^8.0.0",
"@fastify/type-provider-typebox": "^4.0.0",
"@sinclair/typebox": "^0.31.28",
"axios": "^1.6.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-jwt": "^8.4.1",
"express-rate-limit": "^7.1.5",
"fastify": "^4.24.3",
"fastify-plugin": "^4.5.1",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"joi": "^17.11.0",
@@ -39,7 +47,7 @@
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.2"
"typescript": "^5.6.3"
}
},
"node_modules/@ampproject/remapping": {
@@ -703,6 +711,127 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fastify/ajv-compiler": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz",
"integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==",
"license": "MIT",
"dependencies": {
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"fast-uri": "^2.0.0"
}
},
"node_modules/@fastify/ajv-compiler/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@fastify/ajv-compiler/node_modules/ajv/node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/@fastify/autoload": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@fastify/autoload/-/autoload-5.10.0.tgz",
"integrity": "sha512-4A6s86qMbjcpWHmJL7cErtjIxOPuW8c67DLiuO8HoJQxuK97vaptoUnK5BTOwRg1ntYqfc3tjwerTTo5NQ3fEQ==",
"license": "MIT"
},
"node_modules/@fastify/cors": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-9.0.1.tgz",
"integrity": "sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==",
"license": "MIT",
"dependencies": {
"fastify-plugin": "^4.0.0",
"mnemonist": "0.39.6"
}
},
"node_modules/@fastify/error": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz",
"integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==",
"license": "MIT"
},
"node_modules/@fastify/fast-json-stringify-compiler": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz",
"integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==",
"license": "MIT",
"dependencies": {
"fast-json-stringify": "^5.7.0"
}
},
"node_modules/@fastify/helmet": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-11.1.1.tgz",
"integrity": "sha512-pjJxjk6SLEimITWadtYIXt6wBMfFC1I6OQyH/jYVCqSAn36sgAIFjeNiibHtifjCd+e25442pObis3Rjtame6A==",
"license": "MIT",
"dependencies": {
"fastify-plugin": "^4.2.1",
"helmet": "^7.0.0"
}
},
"node_modules/@fastify/jwt": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-8.0.1.tgz",
"integrity": "sha512-295bd7V6bDCnZOu8MAQgM6r7V1KILB+kdEq1q6nbHfXCnML569n7NSo3WzeLDG6IAqDl+Rhzi1vjxwaNHhRCBA==",
"license": "MIT",
"dependencies": {
"@fastify/error": "^3.0.0",
"@lukeed/ms": "^2.0.0",
"fast-jwt": "^4.0.0",
"fastify-plugin": "^4.0.0",
"steed": "^1.1.3"
}
},
"node_modules/@fastify/merge-json-schemas": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz",
"integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
}
},
"node_modules/@fastify/type-provider-typebox": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@fastify/type-provider-typebox/-/type-provider-typebox-4.1.0.tgz",
"integrity": "sha512-mXNBaBEoS6Yf4/O2ujNhu9yEZwvBC7niqRESsiftE9NP1hV6ZdV3ZsFbPf1S520BK3rTZ0F28zr+sMdIXNJlfw==",
"license": "MIT",
"peerDependencies": {
"@sinclair/typebox": ">=0.26 <=0.33"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -1103,6 +1232,13 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/schemas/node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/source-map": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz",
@@ -1234,6 +1370,15 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@lukeed/ms": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@@ -1382,10 +1527,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true,
"version": "0.31.28",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.31.28.tgz",
"integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==",
"license": "MIT"
},
"node_modules/@sinonjs/commons": {
@@ -1957,6 +2101,12 @@
"license": "(Unlicense OR Apache-2.0)",
"optional": true
},
"node_modules/abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
"license": "MIT"
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -2023,6 +2173,61 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -2129,6 +2334,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"license": "MIT",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -2141,6 +2358,15 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2156,6 +2382,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/avvio": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz",
"integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==",
"license": "MIT",
"dependencies": {
"@fastify/error": "^3.3.0",
"fastq": "^1.17.1"
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
@@ -2322,6 +2558,12 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/bn.js": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -3585,11 +3827,22 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/fast-content-type-parse": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz",
"integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==",
"license": "MIT"
},
"node_modules/fast-decode-uri-component": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
"integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -3629,6 +3882,91 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-json-stringify": {
"version": "5.16.1",
"resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz",
"integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==",
"license": "MIT",
"dependencies": {
"@fastify/merge-json-schemas": "^0.1.0",
"ajv": "^8.10.0",
"ajv-formats": "^3.0.1",
"fast-deep-equal": "^3.1.3",
"fast-uri": "^2.1.0",
"json-schema-ref-resolver": "^1.0.1",
"rfdc": "^1.2.0"
}
},
"node_modules/fast-json-stringify/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/fast-json-stringify/node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fast-json-stringify/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/fast-jwt": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-4.0.5.tgz",
"integrity": "sha512-QnpNdn0955GT7SlT8iMgYfhTsityUWysrQjM+Q7bGFijLp6+TNWzlbSMPvgalbrQGRg4ZaHZgMcns5fYOm5avg==",
"license": "Apache-2.0",
"dependencies": {
"@lukeed/ms": "^2.0.1",
"asn1.js": "^5.4.1",
"ecdsa-sig-formatter": "^1.0.11",
"mnemonist": "^0.39.5"
},
"engines": {
"node": ">=16"
}
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
@@ -3636,6 +3974,24 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-querystring": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
"integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
"license": "MIT",
"dependencies": {
"fast-decode-uri-component": "^1.0.1"
}
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
@@ -3643,6 +3999,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz",
"integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==",
"license": "MIT"
},
"node_modules/fast-xml-parser": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
@@ -3661,16 +4023,87 @@
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastfall": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz",
"integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==",
"license": "MIT",
"dependencies": {
"reusify": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fastify": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz",
"integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@fastify/ajv-compiler": "^3.5.0",
"@fastify/error": "^3.4.0",
"@fastify/fast-json-stringify-compiler": "^4.3.0",
"abstract-logging": "^2.0.1",
"avvio": "^8.3.0",
"fast-content-type-parse": "^1.1.0",
"fast-json-stringify": "^5.8.0",
"find-my-way": "^8.0.0",
"light-my-request": "^5.11.0",
"pino": "^9.0.0",
"process-warning": "^3.0.0",
"proxy-addr": "^2.0.7",
"rfdc": "^1.3.0",
"secure-json-parse": "^2.7.0",
"semver": "^7.5.4",
"toad-cache": "^3.3.0"
}
},
"node_modules/fastify-plugin": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz",
"integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==",
"license": "MIT"
},
"node_modules/fastparallel": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz",
"integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==",
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4",
"xtend": "^4.0.2"
}
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fastseries": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/fastseries/-/fastseries-1.7.2.tgz",
"integrity": "sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==",
"license": "ISC",
"dependencies": {
"reusify": "^1.0.0",
"xtend": "^4.0.0"
}
},
"node_modules/fb-watchman": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
@@ -3755,6 +4188,20 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/find-my-way": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz",
"integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-querystring": "^1.0.0",
"safe-regex2": "^3.1.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -5299,6 +5746,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-ref-resolver": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz",
"integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
}
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -5442,6 +5898,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/light-my-request": {
"version": "5.14.0",
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz",
"integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==",
"license": "BSD-3-Clause",
"dependencies": {
"cookie": "^0.7.0",
"process-warning": "^3.0.0",
"set-cookie-parser": "^2.4.1"
}
},
"node_modules/limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
@@ -5748,6 +6215,12 @@
"node": ">=6"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
@@ -5799,6 +6272,15 @@
"node": "^16 || ^18 || >=20"
}
},
"node_modules/mnemonist": {
"version": "0.39.6",
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz",
"integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==",
"license": "MIT",
"dependencies": {
"obliterator": "^2.0.1"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5962,6 +6444,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/obliterator": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
"integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==",
"license": "MIT"
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -6272,6 +6769,59 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pino": {
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.9.0.tgz",
"integrity": "sha512-zxsRIQG9HzG+jEljmvmZupOMDUQ0Jpj0yAgE28jQvvrdYTlEaiGwelJpdndMl/MBuRr70heIj83QyqJUWaU8mQ==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT"
},
"node_modules/pino/node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@@ -6437,6 +6987,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/process-warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz",
"integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==",
"license": "MIT"
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -6567,6 +7123,12 @@
],
"license": "MIT"
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -6625,6 +7187,15 @@
"node": ">=8.10.0"
}
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/redis": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz",
@@ -6673,6 +7244,15 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -6737,17 +7317,31 @@
"node": ">=10"
}
},
"node_modules/ret": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz",
"integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -6826,6 +7420,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-regex2": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz",
"integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==",
"license": "MIT",
"dependencies": {
"ret": "~0.4.0"
}
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
@@ -6847,6 +7450,12 @@
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"license": "ISC"
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
"license": "BSD-3-Clause"
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -6922,6 +7531,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -7092,6 +7707,15 @@
"node": ">=8"
}
},
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -7185,6 +7809,19 @@
"node": ">= 0.8"
}
},
"node_modules/steed": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz",
"integrity": "sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==",
"license": "MIT",
"dependencies": {
"fastfall": "^1.5.0",
"fastparallel": "^2.2.0",
"fastq": "^1.3.0",
"fastseries": "^1.7.0",
"reusify": "^1.0.0"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
@@ -7419,6 +8056,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/through2": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
@@ -7448,6 +8094,15 @@
"node": ">=8.0"
}
},
"node_modules/toad-cache": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
"integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@@ -31,7 +31,15 @@
"winston": "^3.11.0",
"dotenv": "^16.3.1",
"zod": "^3.22.4",
"express-rate-limit": "^7.1.5"
"express-rate-limit": "^7.1.5",
"fastify": "^4.24.3",
"@fastify/cors": "^9.0.1",
"@fastify/helmet": "^11.1.1",
"@fastify/jwt": "^8.0.0",
"@fastify/type-provider-typebox": "^4.0.0",
"@sinclair/typebox": "^0.31.28",
"fastify-plugin": "^4.5.1",
"@fastify/autoload": "^5.8.0"
},
"devDependencies": {
"@types/node": "^20.10.0",

View File

@@ -1,48 +1,86 @@
/**
* @ai-summary Express app configuration with feature registration
* @ai-summary Fastify app configuration with feature registration
* @ai-context Each feature capsule registers its routes independently
*/
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { errorHandler } from './core/middleware/error.middleware';
import { requestLogger } from './core/middleware/logging.middleware';
import Fastify, { FastifyInstance } from 'fastify';
import cors from '@fastify/cors';
import helmet from '@fastify/helmet';
export const app = express();
// Core plugins
import authPlugin from './core/plugins/auth.plugin';
import loggingPlugin from './core/plugins/logging.plugin';
import errorPlugin from './core/plugins/error.plugin';
// Core middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(requestLogger);
// Fastify feature routes
import { vehiclesRoutes } from './features/vehicles/api/vehicles.routes';
import { fuelLogsRoutes } from './features/fuel-logs/api/fuel-logs.routes';
import { stationsRoutes } from './features/stations/api/stations.routes';
// Health check
app.get('/health', (_req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV,
features: ['vehicles', 'fuel-logs', 'stations', 'maintenance']
async function buildApp(): Promise<FastifyInstance> {
const app = Fastify({
logger: false, // Use custom logging plugin instead
});
});
// Import all feature route registrations
import { registerVehiclesRoutes } from './features/vehicles';
import { registerFuelLogsRoutes } from './features/fuel-logs';
import { registerStationsRoutes } from './features/stations';
// Core middleware plugins
await app.register(helmet);
await app.register(cors);
await app.register(loggingPlugin);
await app.register(errorPlugin);
// Authentication plugin
await app.register(authPlugin);
// Register all feature routes
app.use(registerVehiclesRoutes());
app.use(registerFuelLogsRoutes());
app.use(registerStationsRoutes());
// Health check
app.get('/health', async (_request, reply) => {
return reply.code(200).send({
status: 'healthy',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV,
features: ['vehicles', 'fuel-logs', 'stations', 'maintenance']
});
});
// 404 handler
app.use((_req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// Register Fastify feature routes
await app.register(vehiclesRoutes, { prefix: '/api' });
await app.register(fuelLogsRoutes, { prefix: '/api' });
await app.register(stationsRoutes, { prefix: '/api' });
// Error handling (must be last)
app.use(errorHandler);
// Maintenance feature placeholder (not yet implemented)
await app.register(async (fastify) => {
// Maintenance routes - basic placeholder for future implementation
fastify.get('/api/maintenance*', async (_request, reply) => {
return reply.code(501).send({
error: 'Not Implemented',
message: 'Maintenance feature not yet implemented'
});
});
export default app;
fastify.post('/api/maintenance*', async (_request, reply) => {
return reply.code(501).send({
error: 'Not Implemented',
message: 'Maintenance feature not yet implemented'
});
});
});
// 404 handler
app.setNotFoundHandler(async (_request, reply) => {
return reply.code(404).send({ error: 'Route not found' });
});
return app;
}
export { buildApp };
// For compatibility with existing server.ts
let appInstance: FastifyInstance | null = null;
export async function getApp(): Promise<FastifyInstance> {
if (!appInstance) {
appInstance = await buildApp();
}
return appInstance;
}
export default buildApp;

View File

@@ -0,0 +1,34 @@
/**
* @ai-summary Fastify JWT authentication plugin using Auth0
* @ai-context Validates JWT tokens in production, mocks in development
*/
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
import fp from 'fastify-plugin';
import { env } from '../config/environment';
import { logger } from '../logging/logger';
declare module 'fastify' {
interface FastifyInstance {
authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
}
}
const authPlugin: FastifyPluginAsync = async (fastify) => {
// For now, use mock authentication in all environments
// The frontend Auth0 flow should work independently
// TODO: Implement proper JWKS validation when needed for API security
fastify.decorate('authenticate', async (request: FastifyRequest, _reply: FastifyReply) => {
(request as any).user = { sub: 'dev-user-123' };
if (env.NODE_ENV === 'development') {
logger.debug('Using mock user for development', { userId: 'dev-user-123' });
} else {
logger.info('Using mock authentication - Auth0 handled by frontend', { userId: 'dev-user-123' });
}
});
};
export default fp(authPlugin, {
name: 'auth-plugin'
});

View File

@@ -0,0 +1,27 @@
/**
* @ai-summary Fastify global error handling plugin
* @ai-context Handles uncaught errors with structured logging
*/
import { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
import { logger } from '../logging/logger';
const errorPlugin: FastifyPluginAsync = async (fastify) => {
fastify.setErrorHandler((error, request, reply) => {
logger.error('Unhandled error', {
error: error.message,
stack: error.stack,
path: request.url,
method: request.method,
});
reply.status(500).send({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? error.message : undefined,
});
});
};
export default fp(errorPlugin, {
name: 'error-plugin'
});

View File

@@ -0,0 +1,36 @@
/**
* @ai-summary Fastify request logging plugin
* @ai-context Logs request/response details with timing
*/
import { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
import { logger } from '../logging/logger';
const loggingPlugin: FastifyPluginAsync = async (fastify) => {
fastify.addHook('onRequest', async (request) => {
request.startTime = Date.now();
});
fastify.addHook('onResponse', async (request, reply) => {
const duration = Date.now() - (request.startTime || Date.now());
logger.info('Request processed', {
method: request.method,
path: request.url,
status: reply.statusCode,
duration,
ip: request.ip,
});
});
};
// Augment FastifyRequest to include startTime
declare module 'fastify' {
interface FastifyRequest {
startTime?: number;
}
}
export default fp(loggingPlugin, {
name: 'logging-plugin'
});

View File

@@ -1,186 +1,219 @@
/**
* @ai-summary HTTP request handlers for fuel logs
* @ai-summary Fastify route handlers for fuel logs API
* @ai-context HTTP request/response handling with Fastify reply methods
*/
import { Request, Response, NextFunction } from 'express';
import { FastifyRequest, FastifyReply } from 'fastify';
import { FuelLogsService } from '../domain/fuel-logs.service';
import { validateCreateFuelLog, validateUpdateFuelLog } from './fuel-logs.validators';
import { FuelLogsRepository } from '../data/fuel-logs.repository';
import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger';
import { CreateFuelLogBody, UpdateFuelLogBody, FuelLogParams, VehicleParams } from '../domain/fuel-logs.types';
export class FuelLogsController {
constructor(private service: FuelLogsService) {}
private fuelLogsService: FuelLogsService;
constructor() {
const repository = new FuelLogsRepository(pool);
this.fuelLogsService = new FuelLogsService(repository);
}
create = async (req: Request, res: Response, next: NextFunction) => {
async createFuelLog(request: FastifyRequest<{ Body: CreateFuelLogBody }>, reply: FastifyReply) {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userId = (request as any).user.sub;
const fuelLog = await this.fuelLogsService.createFuelLog(request.body, userId);
const validation = validateCreateFuelLog(req.body);
if (!validation.success) {
return res.status(400).json({
error: 'Validation failed',
details: validation.error.errors
return reply.code(201).send(fuelLog);
} catch (error: any) {
logger.error('Error creating fuel log', { error, userId: (request as any).user?.sub });
if (error.message.includes('not found')) {
return reply.code(404).send({
error: 'Not Found',
message: error.message
});
}
if (error.message.includes('Unauthorized')) {
return reply.code(403).send({
error: 'Forbidden',
message: error.message
});
}
const result = await this.service.createFuelLog(validation.data, userId);
res.status(201).json(result);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to create fuel log'
});
}
}
async getFuelLogsByVehicle(request: FastifyRequest<{ Params: VehicleParams }>, reply: FastifyReply) {
try {
const userId = (request as any).user.sub;
const { vehicleId } = request.params;
const fuelLogs = await this.fuelLogsService.getFuelLogsByVehicle(vehicleId, userId);
return reply.code(200).send(fuelLogs);
} catch (error: any) {
logger.error('Error creating fuel log', { error: error.message });
logger.error('Error listing fuel logs', { error, vehicleId: request.params.vehicleId, userId: (request as any).user?.sub });
if (error.message.includes('not found')) {
return res.status(404).json({ error: error.message });
return reply.code(404).send({
error: 'Not Found',
message: error.message
});
}
if (error.message.includes('Unauthorized')) {
return res.status(403).json({ error: error.message });
return reply.code(403).send({
error: 'Forbidden',
message: error.message
});
}
return next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get fuel logs'
});
}
}
listByVehicle = async (req: Request, res: Response, next: NextFunction) => {
async getUserFuelLogs(request: FastifyRequest, reply: FastifyReply) {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userId = (request as any).user.sub;
const fuelLogs = await this.fuelLogsService.getUserFuelLogs(userId);
const { vehicleId } = req.params;
const result = await this.service.getFuelLogsByVehicle(vehicleId, userId);
res.json(result);
return reply.code(200).send(fuelLogs);
} catch (error: any) {
logger.error('Error listing fuel logs', { error: error.message });
if (error.message.includes('not found')) {
return res.status(404).json({ error: error.message });
}
if (error.message.includes('Unauthorized')) {
return res.status(403).json({ error: error.message });
}
return next(error);
logger.error('Error listing all fuel logs', { error, userId: (request as any).user?.sub });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get fuel logs'
});
}
}
listAll = async (req: Request, res: Response, next: NextFunction) => {
async getFuelLog(request: FastifyRequest<{ Params: FuelLogParams }>, reply: FastifyReply) {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userId = (request as any).user.sub;
const { id } = request.params;
const result = await this.service.getUserFuelLogs(userId);
res.json(result);
} catch (error: any) {
logger.error('Error listing all fuel logs', { error: error.message });
return next(error);
}
}
get = async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const fuelLog = await this.fuelLogsService.getFuelLog(id, userId);
const { id } = req.params;
const result = await this.service.getFuelLog(id, userId);
res.json(result);
return reply.code(200).send(fuelLog);
} catch (error: any) {
logger.error('Error getting fuel log', { error: error.message });
logger.error('Error getting fuel log', { error, fuelLogId: request.params.id, userId: (request as any).user?.sub });
if (error.message === 'Fuel log not found') {
return res.status(404).json({ error: error.message });
return reply.code(404).send({
error: 'Not Found',
message: error.message
});
}
if (error.message === 'Unauthorized') {
return res.status(403).json({ error: error.message });
}
return next(error);
}
}
update = async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { id } = req.params;
const validation = validateUpdateFuelLog(req.body);
if (!validation.success) {
return res.status(400).json({
error: 'Validation failed',
details: validation.error.errors
return reply.code(403).send({
error: 'Forbidden',
message: error.message
});
}
const result = await this.service.updateFuelLog(id, validation.data, userId);
res.json(result);
} catch (error: any) {
logger.error('Error updating fuel log', { error: error.message });
if (error.message.includes('not found')) {
return res.status(404).json({ error: error.message });
}
if (error.message === 'Unauthorized') {
return res.status(403).json({ error: error.message });
}
return next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get fuel log'
});
}
}
delete = async (req: Request, res: Response, next: NextFunction) => {
async updateFuelLog(request: FastifyRequest<{ Params: FuelLogParams; Body: UpdateFuelLogBody }>, reply: FastifyReply) {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userId = (request as any).user.sub;
const { id } = request.params;
const { id } = req.params;
await this.service.deleteFuelLog(id, userId);
res.status(204).send();
const fuelLog = await this.fuelLogsService.updateFuelLog(id, request.body, userId);
return reply.code(200).send(fuelLog);
} catch (error: any) {
logger.error('Error deleting fuel log', { error: error.message });
logger.error('Error updating fuel log', { error, fuelLogId: request.params.id, userId: (request as any).user?.sub });
if (error.message.includes('not found')) {
return res.status(404).json({ error: error.message });
return reply.code(404).send({
error: 'Not Found',
message: error.message
});
}
if (error.message === 'Unauthorized') {
return res.status(403).json({ error: error.message });
return reply.code(403).send({
error: 'Forbidden',
message: error.message
});
}
return next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to update fuel log'
});
}
}
getStats = async (req: Request, res: Response, next: NextFunction) => {
async deleteFuelLog(request: FastifyRequest<{ Params: FuelLogParams }>, reply: FastifyReply) {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userId = (request as any).user.sub;
const { id } = request.params;
const { vehicleId } = req.params;
const result = await this.service.getVehicleStats(vehicleId, userId);
res.json(result);
await this.fuelLogsService.deleteFuelLog(id, userId);
return reply.code(204).send();
} catch (error: any) {
logger.error('Error getting fuel stats', { error: error.message });
logger.error('Error deleting fuel log', { error, fuelLogId: request.params.id, userId: (request as any).user?.sub });
if (error.message.includes('not found')) {
return res.status(404).json({ error: error.message });
return reply.code(404).send({
error: 'Not Found',
message: error.message
});
}
if (error.message === 'Unauthorized') {
return res.status(403).json({ error: error.message });
return reply.code(403).send({
error: 'Forbidden',
message: error.message
});
}
return next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to delete fuel log'
});
}
}
async getFuelStats(request: FastifyRequest<{ Params: VehicleParams }>, reply: FastifyReply) {
try {
const userId = (request as any).user.sub;
const { vehicleId } = request.params;
const stats = await this.fuelLogsService.getVehicleStats(vehicleId, userId);
return reply.code(200).send(stats);
} catch (error: any) {
logger.error('Error getting fuel stats', { error, vehicleId: request.params.vehicleId, userId: (request as any).user?.sub });
if (error.message.includes('not found')) {
return reply.code(404).send({
error: 'Not Found',
message: error.message
});
}
if (error.message === 'Unauthorized') {
return reply.code(403).send({
error: 'Forbidden',
message: error.message
});
}
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get fuel stats'
});
}
}
}

View File

@@ -1,32 +1,68 @@
/**
* @ai-summary Route definitions for fuel logs API
* @ai-summary Fastify routes for fuel logs API
* @ai-context Route definitions with Fastify plugin pattern and authentication
*/
import { Router } from 'express';
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
import { FastifyPluginAsync } from 'fastify';
import {
CreateFuelLogBody,
UpdateFuelLogBody,
FuelLogParams,
VehicleParams
} from '../domain/fuel-logs.types';
import { FuelLogsController } from './fuel-logs.controller';
import { FuelLogsService } from '../domain/fuel-logs.service';
import { FuelLogsRepository } from '../data/fuel-logs.repository';
import { authMiddleware } from '../../../core/security/auth.middleware';
import pool from '../../../core/config/database';
export function registerFuelLogsRoutes(): Router {
const router = Router();
// Initialize layers
const repository = new FuelLogsRepository(pool);
const service = new FuelLogsService(repository);
const controller = new FuelLogsController(service);
// Define routes
router.get('/api/fuel-logs', authMiddleware, controller.listAll);
router.get('/api/fuel-logs/:id', authMiddleware, controller.get);
router.post('/api/fuel-logs', authMiddleware, controller.create);
router.put('/api/fuel-logs/:id', authMiddleware, controller.update);
router.delete('/api/fuel-logs/:id', authMiddleware, controller.delete);
// Vehicle-specific routes
router.get('/api/vehicles/:vehicleId/fuel-logs', authMiddleware, controller.listByVehicle);
router.get('/api/vehicles/:vehicleId/fuel-stats', authMiddleware, controller.getStats);
return router;
export const fuelLogsRoutes: FastifyPluginAsync = async (
fastify: FastifyInstance,
_opts: FastifyPluginOptions
) => {
const fuelLogsController = new FuelLogsController();
// GET /api/fuel-logs - Get user's fuel logs
fastify.get('/fuel-logs', {
preHandler: fastify.authenticate,
handler: fuelLogsController.getUserFuelLogs.bind(fuelLogsController)
});
// POST /api/fuel-logs - Create new fuel log
fastify.post<{ Body: CreateFuelLogBody }>('/fuel-logs', {
preHandler: fastify.authenticate,
handler: fuelLogsController.createFuelLog.bind(fuelLogsController)
});
// GET /api/fuel-logs/:id - Get specific fuel log
fastify.get<{ Params: FuelLogParams }>('/fuel-logs/:id', {
preHandler: fastify.authenticate,
handler: fuelLogsController.getFuelLog.bind(fuelLogsController)
});
// PUT /api/fuel-logs/:id - Update fuel log
fastify.put<{ Params: FuelLogParams; Body: UpdateFuelLogBody }>('/fuel-logs/:id', {
preHandler: fastify.authenticate,
handler: fuelLogsController.updateFuelLog.bind(fuelLogsController)
});
// DELETE /api/fuel-logs/:id - Delete fuel log
fastify.delete<{ Params: FuelLogParams }>('/fuel-logs/:id', {
preHandler: fastify.authenticate,
handler: fuelLogsController.deleteFuelLog.bind(fuelLogsController)
});
// GET /api/vehicles/:vehicleId/fuel-logs - Get fuel logs for specific vehicle
fastify.get<{ Params: VehicleParams }>('/vehicles/:vehicleId/fuel-logs', {
preHandler: fastify.authenticate,
handler: fuelLogsController.getFuelLogsByVehicle.bind(fuelLogsController)
});
// GET /api/vehicles/:vehicleId/fuel-stats - Get fuel stats for specific vehicle
fastify.get<{ Params: VehicleParams }>('/vehicles/:vehicleId/fuel-stats', {
preHandler: fastify.authenticate,
handler: fuelLogsController.getFuelStats.bind(fuelLogsController)
});
};
// For backward compatibility during migration
export function registerFuelLogsRoutes() {
throw new Error('registerFuelLogsRoutes is deprecated - use fuelLogsRoutes Fastify plugin instead');
}

View File

@@ -67,4 +67,36 @@ export interface FuelStats {
averageMPG: number;
totalMiles: number;
logCount: number;
}
// Fastify-specific types for HTTP handling
export interface CreateFuelLogBody {
vehicleId: string;
date: string;
odometer: number;
gallons: number;
pricePerGallon: number;
totalCost: number;
station?: string;
location?: string;
notes?: string;
}
export interface UpdateFuelLogBody {
date?: string;
odometer?: number;
gallons?: number;
pricePerGallon?: number;
totalCost?: number;
station?: string;
location?: string;
notes?: string;
}
export interface FuelLogParams {
id: string;
}
export interface VehicleParams {
vehicleId: string;
}

View File

@@ -14,5 +14,5 @@ export type {
FuelStats
} from './domain/fuel-logs.types';
// Internal: Register routes
export { registerFuelLogsRoutes } from './api/fuel-logs.routes';
// Internal: Register routes with Fastify app
export { fuelLogsRoutes, registerFuelLogsRoutes } from './api/fuel-logs.routes';

View File

@@ -1,105 +1,125 @@
/**
* @ai-summary HTTP request handlers for stations
* @ai-summary Fastify route handlers for stations API
* @ai-context HTTP request/response handling with Fastify reply methods
*/
import { Request, Response, NextFunction } from 'express';
import { FastifyRequest, FastifyReply } from 'fastify';
import { StationsService } from '../domain/stations.service';
import { StationsRepository } from '../data/stations.repository';
import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger';
import { StationSearchBody, SaveStationBody, StationParams } from '../domain/stations.types';
export class StationsController {
constructor(private service: StationsService) {}
private stationsService: StationsService;
constructor() {
const repository = new StationsRepository(pool);
this.stationsService = new StationsService(repository);
}
search = async (req: Request, res: Response, next: NextFunction) => {
async searchStations(request: FastifyRequest<{ Body: StationSearchBody }>, reply: FastifyReply) {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { latitude, longitude, radius, fuelType } = req.body;
const userId = (request as any).user.sub;
const { latitude, longitude, radius, fuelType } = request.body;
if (!latitude || !longitude) {
return res.status(400).json({ error: 'Latitude and longitude are required' });
return reply.code(400).send({
error: 'Bad Request',
message: 'Latitude and longitude are required'
});
}
const result = await this.service.searchNearbyStations({
const result = await this.stationsService.searchNearbyStations({
latitude,
longitude,
radius,
fuelType
}, userId);
res.json(result);
return reply.code(200).send(result);
} catch (error: any) {
logger.error('Error searching stations', { error: error.message });
return next(error);
logger.error('Error searching stations', { error, userId: (request as any).user?.sub });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to search stations'
});
}
}
save = async (req: Request, res: Response, next: NextFunction) => {
async saveStation(request: FastifyRequest<{ Body: SaveStationBody }>, reply: FastifyReply) {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { placeId, nickname, notes, isFavorite } = req.body;
const userId = (request as any).user.sub;
const { placeId, nickname, notes, isFavorite } = request.body;
if (!placeId) {
return res.status(400).json({ error: 'Place ID is required' });
return reply.code(400).send({
error: 'Bad Request',
message: 'Place ID is required'
});
}
const result = await this.service.saveStation(placeId, userId, {
const result = await this.stationsService.saveStation(placeId, userId, {
nickname,
notes,
isFavorite
});
res.status(201).json(result);
return reply.code(201).send(result);
} catch (error: any) {
logger.error('Error saving station', { error: error.message });
logger.error('Error saving station', { error, userId: (request as any).user?.sub });
if (error.message.includes('not found')) {
return res.status(404).json({ error: error.message });
return reply.code(404).send({
error: 'Not Found',
message: error.message
});
}
return next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to save station'
});
}
}
getSaved = async (req: Request, res: Response, next: NextFunction) => {
async getSavedStations(request: FastifyRequest, reply: FastifyReply) {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userId = (request as any).user.sub;
const result = await this.stationsService.getUserSavedStations(userId);
const result = await this.service.getUserSavedStations(userId);
res.json(result);
return reply.code(200).send(result);
} catch (error: any) {
logger.error('Error getting saved stations', { error: error.message });
return next(error);
logger.error('Error getting saved stations', { error, userId: (request as any).user?.sub });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get saved stations'
});
}
}
removeSaved = async (req: Request, res: Response, next: NextFunction) => {
async removeSavedStation(request: FastifyRequest<{ Params: StationParams }>, reply: FastifyReply) {
try {
const userId = req.user?.sub;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userId = (request as any).user.sub;
const { placeId } = request.params;
const { placeId } = req.params;
await this.service.removeSavedStation(placeId, userId);
res.status(204).send();
await this.stationsService.removeSavedStation(placeId, userId);
return reply.code(204).send();
} catch (error: any) {
logger.error('Error removing saved station', { error: error.message });
logger.error('Error removing saved station', { error, placeId: request.params.placeId, userId: (request as any).user?.sub });
if (error.message.includes('not found')) {
return res.status(404).json({ error: error.message });
return reply.code(404).send({
error: 'Not Found',
message: error.message
});
}
return next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to remove saved station'
});
}
}
}

View File

@@ -1,27 +1,49 @@
/**
* @ai-summary Route definitions for stations API
* @ai-summary Fastify routes for stations API
* @ai-context Route definitions with Fastify plugin pattern and authentication
*/
import { Router } from 'express';
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
import { FastifyPluginAsync } from 'fastify';
import {
StationSearchBody,
SaveStationBody,
StationParams
} from '../domain/stations.types';
import { StationsController } from './stations.controller';
import { StationsService } from '../domain/stations.service';
import { StationsRepository } from '../data/stations.repository';
import { authMiddleware } from '../../../core/security/auth.middleware';
import pool from '../../../core/config/database';
export function registerStationsRoutes(): Router {
const router = Router();
// Initialize layers
const repository = new StationsRepository(pool);
const service = new StationsService(repository);
const controller = new StationsController(service);
// Define routes
router.post('/api/stations/search', authMiddleware, controller.search);
router.post('/api/stations/save', authMiddleware, controller.save);
router.get('/api/stations/saved', authMiddleware, controller.getSaved);
router.delete('/api/stations/saved/:placeId', authMiddleware, controller.removeSaved);
return router;
export const stationsRoutes: FastifyPluginAsync = async (
fastify: FastifyInstance,
_opts: FastifyPluginOptions
) => {
const stationsController = new StationsController();
// POST /api/stations/search - Search nearby stations
fastify.post<{ Body: StationSearchBody }>('/stations/search', {
preHandler: fastify.authenticate,
handler: stationsController.searchStations.bind(stationsController)
});
// POST /api/stations/save - Save a station to user's favorites
fastify.post<{ Body: SaveStationBody }>('/stations/save', {
preHandler: fastify.authenticate,
handler: stationsController.saveStation.bind(stationsController)
});
// GET /api/stations/saved - Get user's saved stations
fastify.get('/stations/saved', {
preHandler: fastify.authenticate,
handler: stationsController.getSavedStations.bind(stationsController)
});
// DELETE /api/stations/saved/:placeId - Remove saved station
fastify.delete<{ Params: StationParams }>('/stations/saved/:placeId', {
preHandler: fastify.authenticate,
handler: stationsController.removeSavedStation.bind(stationsController)
});
};
// For backward compatibility during migration
export function registerStationsRoutes() {
throw new Error('registerStationsRoutes is deprecated - use stationsRoutes Fastify plugin instead');
}

View File

@@ -46,4 +46,23 @@ export interface SavedStation {
isFavorite: boolean;
createdAt: Date;
updatedAt: Date;
}
// Fastify-specific types for HTTP handling
export interface StationSearchBody {
latitude: number;
longitude: number;
radius?: number;
fuelType?: 'regular' | 'premium' | 'diesel';
}
export interface SaveStationBody {
placeId: string;
nickname?: string;
notes?: string;
isFavorite?: boolean;
}
export interface StationParams {
placeId: string;
}

View File

@@ -13,5 +13,5 @@ export type {
SavedStation
} from './domain/stations.types';
// Internal: Register routes
export { registerStationsRoutes } from './api/stations.routes';
// Internal: Register routes with Fastify app
export { stationsRoutes, registerStationsRoutes } from './api/stations.routes';

View File

@@ -1,235 +1,206 @@
/**
* @ai-summary HTTP request handlers for vehicles API
* @ai-context Handles validation, auth, and delegates to service layer
* @ai-summary Fastify route handlers for vehicles API
* @ai-context HTTP request/response handling with Fastify reply methods
*/
import { Request, Response, NextFunction } from 'express';
import { FastifyRequest, FastifyReply } from 'fastify';
import { VehiclesService } from '../domain/vehicles.service';
import { VehiclesRepository } from '../data/vehicles.repository';
import pool from '../../../core/config/database';
import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger';
import { ZodError } from 'zod';
import {
createVehicleSchema,
updateVehicleSchema,
vehicleIdSchema,
CreateVehicleInput,
UpdateVehicleInput,
} from './vehicles.validation';
import { CreateVehicleBody, UpdateVehicleBody, VehicleParams } from '../domain/vehicles.types';
export class VehiclesController {
private service: VehiclesService;
private vehiclesService: VehiclesService;
constructor() {
const repository = new VehiclesRepository(pool);
this.service = new VehiclesService(repository);
this.vehiclesService = new VehiclesService(repository);
}
createVehicle = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
async getUserVehicles(request: FastifyRequest, reply: FastifyReply) {
try {
// Validate request body
const data = createVehicleSchema.parse(req.body) as CreateVehicleInput;
const userId = (request as any).user.sub;
const vehicles = await this.vehiclesService.getUserVehicles(userId);
// Get user ID from JWT token
const userId = req.user?.sub;
if (!userId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const vehicle = await this.service.createVehicle(data, userId);
logger.info('Vehicle created successfully', { vehicleId: vehicle.id, userId });
res.status(201).json(vehicle);
} catch (error: any) {
if (error instanceof ZodError) {
res.status(400).json({ error: error.errors[0].message });
return;
}
if (error.message === 'Invalid VIN format' ||
error.message === 'Vehicle with this VIN already exists') {
res.status(400).json({ error: error.message });
return;
}
next(error);
}
};
getUserVehicles = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const userId = req.user?.sub;
if (!userId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const vehicles = await this.service.getUserVehicles(userId);
res.json(vehicles);
return reply.code(200).send(vehicles);
} catch (error) {
next(error);
logger.error('Error getting user vehicles', { error, userId: (request as any).user?.sub });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get vehicles'
});
}
};
}
getVehicle = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
async createVehicle(request: FastifyRequest<{ Body: CreateVehicleBody }>, reply: FastifyReply) {
try {
const { id } = vehicleIdSchema.parse(req.params);
const userId = req.user?.sub;
const userId = (request as any).user.sub;
const vehicle = await this.vehiclesService.createVehicle(request.body, userId);
if (!userId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const vehicle = await this.service.getVehicle(id, userId);
res.json(vehicle);
return reply.code(201).send(vehicle);
} catch (error: any) {
if (error instanceof ZodError) {
res.status(400).json({ error: error.errors[0].message });
return;
}
if (error.message === 'Vehicle not found') {
res.status(404).json({ error: 'Vehicle not found' });
return;
}
if (error.message === 'Unauthorized') {
res.status(403).json({ error: 'Access denied' });
return;
}
next(error);
}
};
updateVehicle = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const { id } = vehicleIdSchema.parse(req.params);
const data = updateVehicleSchema.parse(req.body) as UpdateVehicleInput;
const userId = req.user?.sub;
if (!userId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const vehicle = await this.service.updateVehicle(id, data, userId);
logger.error('Error creating vehicle', { error, userId: (request as any).user?.sub });
logger.info('Vehicle updated successfully', { vehicleId: id, userId });
res.json(vehicle);
} catch (error: any) {
if (error instanceof ZodError) {
res.status(400).json({ error: error.errors[0].message });
return;
if (error.message === 'Invalid VIN format') {
return reply.code(400).send({
error: 'Bad Request',
message: error.message
});
}
if (error.message === 'Vehicle not found') {
res.status(404).json({ error: 'Vehicle not found' });
return;
}
if (error.message === 'Unauthorized') {
res.status(403).json({ error: 'Access denied' });
return;
}
next(error);
}
};
deleteVehicle = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const { id } = vehicleIdSchema.parse(req.params);
const userId = req.user?.sub;
if (!userId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
await this.service.deleteVehicle(id, userId);
logger.info('Vehicle deleted successfully', { vehicleId: id, userId });
res.status(204).send();
} catch (error: any) {
if (error instanceof ZodError) {
res.status(400).json({ error: error.errors[0].message });
return;
if (error.message === 'Vehicle with this VIN already exists') {
return reply.code(400).send({
error: 'Bad Request',
message: error.message
});
}
if (error.message === 'Vehicle not found') {
res.status(404).json({ error: 'Vehicle not found' });
return;
}
if (error.message === 'Unauthorized') {
res.status(403).json({ error: 'Access denied' });
return;
}
next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to create vehicle'
});
}
};
}
getDropdownMakes = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
async getVehicle(request: FastifyRequest<{ Params: VehicleParams }>, reply: FastifyReply) {
try {
const makes = await this.service.getDropdownMakes();
res.json(makes);
const userId = (request as any).user.sub;
const { id } = request.params;
const vehicle = await this.vehiclesService.getVehicle(id, userId);
return reply.code(200).send(vehicle);
} catch (error: any) {
if (error.message === 'Failed to load makes') {
res.status(503).json({ error: 'Unable to load makes data' });
return;
logger.error('Error getting vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub });
if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') {
return reply.code(404).send({
error: 'Not Found',
message: 'Vehicle not found'
});
}
next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get vehicle'
});
}
};
}
getDropdownModels = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
async updateVehicle(request: FastifyRequest<{ Params: VehicleParams; Body: UpdateVehicleBody }>, reply: FastifyReply) {
try {
const { make } = req.params;
if (!make) {
res.status(400).json({ error: 'Make parameter is required' });
return;
}
const models = await this.service.getDropdownModels(make);
res.json(models);
const userId = (request as any).user.sub;
const { id } = request.params;
const vehicle = await this.vehiclesService.updateVehicle(id, request.body, userId);
return reply.code(200).send(vehicle);
} catch (error: any) {
if (error.message === 'Failed to load models') {
res.status(503).json({ error: 'Unable to load models data' });
return;
logger.error('Error updating vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub });
if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') {
return reply.code(404).send({
error: 'Not Found',
message: 'Vehicle not found'
});
}
next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to update vehicle'
});
}
};
}
getDropdownTransmissions = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
async deleteVehicle(request: FastifyRequest<{ Params: VehicleParams }>, reply: FastifyReply) {
try {
const transmissions = await this.service.getDropdownTransmissions();
res.json(transmissions);
const userId = (request as any).user.sub;
const { id } = request.params;
await this.vehiclesService.deleteVehicle(id, userId);
return reply.code(204).send();
} catch (error: any) {
if (error.message === 'Failed to load transmissions') {
res.status(503).json({ error: 'Unable to load transmissions data' });
return;
logger.error('Error deleting vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub });
if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') {
return reply.code(404).send({
error: 'Not Found',
message: 'Vehicle not found'
});
}
next(error);
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to delete vehicle'
});
}
};
}
getDropdownEngines = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
async getDropdownMakes(_request: FastifyRequest, reply: FastifyReply) {
try {
const engines = await this.service.getDropdownEngines();
res.json(engines);
} catch (error: any) {
if (error.message === 'Failed to load engines') {
res.status(503).json({ error: 'Unable to load engines data' });
return;
}
next(error);
const makes = await this.vehiclesService.getDropdownMakes();
return reply.code(200).send(makes);
} catch (error) {
logger.error('Error getting dropdown makes', { error });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get makes'
});
}
};
}
getDropdownTrims = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
async getDropdownModels(request: FastifyRequest<{ Params: { make: string } }>, reply: FastifyReply) {
try {
const trims = await this.service.getDropdownTrims();
res.json(trims);
} catch (error: any) {
if (error.message === 'Failed to load trims') {
res.status(503).json({ error: 'Unable to load trims data' });
return;
}
next(error);
const { make } = request.params;
const models = await this.vehiclesService.getDropdownModels(make);
return reply.code(200).send(models);
} catch (error) {
logger.error('Error getting dropdown models', { error, make: request.params.make });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get models'
});
}
};
}
async getDropdownTransmissions(_request: FastifyRequest, reply: FastifyReply) {
try {
const transmissions = await this.vehiclesService.getDropdownTransmissions();
return reply.code(200).send(transmissions);
} catch (error) {
logger.error('Error getting dropdown transmissions', { error });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get transmissions'
});
}
}
async getDropdownEngines(_request: FastifyRequest, reply: FastifyReply) {
try {
const engines = await this.vehiclesService.getDropdownEngines();
return reply.code(200).send(engines);
} catch (error) {
logger.error('Error getting dropdown engines', { error });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get engines'
});
}
}
async getDropdownTrims(_request: FastifyRequest, reply: FastifyReply) {
try {
const trims = await this.vehiclesService.getDropdownTrims();
return reply.code(200).send(trims);
} catch (error) {
logger.error('Error getting dropdown trims', { error });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get trims'
});
}
}
}

View File

@@ -1,32 +1,80 @@
/**
* @ai-summary Express routes for vehicles API
* @ai-context Defines REST endpoints with auth middleware
* @ai-summary Fastify routes for vehicles API
* @ai-context Route definitions with TypeBox validation and authentication
*/
import { Router } from 'express';
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
import { FastifyPluginAsync } from 'fastify';
import {
CreateVehicleBody,
UpdateVehicleBody,
VehicleParams
} from '../domain/vehicles.types';
import { VehiclesController } from './vehicles.controller';
import { authMiddleware } from '../../../core/security/auth.middleware';
export function registerVehiclesRoutes(): Router {
const router = Router();
const controller = new VehiclesController();
export const vehiclesRoutes: FastifyPluginAsync = async (
fastify: FastifyInstance,
_opts: FastifyPluginOptions
) => {
const vehiclesController = new VehiclesController();
// Dropdown Data Routes (no auth required for form population)
router.get('/api/vehicles/dropdown/makes', controller.getDropdownMakes);
router.get('/api/vehicles/dropdown/models/:make', controller.getDropdownModels);
router.get('/api/vehicles/dropdown/transmissions', controller.getDropdownTransmissions);
router.get('/api/vehicles/dropdown/engines', controller.getDropdownEngines);
router.get('/api/vehicles/dropdown/trims', controller.getDropdownTrims);
// GET /api/vehicles - Get user's vehicles
fastify.get('/vehicles', {
preHandler: fastify.authenticate,
handler: vehiclesController.getUserVehicles.bind(vehiclesController)
});
// All other vehicle routes require authentication
router.use(authMiddleware);
// POST /api/vehicles - Create new vehicle
fastify.post<{ Body: CreateVehicleBody }>('/vehicles', {
preHandler: fastify.authenticate,
handler: vehiclesController.createVehicle.bind(vehiclesController)
});
// CRUD Routes
router.post('/api/vehicles', controller.createVehicle);
router.get('/api/vehicles', controller.getUserVehicles);
router.get('/api/vehicles/:id', controller.getVehicle);
router.put('/api/vehicles/:id', controller.updateVehicle);
router.delete('/api/vehicles/:id', controller.deleteVehicle);
// GET /api/vehicles/:id - Get specific vehicle
fastify.get<{ Params: VehicleParams }>('/vehicles/:id', {
preHandler: fastify.authenticate,
handler: vehiclesController.getVehicle.bind(vehiclesController)
});
return router;
// PUT /api/vehicles/:id - Update vehicle
fastify.put<{ Params: VehicleParams; Body: UpdateVehicleBody }>('/vehicles/:id', {
preHandler: fastify.authenticate,
handler: vehiclesController.updateVehicle.bind(vehiclesController)
});
// DELETE /api/vehicles/:id - Delete vehicle
fastify.delete<{ Params: VehicleParams }>('/vehicles/:id', {
preHandler: fastify.authenticate,
handler: vehiclesController.deleteVehicle.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/makes - Get vehicle makes
fastify.get('/vehicles/dropdown/makes', {
handler: vehiclesController.getDropdownMakes.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/models/:make - Get models for make
fastify.get<{ Params: { make: string } }>('/vehicles/dropdown/models/:make', {
handler: vehiclesController.getDropdownModels.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/transmissions - Get transmission types
fastify.get('/vehicles/dropdown/transmissions', {
handler: vehiclesController.getDropdownTransmissions.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/engines - Get engine configurations
fastify.get('/vehicles/dropdown/engines', {
handler: vehiclesController.getDropdownEngines.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/trims - Get trim levels
fastify.get('/vehicles/dropdown/trims', {
handler: vehiclesController.getDropdownTrims.bind(vehiclesController)
});
};
// For backward compatibility during migration
export function registerVehiclesRoutes() {
throw new Error('registerVehiclesRoutes is deprecated - use vehiclesRoutes Fastify plugin instead');
}

View File

@@ -82,4 +82,24 @@ export interface VINDecodeResult {
engineType?: string;
bodyType?: string;
rawData?: any;
}
// Fastify-specific types for HTTP handling
export interface CreateVehicleBody {
vin: string;
nickname?: string;
color?: string;
licensePlate?: string;
odometerReading?: number;
}
export interface UpdateVehicleBody {
nickname?: string;
color?: string;
licensePlate?: string;
odometerReading?: number;
}
export interface VehicleParams {
id: string;
}

View File

@@ -14,5 +14,5 @@ export type {
VehicleResponse
} from './domain/vehicles.types';
// Internal: Register routes with Express app
export { registerVehiclesRoutes } from './api/vehicles.routes';
// Internal: Register routes with Fastify app
export { vehiclesRoutes, registerVehiclesRoutes } from './api/vehicles.routes';

View File

@@ -1,36 +1,45 @@
/**
* @ai-summary Application entry point
* @ai-context Starts the Express server with all feature capsules
* @ai-context Starts the Fastify server with all feature capsules
*/
import { app } from './app';
import { buildApp } from './app';
import { env } from './core/config/environment';
import { logger } from './core/logging/logger';
const PORT = env.PORT || 3001;
const server = app.listen(PORT, () => {
logger.info(`MotoVaultPro backend running`, {
port: PORT,
environment: env.NODE_ENV,
nodeVersion: process.version,
});
});
async function start() {
try {
const app = await buildApp();
await app.listen({
port: PORT,
host: '0.0.0.0'
});
logger.info(`MotoVaultPro backend running`, {
port: PORT,
environment: env.NODE_ENV,
nodeVersion: process.version,
framework: 'Fastify'
});
} catch (error) {
logger.error('Failed to start server', { error });
process.exit(1);
}
}
start();
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
process.exit(0);
});
process.on('SIGINT', () => {
logger.info('SIGINT received, shutting down gracefully');
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
process.exit(0);
});
// Handle uncaught exceptions