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}
- ${safeTag}
- ${safeLabel}
- ${safeLabel}
+
+ ${safeTag}
+ ${safeTag}
+ ${safeLabel}
+ ${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 };