Refactor CityProfile and PlayerProfile components for improved data fetching and error handling; add NPC management modals for banner, gallery, and logo with enhanced user experience and error feedback.

This commit is contained in:
Lars Behrends
2025-12-30 13:56:00 +01:00
parent 5eb2eca110
commit c6ad8a92ec
14 changed files with 2539 additions and 102 deletions

View File

@@ -14,7 +14,483 @@
"multer": "^2.0.2",
"mysql2": "^3.6.0",
"passport": "^0.6.0",
"passport-discord": "^0.1.4"
"passport-discord": "^0.1.4",
"sharp": "^0.34.5"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/accepts": {
@@ -240,6 +716,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -976,6 +1461,18 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
@@ -1032,6 +1529,50 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -1148,6 +1689,13 @@
"node": ">=0.6"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",

View File

@@ -13,6 +13,8 @@
"multer": "^2.0.2",
"mysql2": "^3.6.0",
"passport": "^0.6.0",
"passport-discord": "^0.1.4"
"passport-discord": "^0.1.4",
"sharp": "^0.34.5",
"express-mysql-session": "^3.0.0"
}
}

View File

@@ -1,5 +1,6 @@
const express = require('express');
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const passport = require('passport');
const DiscordStrategy = require('passport-discord').Strategy;
const cors = require('cors');
@@ -58,14 +59,41 @@ const upload = multer({
init(); // Initialize DB
// Configure MySQL session store
const sessionStore = new MySQLStore({
host: process.env.DB_HOST || 'db',
port: 3306,
user: process.env.DB_USER || 'obsidian_user',
password: process.env.DB_PASS || 'obsidian_pass',
database: process.env.DB_NAME || 'obsidian_db',
clearExpired: true,
checkExpirationInterval: 900000, // Clean expired sessions every 15 minutes
expiration: 86400000 * 30, // 30 days default
createDatabaseTable: true, // Auto-create sessions table
schema: {
tableName: 'sessions',
columnNames: {
session_id: 'session_id',
expires: 'expires',
data: 'data'
}
}
});
// Middleware
app.use(express.json());
app.use(cors({ origin: corsOrigins, credentials: true }));
app.use(session({
secret: process.env.SESSION_SECRET || 'dev_secret',
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set true if using https
cookie: {
secure: false, // Set true if using https
maxAge: 24 * 60 * 60 * 1000, // 24 hours default
httpOnly: true,
sameSite: 'lax'
}
}));
app.use(passport.initialize());
app.use(passport.session());
@@ -129,11 +157,34 @@ app.get('/api/status', (req, res) => {
// --- ROUTES ---
// Auth
app.get('/auth/discord', passport.authenticate('discord'));
app.get('/auth/discord', (req, res, next) => {
// Check if user wants to be remembered (longer session)
const rememberMe = req.query.remember_me === 'true';
req.session.rememberMe = rememberMe;
// Set session maxAge based on remember me preference
if (rememberMe) {
req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
req.session.cookie.expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
} else {
req.session.cookie.maxAge = 24 * 60 * 60 * 1000; // 24 hours
req.session.cookie.expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
}
passport.authenticate('discord')(req, res, next);
});
app.get('/auth/discord/callback', passport.authenticate('discord', {
failureRedirect: FRONTEND_URL + '?error=login_failed'
}), (req, res) => {
res.redirect(FRONTEND_URL);
// Ensure session is saved with correct maxAge
if (req.session.rememberMe) {
req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
req.session.cookie.expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
}
req.session.save(() => {
res.redirect(FRONTEND_URL);
});
});
app.get('/auth/me', (req, res) => {
@@ -494,6 +545,282 @@ app.post('/api/admin/npc-company', (req, res) => {
);
});
// Upload NPC Company Banner
app.post('/api/admin/npc-companies/:projectId/banner/upload', upload.single('banner'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { projectId } = req.params;
// Verify this is an NPC company
if (!projectId.startsWith('npc_proj_')) {
return res.status(400).json({error: 'Nur NPC-Firmen können über diese Route bearbeitet werden'});
}
// Check if NPC company exists
db.get("SELECT title FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'NPC-Firma nicht gefunden'});
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Convert to WebP
const originalPath = path.join(UPLOAD_DIR, req.file.filename);
const webpFilename = imageId + '.webp';
const webpPath = path.join(UPLOAD_DIR, webpFilename);
convertToWebP(originalPath, webpPath).then((webpSize) => {
// Clean up original file
fs.unlinkSync(originalPath);
// Create image record with WebP data
const imageData = {
id: imageId,
filename: webpFilename,
originalName: req.file.originalname,
mimeType: 'image/webp',
size: webpSize,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.title} Banner`,
entityType: 'project',
entityId: projectId,
imageType: 'banner'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating NPC company banner image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Banners'});
}
// Update NPC company banner
db.run("UPDATE projects SET bannerImageId = ? WHERE id = ?", [imageId, projectId], function(err) {
if (err) {
console.error('Error updating NPC company banner:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Banners'});
}
res.json({
success: true,
imageId: imageId,
imageUrl: `${BACKEND_URL}/api/images/${imageId}`,
message: 'Banner erfolgreich hochgeladen'
});
});
});
}).catch((error) => {
console.error('Error converting NPC company banner to WebP:', error);
// Clean up files on error
try {
if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath);
if (fs.existsSync(webpPath)) fs.unlinkSync(webpPath);
} catch (cleanupErr) {
console.error('Error cleaning up files:', cleanupErr);
}
res.status(500).json({error: 'Fehler beim Konvertieren des Bildes'});
});
});
});
// Upload NPC Company Logo
app.post('/api/admin/npc-companies/:projectId/logo/upload', upload.single('logo'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { projectId } = req.params;
// Verify this is an NPC company
if (!projectId.startsWith('npc_proj_')) {
return res.status(400).json({error: 'Nur NPC-Firmen können über diese Route bearbeitet werden'});
}
// Check if NPC company exists
db.get("SELECT title FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'NPC-Firma nicht gefunden'});
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Convert to WebP
const originalPath = path.join(UPLOAD_DIR, req.file.filename);
const webpFilename = imageId + '.webp';
const webpPath = path.join(UPLOAD_DIR, webpFilename);
convertToWebP(originalPath, webpPath).then((webpSize) => {
// Clean up original file
fs.unlinkSync(originalPath);
// Create image record with WebP data
const imageData = {
id: imageId,
filename: webpFilename,
originalName: req.file.originalname,
mimeType: 'image/webp',
size: webpSize,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.title} Logo`,
entityType: 'project',
entityId: projectId,
imageType: 'logo'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating NPC company logo image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Logos'});
}
// Update NPC company logo
db.run("UPDATE projects SET logoImageId = ? WHERE id = ?", [imageId, projectId], function(err) {
if (err) {
console.error('Error updating NPC company logo:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Logos'});
}
res.json({
success: true,
imageId: imageId,
logoUrl: `${BACKEND_URL}/api/images/${imageId}`,
message: 'Logo erfolgreich hochgeladen'
});
});
});
}).catch((error) => {
console.error('Error converting NPC company logo to WebP:', error);
// Clean up files on error
try {
if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath);
if (fs.existsSync(webpPath)) fs.unlinkSync(webpPath);
} catch (cleanupErr) {
console.error('Error cleaning up files:', cleanupErr);
}
res.status(500).json({error: 'Fehler beim Konvertieren des Bildes'});
});
});
});
// Upload NPC Company Gallery Image
app.post('/api/admin/npc-companies/:projectId/gallery/upload', upload.single('gallery'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { projectId } = req.params;
// Verify this is an NPC company
if (!projectId.startsWith('npc_proj_')) {
return res.status(400).json({error: 'Nur NPC-Firmen können über diese Route bearbeitet werden'});
}
// Check if NPC company exists
db.get("SELECT title, gallery FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'NPC-Firma nicht gefunden'});
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Convert to WebP
const originalPath = path.join(UPLOAD_DIR, req.file.filename);
const webpFilename = imageId + '.webp';
const webpPath = path.join(UPLOAD_DIR, webpFilename);
convertToWebP(originalPath, webpPath).then((webpSize) => {
// Clean up original file
fs.unlinkSync(originalPath);
// Create image record with WebP data
const imageData = {
id: imageId,
filename: webpFilename,
originalName: req.file.originalname,
mimeType: 'image/webp',
size: webpSize,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.title} Portfolio`,
entityType: 'project',
entityId: projectId,
imageType: 'gallery'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating NPC company gallery image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Bildes'});
}
try {
// Add image ID to gallery array
const gallery = JSON.parse(row.gallery || '[]');
gallery.push(imageId);
db.run("UPDATE projects SET gallery = ? WHERE id = ?", [JSON.stringify(gallery), projectId], function(err) {
if (err) {
console.error('Error updating NPC company gallery:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren der Galerie'});
}
res.json({
success: true,
imageId: imageId,
imageUrl: `${BACKEND_URL}/api/images/${imageId}`,
gallery: gallery,
message: 'Bild erfolgreich zum Portfolio hinzugefügt'
});
});
} catch (e) {
console.error('Error parsing NPC company gallery:', e);
res.status(500).json({error: 'Fehler beim Verarbeiten der Galerie-Daten'});
}
});
}).catch((error) => {
console.error('Error converting NPC company gallery image to WebP:', error);
// Clean up files on error
try {
if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath);
if (fs.existsSync(webpPath)) fs.unlinkSync(webpPath);
} catch (cleanupErr) {
console.error('Error cleaning up files:', cleanupErr);
}
res.status(500).json({error: 'Fehler beim Konvertieren des Bildes'});
});
});
});
// Grant admin rights to a player (Admin only)
app.post('/api/admin/grant-admin/:uuid', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
@@ -1019,7 +1346,7 @@ app.get('/api/images/:imageId/meta', (req, res) => {
});
// Upload banner image
app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), (req, res) => {
app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
const { projectId } = req.params;
@@ -1081,7 +1408,7 @@ app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), (req
});
// Upload gallery image
app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), (req, res) => {
app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
const { projectId } = req.params;
@@ -1152,7 +1479,7 @@ app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), (r
});
// Upload logo image
app.post('/api/projects/:projectId/logo/upload', upload.single('logo'), (req, res) => {
app.post('/api/projects/:projectId/logo/upload', upload.single('logo'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
const { projectId } = req.params;
@@ -1344,7 +1671,7 @@ app.delete('/api/admin/cities/:cityId', (req, res) => {
});
// Upload city banner
app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), (req, res) => {
app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
@@ -1408,7 +1735,7 @@ app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), (re
});
// Upload city logo
app.post('/api/admin/cities/:cityId/logo/upload', upload.single('logo'), (req, res) => {
app.post('/api/admin/cities/:cityId/logo/upload', upload.single('logo'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
@@ -1472,7 +1799,7 @@ app.post('/api/admin/cities/:cityId/logo/upload', upload.single('logo'), (req, r
});
// Upload city gallery image
app.post('/api/admin/cities/:cityId/gallery/upload', upload.single('gallery'), (req, res) => {
app.post('/api/admin/cities/:cityId/gallery/upload', upload.single('gallery'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin