From 729b7f566f8f8b44a498d3f657d3345e6238b830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20Andr=C3=A1s=20T=C3=B3th?= Date: Fri, 17 Jul 2026 13:55:39 +0200 Subject: [PATCH] code quality updates --- .env.example | 11 ++ package-lock.json | 281 ++++++++++++++++++++++++++++++++++++++++++- package.json | 5 +- src/api/badge.js | 41 +++---- src/api/tag.js | 49 ++++---- src/index.js | 19 ++- src/lib/logger.js | 13 ++ src/lib/svg-utils.js | 16 +++ src/lib/validate.js | 40 ++++++ 9 files changed, 422 insertions(+), 53 deletions(-) create mode 100644 .env.example create mode 100644 src/lib/logger.js create mode 100644 src/lib/svg-utils.js create mode 100644 src/lib/validate.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..030945c --- /dev/null +++ b/.env.example @@ -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 diff --git a/package-lock.json b/package-lock.json index 14942c6..84c1a32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,14 @@ "license": "ISC", "dependencies": { "axios": "^1.10.0", + "dotenv": "^17.4.2", "express": "^5.1.0", - "helmet": "^8.1.0" + "helmet": "^8.1.0", + "pino": "^10.3.1" }, "devDependencies": { "jest": "^29.7.0", + "pino-pretty": "^13.1.3", "supertest": "^7.1.3" } }, @@ -909,6 +912,12 @@ "@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": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1144,6 +1153,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/axios": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", @@ -1554,6 +1572,13 @@ "dev": true, "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": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1673,6 +1698,16 @@ "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": { "version": "4.4.1", "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_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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1820,6 +1867,16 @@ "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": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2016,6 +2073,13 @@ "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": { "version": "2.1.0", "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_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": { "version": "2.0.2", "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" } }, + "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3518,6 +3599,16 @@ "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": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3589,6 +3680,15 @@ "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": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3775,6 +3875,81 @@ "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": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -3826,6 +4001,22 @@ "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": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -3859,6 +4050,17 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "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": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -3891,6 +4093,12 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3922,6 +4130,15 @@ "dev": true, "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4022,12 +4239,38 @@ ], "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": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "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": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4200,6 +4443,15 @@ "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": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4221,6 +4473,15 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4401,6 +4662,24 @@ "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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index efe8d90..c4f9acb 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,14 @@ "license": "ISC", "dependencies": { "axios": "^1.10.0", + "dotenv": "^17.4.2", "express": "^5.1.0", - "helmet": "^8.1.0" + "helmet": "^8.1.0", + "pino": "^10.3.1" }, "devDependencies": { "jest": "^29.7.0", + "pino-pretty": "^13.1.3", "supertest": "^7.1.3" } } diff --git a/src/api/badge.js b/src/api/badge.js index cbe890b..ac12604 100644 --- a/src/api/badge.js +++ b/src/api/badge.js @@ -1,19 +1,20 @@ const router = require('express').Router(); const axios = require('axios'); 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) => { - const { - icon = 'simpleicons', - label = 'Simple Icons', - style = 'rect', // badge stílusa: rect (négyzetes), flat (lekerekített sarkok) - bgicon = 'none', // ikon háttérszín, alap: világosszürke - bglabel = 'none', // label háttérszín, alap: sötétszürke - color = '#000000', - size = 24, - labelpos = 'right', - fontweight = 'normal', - } = req.query; + const icon = req.query.icon || 'simpleicons'; + const label = req.query.label || 'Simple Icons'; + const style = parseStyle(req.query.style); + const bgicon = req.query.bgicon || 'none'; + const bglabel = req.query.bglabel || 'none'; + const color = req.query.color || '#000000'; + const size = parseSize(req.query.size); + const labelpos = parseLabelpos(req.query.labelpos); + const fontweight = parseFontweight(req.query.fontweight); try { // download icon SVG from SimpleIcons CDN @@ -41,15 +42,11 @@ router.get('/badge', async (req, res) => { const safeFontWeight = escapeSvg(fontweight); const padding = 10; - const iconSize = parseInt(size); - const fontSize = Math.round(iconSize * 0.6); - const textPadding = Math.round(iconSize * 0.4); - const radius = 0; - const approxCharWidth = fontSize * 0.6; - const textWidth = effectiveLabel.length * approxCharWidth; - - // Kinyerjük az ikon SVG-ből a width és height attribútumokat (ha vannak) - let iconViewBoxWidth = iconSize; + const iconSize = size; + const fontSize = calcFontSize(iconSize); + const textPadding = Math.round(iconSize * 0.4); + const radius = style === 'flat' ? 3 : 0; + const textWidth = estimateTextWidth(effectiveLabel, fontSize); let iconViewBoxHeight = iconSize; const widthMatch = iconSVG.match(/width="(\d+\.?\d*)"/); const heightMatch = iconSVG.match(/height="(\d+\.?\d*)"/); @@ -78,7 +75,7 @@ router.get('/badge', async (req, res) => { let width, height, iconGroup, textElem; - const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif"; + const fontFamily = FONT_FAMILY; const fontWeight = safeFontWeight; // paraméterből if (labelpos === 'left') { @@ -149,7 +146,7 @@ router.get('/badge', async (req, res) => { if (err.response && err.response.status === 404) { return res.status(404).send('Icon not found'); } else { - console.error(err); + logger.error({ err, icon, url: req.url }, 'Badge generation failed'); res.status(500).send('Internal Server Error'); } } diff --git a/src/api/tag.js b/src/api/tag.js index 83c0e45..e2ffd5e 100644 --- a/src/api/tag.js +++ b/src/api/tag.js @@ -1,21 +1,21 @@ const router = require('express').Router(); 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) => { - const { - tag = 'badgedex', - label = 'BadgeDex', - style = 'rect', // badge stílusa: rect (négyzetes), flat (lekerekített sarkok) - bgtag = 'none', // ikon háttérszín, alap: világosszürke - bglabel = 'none', // label háttérszín, alap: sötétszürke - color = '#000000', - size = 24, - labelpos = 'right', - fontweight = 'normal', - } = req.query; + const tag = req.query.tag || 'badgedex'; + const label = req.query.label || 'BadgeDex'; + const style = parseStyle(req.query.style); + const bgtag = req.query.bgtag || 'none'; + const bglabel = req.query.bglabel || 'none'; + const color = req.query.color || '#000000'; + const size = parseSize(req.query.size); + const labelpos = req.query.labelpos || 'right'; + const fontweight = req.query.fontweight || 'normal'; - const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif"; + const fontFamily = FONT_FAMILY; const fontWeight = escapeSvg(fontweight); const safeTag = escapeSvg(tag); const safeLabel = escapeSvg(label); @@ -28,9 +28,9 @@ router.get('/tag', (req, res) => { //const tagX = labelpos === 'left' ? 455 : 165; // A méretet pixelben adjuk meg, alapértelmezett 24px - const textPadding = Number(5); - const badgeSize = Number(size); - const badgeFontSize = Math.round(badgeSize * 0.6); // A szöveg mérete a badge méretéhez igazítva + const textPadding = 5; + const badgeSize = size; + const badgeFontSize = calcFontSize(badgeSize); 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 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) => { `.trim(); + const rx = 3; const flat = ` - + - - - + + + - - - ${safeTag} - - ${safeLabel} + + + ${safeTag} + + ${safeLabel} `.trim(); const svg = ` diff --git a/src/index.js b/src/index.js index 2557919..df23f10 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,13 @@ +require('dotenv').config(); + const express = require('express'); const helmet = require('helmet'); -const app = express(); -const PORT = process.env.PORT || 3000; +const logger = require('./lib/logger'); +const api = require('./api'); const version = require('./lib/version').version(); -const api = require('./api'); +const app = express(); +const PORT = process.env.PORT || 3000; app.use(helmet({ crossOriginResourcePolicy: false, @@ -12,7 +15,13 @@ app.use(helmet({ 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, () => { - console.log(`[ ${new Date()} ] [ 🚀 BadgeDex version: ${version} ]`); - console.log(`[ ${new Date()} ] [ 🚀 BadgeDex API running at http://localhost:${PORT}/ ]`); + logger.info(`BadgeDex version ${version} listening on http://localhost:${PORT}/`); }); diff --git a/src/lib/logger.js b/src/lib/logger.js new file mode 100644 index 0000000..fb548c1 --- /dev/null +++ b/src/lib/logger.js @@ -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; diff --git a/src/lib/svg-utils.js b/src/lib/svg-utils.js new file mode 100644 index 0000000..07ac730 --- /dev/null +++ b/src/lib/svg-utils.js @@ -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 }; diff --git a/src/lib/validate.js b/src/lib/validate.js new file mode 100644 index 0000000..35e42bb --- /dev/null +++ b/src/lib/validate.js @@ -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, 100–900. + */ +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 };