code quality updates

This commit is contained in:
2026-07-17 13:55:39 +02:00
parent d74fd3a248
commit 729b7f566f
9 changed files with 422 additions and 53 deletions
+11
View File
@@ -0,0 +1,11 @@
# Server port
PORT=3000
# Log level: fatal | error | warn | info | debug | trace
LOG_LEVEL=info
# Node environment: production | development | test
NODE_ENV=development
# App version override (optional falls back to VERSION file)
# APP_VERSION=1.0.0
+280 -1
View File
@@ -10,11 +10,14 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.10.0", "axios": "^1.10.0",
"dotenv": "^17.4.2",
"express": "^5.1.0", "express": "^5.1.0",
"helmet": "^8.1.0" "helmet": "^8.1.0",
"pino": "^10.3.1"
}, },
"devDependencies": { "devDependencies": {
"jest": "^29.7.0", "jest": "^29.7.0",
"pino-pretty": "^13.1.3",
"supertest": "^7.1.3" "supertest": "^7.1.3"
} }
}, },
@@ -909,6 +912,12 @@
"@noble/hashes": "^1.1.5" "@noble/hashes": "^1.1.5"
} }
}, },
"node_modules/@pinojs/redact": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
"license": "MIT"
},
"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",
@@ -1144,6 +1153,15 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT" "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/axios": { "node_modules/axios": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
@@ -1554,6 +1572,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1673,6 +1698,16 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -1764,6 +1799,18 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1820,6 +1867,16 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -2016,6 +2073,13 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/fast-copy": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz",
"integrity": "sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -2387,6 +2451,13 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"dev": true,
"license": "MIT"
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -3257,6 +3328,16 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -3518,6 +3599,16 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3589,6 +3680,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"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": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -3775,6 +3875,81 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pino": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
"integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
"license": "MIT",
"dependencies": {
"@pinojs/redact": "^0.4.0",
"atomic-sleep": "^1.0.0",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^3.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": "^4.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-pretty": {
"version": "13.1.3",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz",
"integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^4.0.0",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^3.0.0",
"pump": "^3.0.0",
"secure-json-parse": "^4.0.0",
"sonic-boom": "^4.0.1",
"strip-json-comments": "^5.0.2"
},
"bin": {
"pino-pretty": "bin.js"
}
},
"node_modules/pino-pretty/node_modules/strip-json-comments": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pino-std-serializers": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
"license": "MIT"
},
"node_modules/pirates": { "node_modules/pirates": {
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@@ -3826,6 +4001,22 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"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/prompts": { "node_modules/prompts": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -3859,6 +4050,17 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/pure-rand": { "node_modules/pure-rand": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
@@ -3891,6 +4093,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"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": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -3922,6 +4130,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"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/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -4022,12 +4239,38 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/secure-json-parse": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
"integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -4200,6 +4443,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
"integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -4221,6 +4473,15 @@
"source-map": "^0.6.0" "source-map": "^0.6.0"
} }
}, },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": { "node_modules/sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -4401,6 +4662,24 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/thread-stream": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
"integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==",
"license": "MIT",
"dependencies": {
"real-require": "^1.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/thread-stream/node_modules/real-require": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz",
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
"license": "MIT"
},
"node_modules/tmpl": { "node_modules/tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+4 -1
View File
@@ -19,11 +19,14 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.10.0", "axios": "^1.10.0",
"dotenv": "^17.4.2",
"express": "^5.1.0", "express": "^5.1.0",
"helmet": "^8.1.0" "helmet": "^8.1.0",
"pino": "^10.3.1"
}, },
"devDependencies": { "devDependencies": {
"jest": "^29.7.0", "jest": "^29.7.0",
"pino-pretty": "^13.1.3",
"supertest": "^7.1.3" "supertest": "^7.1.3"
} }
} }
+18 -21
View File
@@ -1,19 +1,20 @@
const router = require('express').Router(); const router = require('express').Router();
const axios = require('axios'); const axios = require('axios');
const { escapeSvg, safeColor } = require('../lib/sanitize'); const { escapeSvg, safeColor } = require('../lib/sanitize');
const { parseSize, parseStyle, parseLabelpos, parseFontweight } = require('../lib/validate');
const { FONT_FAMILY, calcFontSize, estimateTextWidth } = require('../lib/svg-utils');
const logger = require('../lib/logger');
router.get('/badge', async (req, res) => { router.get('/badge', async (req, res) => {
const { const icon = req.query.icon || 'simpleicons';
icon = 'simpleicons', const label = req.query.label || 'Simple Icons';
label = 'Simple Icons', const style = parseStyle(req.query.style);
style = 'rect', // badge stílusa: rect (négyzetes), flat (lekerekített sarkok) const bgicon = req.query.bgicon || 'none';
bgicon = 'none', // ikon háttérszín, alap: világosszürke const bglabel = req.query.bglabel || 'none';
bglabel = 'none', // label háttérszín, alap: sötétszürke const color = req.query.color || '#000000';
color = '#000000', const size = parseSize(req.query.size);
size = 24, const labelpos = parseLabelpos(req.query.labelpos);
labelpos = 'right', const fontweight = parseFontweight(req.query.fontweight);
fontweight = 'normal',
} = req.query;
try { try {
// download icon SVG from SimpleIcons CDN // download icon SVG from SimpleIcons CDN
@@ -41,15 +42,11 @@ router.get('/badge', async (req, res) => {
const safeFontWeight = escapeSvg(fontweight); const safeFontWeight = escapeSvg(fontweight);
const padding = 10; const padding = 10;
const iconSize = parseInt(size); const iconSize = size;
const fontSize = Math.round(iconSize * 0.6); const fontSize = calcFontSize(iconSize);
const textPadding = Math.round(iconSize * 0.4); const textPadding = Math.round(iconSize * 0.4);
const radius = 0; const radius = style === 'flat' ? 3 : 0;
const approxCharWidth = fontSize * 0.6; const textWidth = estimateTextWidth(effectiveLabel, fontSize);
const textWidth = effectiveLabel.length * approxCharWidth;
// Kinyerjük az ikon SVG-ből a width és height attribútumokat (ha vannak)
let iconViewBoxWidth = iconSize;
let iconViewBoxHeight = iconSize; let iconViewBoxHeight = iconSize;
const widthMatch = iconSVG.match(/width="(\d+\.?\d*)"/); const widthMatch = iconSVG.match(/width="(\d+\.?\d*)"/);
const heightMatch = iconSVG.match(/height="(\d+\.?\d*)"/); const heightMatch = iconSVG.match(/height="(\d+\.?\d*)"/);
@@ -78,7 +75,7 @@ router.get('/badge', async (req, res) => {
let width, height, iconGroup, textElem; let width, height, iconGroup, textElem;
const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif"; const fontFamily = FONT_FAMILY;
const fontWeight = safeFontWeight; // paraméterből const fontWeight = safeFontWeight; // paraméterből
if (labelpos === 'left') { if (labelpos === 'left') {
@@ -149,7 +146,7 @@ router.get('/badge', async (req, res) => {
if (err.response && err.response.status === 404) { if (err.response && err.response.status === 404) {
return res.status(404).send('Icon not found'); return res.status(404).send('Icon not found');
} else { } else {
console.error(err); logger.error({ err, icon, url: req.url }, 'Badge generation failed');
res.status(500).send('Internal Server Error'); res.status(500).send('Internal Server Error');
} }
} }
+25 -24
View File
@@ -1,21 +1,21 @@
const router = require('express').Router(); const router = require('express').Router();
const { escapeSvg, safeColor } = require('../lib/sanitize'); const { escapeSvg, safeColor } = require('../lib/sanitize');
const { parseSize, parseStyle } = require('../lib/validate');
const { FONT_FAMILY, calcFontSize } = require('../lib/svg-utils');
router.get('/tag', (req, res) => { router.get('/tag', (req, res) => {
const { const tag = req.query.tag || 'badgedex';
tag = 'badgedex', const label = req.query.label || 'BadgeDex';
label = 'BadgeDex', const style = parseStyle(req.query.style);
style = 'rect', // badge stílusa: rect (négyzetes), flat (lekerekített sarkok) const bgtag = req.query.bgtag || 'none';
bgtag = 'none', // ikon háttérszín, alap: világosszürke const bglabel = req.query.bglabel || 'none';
bglabel = 'none', // label háttérszín, alap: sötétszürke const color = req.query.color || '#000000';
color = '#000000', const size = parseSize(req.query.size);
size = 24, const labelpos = req.query.labelpos || 'right';
labelpos = 'right', const fontweight = req.query.fontweight || 'normal';
fontweight = 'normal',
} = req.query;
const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif"; const fontFamily = FONT_FAMILY;
const fontWeight = escapeSvg(fontweight); const fontWeight = escapeSvg(fontweight);
const safeTag = escapeSvg(tag); const safeTag = escapeSvg(tag);
const safeLabel = escapeSvg(label); const safeLabel = escapeSvg(label);
@@ -28,9 +28,9 @@ router.get('/tag', (req, res) => {
//const tagX = labelpos === 'left' ? 455 : 165; //const tagX = labelpos === 'left' ? 455 : 165;
// A méretet pixelben adjuk meg, alapértelmezett 24px // A méretet pixelben adjuk meg, alapértelmezett 24px
const textPadding = Number(5); const textPadding = 5;
const badgeSize = Number(size); const badgeSize = size;
const badgeFontSize = Math.round(badgeSize * 0.6); // A szöveg mérete a badge méretéhez igazítva const badgeFontSize = calcFontSize(badgeSize);
const tagTextLength = tag.length * (badgeFontSize * 0.7); // A tag szöveg hossza const tagTextLength = tag.length * (badgeFontSize * 0.7); // A tag szöveg hossza
const labelTextLength = label.length * (badgeFontSize * 0.8); // A label szöveg hossza const labelTextLength = label.length * (badgeFontSize * 0.8); // A label szöveg hossza
const badgeWidth = tagTextLength + labelTextLength + 10; // A badge szélessége a szövegek hosszától függően const badgeWidth = tagTextLength + labelTextLength + 10; // A badge szélessége a szövegek hosszától függően
@@ -48,24 +48,25 @@ router.get('/tag', (req, res) => {
</g>`.trim(); </g>`.trim();
const rx = 3;
const flat = ` const flat = `
<linearGradient id="s" x2="0" y2="100%"> <linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/> <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/> <stop offset="1" stop-opacity=".1"/>
</linearGradient> </linearGradient>
<clipPath id="r"> <clipPath id="r">
<rect width="62" height="20" rx="3" fill="#fff"/> <rect width="${badgeWidth}" height="${badgeSize}" rx="${rx}" fill="#fff"/>
</clipPath> </clipPath>
<g clip-path="url(#r)"> <g clip-path="url(#r)">
<rect width="31" height="20" fill="#555"/> <rect width="${tagTextLength + textPadding}" height="${badgeSize}" fill="${safeBgTag}"/>
<rect x="31" width="31" height="20" fill="#007ec6"/> <rect x="${tagTextLength + textPadding}" width="${labelTextLength + textPadding}" height="${badgeSize}" fill="${safeBgLabel}"/>
<rect width="62" height="20" fill="url(#s)"/> <rect width="${badgeWidth}" height="${badgeSize}" fill="url(#s)"/>
</g> </g>
<g fill="#fff" text-anchor="middle" font-family="${fontFamily}" text-rendering="geometricPrecision" font-size="110" font-weight="${fontWeight}"> <g fill="#fff" text-anchor="middle" font-family="${fontFamily}" text-rendering="geometricPrecision" font-size="${badgeFontSize}" font-weight="${fontWeight}">
<text aria-hidden="true" x="165" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">${safeTag}</text> <text aria-hidden="true" x="${(tagTextLength + textPadding) / 2}" y="${badgeFontSize + textPadding + 1}" fill="#010101" fill-opacity=".3">${safeTag}</text>
<text x="165" y="140" transform="scale(.1)" fill="#fff" textLength="210">${safeTag}</text> <text x="${(tagTextLength + textPadding) / 2}" y="${badgeFontSize + textPadding}" fill="#fff">${safeTag}</text>
<text aria-hidden="true" x="455" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">${safeLabel}</text> <text aria-hidden="true" x="${tagTextLength + textPadding + (labelTextLength + textPadding) / 2}" y="${badgeFontSize + textPadding + 1}" fill="#010101" fill-opacity=".3">${safeLabel}</text>
<text x="455" y="140" transform="scale(.1)" fill="#fff" textLength="210">${safeLabel}</text> <text x="${tagTextLength + textPadding + (labelTextLength + textPadding) / 2}" y="${badgeFontSize + textPadding}" fill="#fff">${safeLabel}</text>
</g>`.trim(); </g>`.trim();
const svg = ` const svg = `
+14 -5
View File
@@ -1,10 +1,13 @@
require('dotenv').config();
const express = require('express'); const express = require('express');
const helmet = require('helmet'); const helmet = require('helmet');
const app = express(); const logger = require('./lib/logger');
const PORT = process.env.PORT || 3000; const api = require('./api');
const version = require('./lib/version').version(); const version = require('./lib/version').version();
const api = require('./api'); const app = express();
const PORT = process.env.PORT || 3000;
app.use(helmet({ app.use(helmet({
crossOriginResourcePolicy: false, crossOriginResourcePolicy: false,
@@ -12,7 +15,13 @@ app.use(helmet({
app.use('/', api.router); app.use('/', api.router);
// Central error handler catches all unhandled errors
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
logger.error({ err, url: req.url }, 'Unhandled error');
res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
});
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`[ ${new Date()} ] [ 🚀 BadgeDex version: ${version} ]`); logger.info(`BadgeDex version ${version} listening on http://localhost:${PORT}/`);
console.log(`[ ${new Date()} ] [ 🚀 BadgeDex API running at http://localhost:${PORT}/ ]`);
}); });
+13
View File
@@ -0,0 +1,13 @@
'use strict';
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport:
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
});
module.exports = logger;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
/** Standard font family used by both badge and tag endpoints. */
const FONT_FAMILY = 'Verdana,Geneva,DejaVu Sans,sans-serif';
/** Font-size scales with the icon / badge size. */
const calcFontSize = (size) => Math.round(size * 0.6);
/**
* Rough text-width estimate based on font-size and character count.
* Uses a 0.6 coefficient which works well for Verdana / sans-serif.
*/
const estimateTextWidth = (text, fontSize) =>
text.length * fontSize * 0.6;
module.exports = { FONT_FAMILY, calcFontSize, estimateTextWidth };
+40
View File
@@ -0,0 +1,40 @@
'use strict';
const ALLOWED_STYLES = ['rect', 'flat'];
const ALLOWED_LABELPOS = ['right', 'left', 'above', 'below'];
const SIZE_MIN = 8;
const SIZE_MAX = 256;
/**
* Parse and clamp the `size` query parameter.
* Returns a positive integer in [SIZE_MIN, SIZE_MAX].
*/
const parseSize = (raw) => {
const n = parseInt(raw, 10);
if (Number.isNaN(n) || n < SIZE_MIN || n > SIZE_MAX) return 24;
return n;
};
/**
* Return a valid style or the default.
*/
const parseStyle = (raw) =>
ALLOWED_STYLES.includes(raw) ? raw : 'rect';
/**
* Return a valid labelpos or the default.
*/
const parseLabelpos = (raw) =>
ALLOWED_LABELPOS.includes(raw) ? raw : 'right';
/**
* Return a valid font-weight string.
* Accepts: normal, bold, lighter, bolder, 100900.
*/
const parseFontweight = (raw) => {
if (/^(normal|bold|lighter|bolder)$/.test(raw)) return raw;
if (/^[1-9]00$/.test(raw)) return raw;
return 'normal';
};
module.exports = { parseSize, parseStyle, parseLabelpos, parseFontweight, SIZE_MIN, SIZE_MAX };