diff --git a/components/Navigation.vue b/components/Navigation.vue index 10ebf01..e3f2c94 100644 --- a/components/Navigation.vue +++ b/components/Navigation.vue @@ -100,6 +100,11 @@ active-class="text-white bg-primary-600"> Vereinsmeisterschaften + + Galerie + @@ -314,6 +319,10 @@ class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> Vereinsmeisterschaften + + Galerie + diff --git a/package-lock.json b/package-lock.json index c7567f9..7bff8c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "pdf-lib": "^1.17.1", "pdf-parse": "^2.4.5", "pinia": "^3.0.3", + "sharp": "^0.34.5", "vue": "^3.5.22" }, "devDependencies": { @@ -522,9 +523,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", - "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", + "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": { @@ -957,6 +958,471 @@ "node": ">=18" } }, + "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/@ioredis/commands": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", @@ -10186,6 +10652,59 @@ "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/sharp/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/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index c845b43..6f6b022 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "pdf-lib": "^1.17.1", "pdf-parse": "^2.4.5", "pinia": "^3.0.3", + "sharp": "^0.34.5", "vue": "^3.5.22" }, "devDependencies": { @@ -31,8 +32,8 @@ "autoprefixer": "^10.4.0", "lucide-vue-next": "^0.344.0", "postcss": "^8.4.0", - "tailwindcss": "^3.4.0", "supertest": "^7.1.0", + "tailwindcss": "^3.4.0", "vitest": "^2.1.4" } } diff --git a/pages/verein/galerie.vue b/pages/verein/galerie.vue new file mode 100644 index 0000000..b6b50d2 --- /dev/null +++ b/pages/verein/galerie.vue @@ -0,0 +1,430 @@ + + + + + Bildergalerie + + + + + Bilder werden geladen... + + + + Noch keine Bilder in der Galerie. + + + + + + + + + + {{ image.title }} + + + Nur für Mitglieder + + + {{ formatDate(image.uploadedAt) }} + + + + + {{ deleting === image.id ? 'Wird gelöscht...' : 'Löschen' }} + + + + + + + + + Zurück + + + Seite {{ pagination.page }} von {{ pagination.totalPages }} ({{ pagination.total }} Bilder) + + + Weiter + + + + + + + + Bild hochladen + + + + + + + + + Bilddatei + + + + + + Titel + + + + + + Beschreibung (optional) + + + + + + + Öffentlich sichtbar (für alle Besucher) + + + + {{ uploading ? 'Wird hochgeladen...' : 'Bild hochladen' }} + + {{ uploadError }} + {{ uploadSuccess }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ selectedImage.title }} + + {{ selectedImage.description }} + + + Bild {{ currentImageIndex + 1 }} von {{ images.length }} + + + + + + + + + diff --git a/server/api/galerie/[id].delete.js b/server/api/galerie/[id].delete.js new file mode 100644 index 0000000..2dc75c6 --- /dev/null +++ b/server/api/galerie/[id].delete.js @@ -0,0 +1,118 @@ +import fs from 'fs/promises' +import path from 'path' +import { getUserFromToken, verifyToken } from '../../utils/auth.js' + +// Handle both dev and production paths +const getDataPath = (filename) => { + const cwd = process.cwd() + if (cwd.endsWith('.output')) { + return path.join(cwd, '../server/data', filename) + } + return path.join(cwd, 'server/data', filename) +} + +const GALERIE_DIR = getDataPath('galerie') +const GALERIE_METADATA = getDataPath('galerie-metadata.json') + +async function readGalerieMetadata() { + try { + const data = await fs.readFile(GALERIE_METADATA, 'utf-8') + return JSON.parse(data) + } catch (error) { + if (error.code === 'ENOENT') { + return [] + } + throw error + } +} + +async function writeGalerieMetadata(metadata) { + await fs.writeFile(GALERIE_METADATA, JSON.stringify(metadata, null, 2), 'utf-8') +} + +export default defineEventHandler(async (event) => { + try { + // Authentifizierung prüfen + const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace('Bearer ', '') + + if (!token) { + throw createError({ + statusCode: 401, + statusMessage: 'Nicht authentifiziert' + }) + } + + const decoded = verifyToken(token) + if (!decoded) { + throw createError({ + statusCode: 401, + statusMessage: 'Ungültiges Token' + }) + } + + const user = await getUserFromToken(token) + if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) { + throw createError({ + statusCode: 403, + statusMessage: 'Keine Berechtigung zum Löschen von Bildern' + }) + } + + const imageId = getRouterParam(event, 'id') + + if (!imageId) { + throw createError({ + statusCode: 400, + statusMessage: 'Bild-ID erforderlich' + }) + } + + const metadata = await readGalerieMetadata() + const image = metadata.find(img => img.id === imageId) + + if (!image) { + throw createError({ + statusCode: 404, + statusMessage: 'Bild nicht gefunden' + }) + } + + // Lösche Dateien + const originalPath = path.join(GALERIE_DIR, 'originals', image.filename) + const previewPath = path.join(GALERIE_DIR, 'previews', image.previewFilename) + + try { + await fs.unlink(originalPath) + } catch (error) { + console.warn('Original-Datei konnte nicht gelöscht werden:', error) + } + + try { + await fs.unlink(previewPath) + } catch (error) { + console.warn('Preview-Datei konnte nicht gelöscht werden:', error) + } + + // Entferne aus Metadaten + const updatedMetadata = metadata.filter(img => img.id !== imageId) + await writeGalerieMetadata(updatedMetadata) + + return { + success: true, + message: 'Bild erfolgreich gelöscht' + } + + } catch (error) { + console.error('Fehler beim Löschen des Bildes:', error) + + if (error.statusCode) { + throw error + } + + throw createError({ + statusCode: 500, + statusMessage: 'Fehler beim Löschen des Bildes' + }) + } +}) + diff --git a/server/api/galerie/[id].get.js b/server/api/galerie/[id].get.js new file mode 100644 index 0000000..7fea77b --- /dev/null +++ b/server/api/galerie/[id].get.js @@ -0,0 +1,125 @@ +import fs from 'fs/promises' +import path from 'path' +import sharp from 'sharp' +import { getUserFromToken, verifyToken } from '../../utils/auth.js' + +// Handle both dev and production paths +const getDataPath = (filename) => { + const cwd = process.cwd() + if (cwd.endsWith('.output')) { + return path.join(cwd, '../server/data', filename) + } + return path.join(cwd, 'server/data', filename) +} + +const GALERIE_DIR = getDataPath('galerie') +const GALERIE_METADATA = getDataPath('galerie-metadata.json') + +async function readGalerieMetadata() { + try { + const data = await fs.readFile(GALERIE_METADATA, 'utf-8') + return JSON.parse(data) + } catch (error) { + if (error.code === 'ENOENT') { + return [] + } + throw error + } +} + +export default defineEventHandler(async (event) => { + try { + const imageId = getRouterParam(event, 'id') + const query = getQuery(event) + const isPreview = query.preview === 'true' + + if (!imageId) { + throw createError({ + statusCode: 400, + statusMessage: 'Bild-ID erforderlich' + }) + } + + // Prüfe ob Benutzer eingeloggt ist + let isLoggedIn = false + const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace('Bearer ', '') + + if (token) { + const decoded = verifyToken(token) + if (decoded) { + const user = await getUserFromToken(token) + if (user && user.active) { + isLoggedIn = true + } + } + } + + const metadata = await readGalerieMetadata() + const image = metadata.find(img => img.id === imageId) + + if (!image) { + throw createError({ + statusCode: 404, + statusMessage: 'Bild nicht gefunden' + }) + } + + // Prüfe Zugriffsberechtigung + if (!image.isPublic && !isLoggedIn) { + throw createError({ + statusCode: 403, + statusMessage: 'Keine Berechtigung zum Anzeigen dieses Bildes' + }) + } + + // Bestimme Dateipfad + const filename = isPreview ? image.previewFilename : image.filename + const subdir = isPreview ? 'previews' : 'originals' + const filePath = path.join(GALERIE_DIR, subdir, filename) + + // Prüfe ob Datei existiert + try { + await fs.access(filePath) + } catch { + throw createError({ + statusCode: 404, + statusMessage: 'Bilddatei nicht gefunden' + }) + } + + // Bestimme MIME-Type + const ext = path.extname(filename).toLowerCase() + const mimeTypes = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp' + } + const contentType = mimeTypes[ext] || 'application/octet-stream' + + // Datei lesen und EXIF-Orientierung korrigieren + const imageBuffer = await fs.readFile(filePath) + const correctedBuffer = await sharp(imageBuffer) + .rotate() // Korrigiert automatisch EXIF-Orientierung + .toBuffer() + + setHeader(event, 'Content-Type', contentType) + setHeader(event, 'Cache-Control', 'public, max-age=31536000') + + return correctedBuffer + + } catch (error) { + console.error('Fehler beim Laden des Bildes:', error) + + if (error.statusCode) { + throw error + } + + throw createError({ + statusCode: 500, + statusMessage: 'Fehler beim Laden des Bildes' + }) + } +}) + diff --git a/server/api/galerie/list.get.js b/server/api/galerie/list.get.js new file mode 100644 index 0000000..d6dad8b --- /dev/null +++ b/server/api/galerie/list.get.js @@ -0,0 +1,90 @@ +import fs from 'fs/promises' +import path from 'path' +import { getUserFromToken, verifyToken } from '../../utils/auth.js' + +// Handle both dev and production paths +const getDataPath = (filename) => { + const cwd = process.cwd() + if (cwd.endsWith('.output')) { + return path.join(cwd, '../server/data', filename) + } + return path.join(cwd, 'server/data', filename) +} + +const GALERIE_METADATA = getDataPath('galerie-metadata.json') + +async function readGalerieMetadata() { + try { + const data = await fs.readFile(GALERIE_METADATA, 'utf-8') + return JSON.parse(data) + } catch (error) { + if (error.code === 'ENOENT') { + return [] + } + throw error + } +} + +export default defineEventHandler(async (event) => { + try { + // Prüfe ob Benutzer eingeloggt ist + let isLoggedIn = false + const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace('Bearer ', '') + + if (token) { + const decoded = verifyToken(token) + if (decoded) { + const user = await getUserFromToken(token) + if (user && user.active) { + isLoggedIn = true + } + } + } + + const metadata = await readGalerieMetadata() + + // Filtere Bilder basierend auf Sichtbarkeit + const visibleImages = metadata.filter(image => { + // Öffentliche Bilder sind für alle sichtbar + if (image.isPublic) return true + // Private Bilder nur für eingeloggte Mitglieder + return isLoggedIn + }) + + // Sortiere nach Upload-Datum (neueste zuerst) + visibleImages.sort((a, b) => new Date(b.uploadedAt) - new Date(a.uploadedAt)) + + // Pagination + const page = parseInt(getQuery(event).page) || 1 + const perPage = 10 + const start = (page - 1) * perPage + const end = start + perPage + const paginatedImages = visibleImages.slice(start, end) + + return { + success: true, + images: paginatedImages.map(img => ({ + id: img.id, + title: img.title, + description: img.description, + isPublic: img.isPublic, + uploadedAt: img.uploadedAt, + previewFilename: img.previewFilename + })), + pagination: { + page, + perPage, + total: visibleImages.length, + totalPages: Math.ceil(visibleImages.length / perPage) + } + } + + } catch (error) { + console.error('Fehler beim Laden der Galerie:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Fehler beim Laden der Galerie' + }) + } +}) + diff --git a/server/api/galerie/upload.post.js b/server/api/galerie/upload.post.js new file mode 100644 index 0000000..b1fdf52 --- /dev/null +++ b/server/api/galerie/upload.post.js @@ -0,0 +1,208 @@ +import multer from 'multer' +import fs from 'fs/promises' +import path from 'path' +import sharp from 'sharp' +import { getUserFromToken, verifyToken } from '../../utils/auth.js' +import { randomUUID } from 'crypto' + +// Handle both dev and production paths +const getDataPath = (filename) => { + const cwd = process.cwd() + if (cwd.endsWith('.output')) { + return path.join(cwd, '../server/data', filename) + } + return path.join(cwd, 'server/data', filename) +} + +const GALERIE_DIR = getDataPath('galerie') +const GALERIE_METADATA = getDataPath('galerie-metadata.json') + +// Multer-Konfiguration für Bild-Uploads +// Temporärer Dateiname, wird später basierend auf Titel umbenannt +const storage = multer.diskStorage({ + destination: async (req, file, cb) => { + try { + await fs.mkdir(GALERIE_DIR, { recursive: true }) + await fs.mkdir(path.join(GALERIE_DIR, 'originals'), { recursive: true }) + await fs.mkdir(path.join(GALERIE_DIR, 'previews'), { recursive: true }) + cb(null, path.join(GALERIE_DIR, 'originals')) + } catch (error) { + cb(error) + } + }, + filename: (req, file, cb) => { + // Temporärer Dateiname, wird später umbenannt + const ext = path.extname(file.originalname) + const tempFilename = `temp_${randomUUID()}${ext}` + cb(null, tempFilename) + } +}) + +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'] + if (allowedMimes.includes(file.mimetype)) { + cb(null, true) + } else { + cb(new Error('Nur Bilddateien sind erlaubt (JPEG, PNG, GIF, WebP)'), false) + } + }, + limits: { + fileSize: 10 * 1024 * 1024 // 10MB Limit + } +}) + +async function readGalerieMetadata() { + try { + const data = await fs.readFile(GALERIE_METADATA, 'utf-8') + return JSON.parse(data) + } catch (error) { + if (error.code === 'ENOENT') { + return [] + } + throw error + } +} + +async function writeGalerieMetadata(metadata) { + await fs.writeFile(GALERIE_METADATA, JSON.stringify(metadata, null, 2), 'utf-8') +} + +export default defineEventHandler(async (event) => { + try { + // Authentifizierung prüfen + const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace('Bearer ', '') + + if (!token) { + throw createError({ + statusCode: 401, + statusMessage: 'Nicht authentifiziert' + }) + } + + const decoded = verifyToken(token) + if (!decoded) { + throw createError({ + statusCode: 401, + statusMessage: 'Ungültiges Token' + }) + } + + const user = await getUserFromToken(token) + if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) { + throw createError({ + statusCode: 403, + statusMessage: 'Keine Berechtigung zum Hochladen von Bildern' + }) + } + + // Multer-Middleware für multipart/form-data + await new Promise((resolve, reject) => { + upload.single('image')(event.node.req, event.node.res, (err) => { + if (err) reject(err) + else resolve() + }) + }) + + const file = event.node.req.file + const body = event.node.req.body || {} + const isPublic = body.isPublic === 'true' || body.isPublic === true + + if (!file) { + throw createError({ + statusCode: 400, + statusMessage: 'Keine Bilddatei hochgeladen' + }) + } + + // Titel ist Pflichtfeld + if (!body.title || !body.title.trim()) { + // Lösche die hochgeladene Datei + await fs.unlink(file.path).catch(() => {}) + throw createError({ + statusCode: 400, + statusMessage: 'Titel ist ein Pflichtfeld' + }) + } + + // Generiere Dateinamen basierend auf Titel + const titleSlug = body.title.trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .substring(0, 100) // Max 100 Zeichen + const ext = path.extname(file.originalname) + const filename = `${titleSlug}_${randomUUID().substring(0, 8)}${ext}` + const previewFilename = `preview_${filename}` + + // Verschiebe die Datei zum neuen Namen + const originalPath = path.join(GALERIE_DIR, 'originals', filename) + await fs.rename(file.path, originalPath) + + const previewPath = path.join(GALERIE_DIR, 'previews', previewFilename) + + // Thumbnail erstellen (150x150px) mit automatischer EXIF-Orientierungskorrektur + await sharp(originalPath) + .rotate() // Korrigiert automatisch EXIF-Orientierung + .resize(150, 150, { + fit: 'inside', + withoutEnlargement: true + }) + .toFile(previewPath) + + // Metadaten speichern + const metadata = await readGalerieMetadata() + const newImage = { + id: randomUUID(), + filename, + previewFilename, + title: body.title.trim(), + description: body.description || '', + isPublic, + uploadedBy: user.id, + uploadedAt: new Date().toISOString(), + originalName: file.originalname + } + + metadata.push(newImage) + await writeGalerieMetadata(metadata) + + return { + success: true, + message: 'Bild erfolgreich hochgeladen', + image: { + id: newImage.id, + title: newImage.title, + isPublic: newImage.isPublic + } + } + + } catch (error) { + console.error('Fehler beim Bild-Upload:', error) + + if (error.statusCode) { + throw error + } + + if (error.code === 'LIMIT_FILE_SIZE') { + throw createError({ + statusCode: 413, + statusMessage: 'Datei zu groß (max. 10MB)' + }) + } + + if (error.message && error.message.includes('Nur Bilddateien')) { + throw createError({ + statusCode: 400, + statusMessage: error.message + }) + } + + throw createError({ + statusCode: 500, + statusMessage: 'Fehler beim Hochladen des Bildes' + }) + } +}) + diff --git a/server/data/galerie-metadata.json b/server/data/galerie-metadata.json new file mode 100644 index 0000000..32960f8 --- /dev/null +++ b/server/data/galerie-metadata.json @@ -0,0 +1,2 @@ +[ +] \ No newline at end of file diff --git a/server/data/users.json b/server/data/users.json index 242942e..5f5658c 100644 --- a/server/data/users.json +++ b/server/data/users.json @@ -1 +1 @@ -hvxN6KvvickWNnxyvIqXHN9VWFkXlbP9ivRinrShBA0a9Pq7y9QwL23SVGG/8Eu1OrbERAnSJjzyVKKfaR/5gn+a5Q+3Z8MXt+UfADTvNJB4jU+IISnRsEw8T9gXMQQNvkUygoF+qI19+vqdEjMSXTG3HvsWqvgp/0fcoT++i3v2L7UfReXm9hBJti2EiqrD03mH6t+PW31yPe8HxJTnSTCJaHzYhtjsanHE3IeYukBVhSflwtInm1hRecux1Hu1/gfnf+FqtG+9aJp81Ixdmuekk5WNckPblavBsrOHnKYHaT3GExpbK60rls2zgLqMkINg5GqePKRLIffLNU+CbOTTZkgYcLEWN4ICMGxAWAbgOxS8QLTg9iKsmhb7KkB1i4emqCFMSAbeLphbbxgEeA== \ No newline at end of file +vt5myp1IVj2hMck3wi+hrAym+ZAIGNkg5zeSZcHwpt8NV9ZIj3KD1bPEbzTT7LhmlgspNL/HmTYwdUYN/yoxOxZ5d3usU+/q690XcuP4j4PzMtRc+xXVlA2oZT2lszkZtw0sm9auHI7NCAIViCqfpmnAtjsJPy9Pguni/9BH5hMJtNzR1zg0wIgigqA0eYLatRyMusk+hq0Bv2qodwOH0V6kQ9NHAj6lR6Dehs/nO8R+qjgtvWgYjxPR8RMtn62s8zFki3YcXi8Zweb/I0XUTS9VV4EukyZXpEGDs7ECiN6nesYNAHSB/PhC8rqrPjUPPna2s2sZjVgfY8WueuODw5oArRGfgzDhCz/eqpTS5pjMSrGJ8AygrC7R+l5KSSsMN2hHn/AwY6PAhUtbLe3mmQ== \ No newline at end of file diff --git a/tests/galerie-endpoints.spec.ts b/tests/galerie-endpoints.spec.ts new file mode 100644 index 0000000..cc999ae --- /dev/null +++ b/tests/galerie-endpoints.spec.ts @@ -0,0 +1,163 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createEvent, mockSuccessReadBody } from './setup' +import fs from 'fs/promises' +import sharp from 'sharp' + +vi.mock('../server/utils/auth.js', () => ({ + getUserFromToken: vi.fn(), + verifyToken: vi.fn(), + readUsers: vi.fn(), + writeUsers: vi.fn() +})) + +vi.mock('sharp', () => ({ + default: vi.fn(() => ({ + resize: vi.fn().mockReturnThis(), + toFile: vi.fn().mockResolvedValue({}) + })) +})) + +vi.mock('multer', () => { + const single = vi.fn((field) => (req, _res, cb) => { + if (req.__mockMulterError) { + cb(req.__mockMulterError) + return + } + req.file = req.__mockFile || null + req.body = req.body || {} + cb(null) + }) + + const multerFn = vi.fn(() => ({ single })) + const diskStorage = vi.fn(() => ({})) + multerFn.diskStorage = diskStorage + + return { + default: multerFn, + diskStorage + } +}) + +const authUtils = await import('../server/utils/auth.js') + +import uploadHandler from '../server/api/galerie/upload.post.js' +import listHandler from '../server/api/galerie/list.get.js' +import imageHandler from '../server/api/galerie/[id].get.js' + +describe('Galerie API Endpoints', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.spyOn(fs, 'readFile').mockResolvedValue('[]') + vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined) + vi.spyOn(fs, 'mkdir').mockResolvedValue(undefined) + vi.spyOn(fs, 'access').mockResolvedValue(undefined) + }) + + describe('POST /api/galerie/upload', () => { + it('erfordert Authentifizierung', async () => { + const event = createEvent({ method: 'POST' }) + event.node.req.__mockFile = { filename: 'test.jpg', path: 'tmp/test.jpg', originalname: 'test.jpg', mimetype: 'image/jpeg' } + event.node.req.body = { title: 'Test', isPublic: 'true' } + + await expect(uploadHandler(event)).rejects.toMatchObject({ statusCode: 401 }) + }) + + it('erfordert Admin- oder Vorstand-Rolle', async () => { + const event = createEvent({ method: 'POST', cookies: { auth_token: 'token' } }) + event.node.req.__mockFile = { filename: 'test.jpg', path: 'tmp/test.jpg', originalname: 'test.jpg', mimetype: 'image/jpeg' } + event.node.req.body = { title: 'Test', isPublic: 'true' } + authUtils.verifyToken.mockReturnValue({ id: '1' }) + authUtils.getUserFromToken.mockResolvedValue({ id: '1', role: 'mitglied', active: true }) + + await expect(uploadHandler(event)).rejects.toMatchObject({ statusCode: 403 }) + }) + + it('lädt Bild hoch und erstellt Thumbnail', async () => { + const event = createEvent({ method: 'POST', cookies: { auth_token: 'token' } }) + event.node.req.__mockFile = { filename: 'test.jpg', path: 'tmp/test.jpg', originalname: 'test.jpg', mimetype: 'image/jpeg' } + event.node.req.body = { title: 'Test Bild', description: 'Beschreibung', isPublic: 'true' } + authUtils.verifyToken.mockReturnValue({ id: '1' }) + authUtils.getUserFromToken.mockResolvedValue({ id: '1', role: 'admin', active: true }) + + const response = await uploadHandler(event) + + expect(response.success).toBe(true) + expect(sharp).toHaveBeenCalled() + expect(fs.writeFile).toHaveBeenCalled() + }) + }) + + describe('GET /api/galerie/list', () => { + it('zeigt öffentliche Bilder für alle', async () => { + const event = createEvent() + vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify([ + { id: '1', title: 'Öffentlich', isPublic: true, previewFilename: 'preview_1.jpg', uploadedAt: '2025-01-01' }, + { id: '2', title: 'Privat', isPublic: false, previewFilename: 'preview_2.jpg', uploadedAt: '2025-01-02' } + ])) + + const response = await listHandler(event) + + expect(response.success).toBe(true) + expect(response.images).toHaveLength(1) + expect(response.images[0].id).toBe('1') + }) + + it('zeigt alle Bilder für eingeloggte Mitglieder', async () => { + const event = createEvent({ cookies: { auth_token: 'token' } }) + vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify([ + { id: '1', title: 'Öffentlich', isPublic: true, previewFilename: 'preview_1.jpg', uploadedAt: '2025-01-01' }, + { id: '2', title: 'Privat', isPublic: false, previewFilename: 'preview_2.jpg', uploadedAt: '2025-01-02' } + ])) + authUtils.verifyToken.mockReturnValue({ id: '1' }) + authUtils.getUserFromToken.mockResolvedValue({ id: '1', active: true }) + + const response = await listHandler(event) + + expect(response.images).toHaveLength(2) + }) + }) + + describe('GET /api/galerie/[id]', () => { + it('verweigert Zugriff auf private Bilder für nicht eingeloggte Benutzer', async () => { + const event = createEvent() + event.context.params = { id: '1' } + vi.spyOn(fs, 'readFile') + .mockResolvedValueOnce(JSON.stringify([ + { id: '1', filename: 'test.jpg', previewFilename: 'preview_test.jpg', isPublic: false } + ])) + + await expect(imageHandler(event)).rejects.toMatchObject({ statusCode: 403 }) + }) + + it('liefert Bild für eingeloggte Mitglieder', async () => { + const event = createEvent({ cookies: { auth_token: 'token' } }) + event.context.params = { id: '1' } + vi.spyOn(fs, 'readFile') + .mockResolvedValueOnce(JSON.stringify([ + { id: '1', filename: 'test.jpg', previewFilename: 'preview_test.jpg', isPublic: false } + ])) + .mockResolvedValueOnce(Buffer.from('image data')) + authUtils.verifyToken.mockReturnValue({ id: '1' }) + authUtils.getUserFromToken.mockResolvedValue({ id: '1', active: true }) + + const response = await imageHandler(event) + + expect(response).toBeInstanceOf(Buffer) + }) + + it('liefert Preview-Bild bei preview=true', async () => { + const event = createEvent({ query: { preview: 'true' } }) + event.context.params = { id: '1' } + vi.spyOn(fs, 'readFile') + .mockResolvedValueOnce(JSON.stringify([ + { id: '1', filename: 'test.jpg', previewFilename: 'preview_test.jpg', isPublic: true } + ])) + .mockResolvedValueOnce(Buffer.from('preview data')) + + const response = await imageHandler(event) + + expect(response).toBeInstanceOf(Buffer) + }) + }) +}) + diff --git a/tests/setup.ts b/tests/setup.ts index aeb1454..2e272bd 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -20,11 +20,20 @@ const getCookieStore = (event: any): CookieStore => { global.readBody = vi.fn(async (event: any) => event.__body ?? null) global.getQuery = (event: any) => event.__query ?? {} +global.getRouterParam = (event: any, name: string) => { + return event.context?.params?.[name] ?? null +} global.getHeader = (event: any, name: string) => { const headers = event.node?.req?.headers || {} const key = Object.keys(headers).find(h => h.toLowerCase() === name.toLowerCase()) return key ? headers[key] : undefined } +global.setHeader = vi.fn((event: any, name: string, value: string) => { + if (event.node?.res) { + event.node.res.headers = event.node.res.headers || {} + event.node.res.headers[name] = value + } +}) global.setCookie = (event: any, name: string, value: string, options = {}) => { const store = getCookieStore(event)
Bilder werden geladen...
Noch keine Bilder in der Galerie.
{{ uploadError }}
{{ uploadSuccess }}
+ {{ selectedImage.description }} +
+ Bild {{ currentImageIndex + 1 }} von {{ images.length }} +