updated team-join-policy

This commit is contained in:
Linrador 2025-10-10 23:16:35 +02:00
parent b2d718738e
commit 2dc92c55c2
27 changed files with 464 additions and 259 deletions

109
package-lock.json generated
View File

@ -15,7 +15,7 @@
"@fortawesome/fontawesome-free": "^7.0.0", "@fortawesome/fontawesome-free": "^7.0.0",
"@preline/dropdown": "^3.0.1", "@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0", "@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.17.0", "@prisma/client": "^6.17.1",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"csgo-sharecode": "^3.1.2", "csgo-sharecode": "^3.1.2",
@ -60,7 +60,7 @@
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.0", "eslint-config-next": "15.3.0",
"prisma": "^6.17.0", "prisma": "^6.17.1",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.19.4", "tsx": "^4.19.4",
@ -85,7 +85,6 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -123,6 +122,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
@ -1577,7 +1577,6 @@
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
} }
@ -1598,9 +1597,9 @@
"license": "Licensed under MIT and Preline UI Fair Use License" "license": "Licensed under MIT and Preline UI Fair Use License"
}, },
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "6.17.0", "version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.1.tgz",
"integrity": "sha512-b42mTLOdLEZ6e/igu8CLdccAUX9AwHknQQ1+pHOftnzDP2QoyZyFvcANqSLs5ockimFKJnV7Ljf+qrhNYf6oAg==", "integrity": "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@ -1620,9 +1619,9 @@
} }
}, },
"node_modules/@prisma/config": { "node_modules/@prisma/config": {
"version": "6.17.0", "version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz",
"integrity": "sha512-k8tuChKpkO/Vj7ZEzaQMNflNGbaW4X0r8+PC+W2JaqVRdiS2+ORSv1SrDwNxsb8YyzIQJucXqLGZbgxD97ZhsQ==", "integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -1633,53 +1632,53 @@
} }
}, },
"node_modules/@prisma/debug": { "node_modules/@prisma/debug": {
"version": "6.17.0", "version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz",
"integrity": "sha512-eE2CB32nr1hRqyLVnOAVY6c//iSJ/PN+Yfoa/2sEzLGpORaCg61d+nvdAkYSh+6Y2B8L4BVyzkRMANLD6nnC2g==", "integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "6.17.0", "version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz",
"integrity": "sha512-XhE9v3hDQTNgCYMjogcCYKi7HCEkZf9WwTGuXy8cmY8JUijvU0ap4M7pGLx4pBblkp5EwUsYzw1YLtH7yi0GZw==", "integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.17.0", "@prisma/debug": "6.17.1",
"@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a", "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"@prisma/fetch-engine": "6.17.0", "@prisma/fetch-engine": "6.17.1",
"@prisma/get-platform": "6.17.0" "@prisma/get-platform": "6.17.1"
} }
}, },
"node_modules/@prisma/engines-version": { "node_modules/@prisma/engines-version": {
"version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a", "version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac.tgz",
"integrity": "sha512-G0VU4uFDreATgTz4sh3dTtU2C+jn+J6c060ixavWZaUaSRZsNQhSPW26lbfez7GHzR02RGCdqs5UcSuGBC3yLw==", "integrity": "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "6.17.0", "version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz",
"integrity": "sha512-YSl5R3WIAPrmshYPkaaszOsBIWRAovOCHn3y7gkTNGG51LjYW4pi6PFNkGouW6CA06qeTjTbGrDRCgFjnmVWDg==", "integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.17.0", "@prisma/debug": "6.17.1",
"@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a", "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"@prisma/get-platform": "6.17.0" "@prisma/get-platform": "6.17.1"
} }
}, },
"node_modules/@prisma/get-platform": { "node_modules/@prisma/get-platform": {
"version": "6.17.0", "version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz",
"integrity": "sha512-3tEKChrnlmLXPd870oiVfRvj7vVKuxqP349hYaMDsbV4TZd3+lFqw8KTI2Tbq5DopamfNuNqhVCj+R6ZxKKYGQ==", "integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.17.0" "@prisma/debug": "6.17.1"
} }
}, },
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
@ -2068,6 +2067,7 @@
"integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==", "integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
} }
@ -2085,6 +2085,7 @@
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -2182,6 +2183,7 @@
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.30.1", "@typescript-eslint/scope-manager": "8.30.1",
"@typescript-eslint/types": "8.30.1", "@typescript-eslint/types": "8.30.1",
@ -2615,6 +2617,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3283,7 +3286,6 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@ -3424,6 +3426,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
@ -3880,6 +3883,7 @@
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -4054,6 +4058,7 @@
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8", "array-includes": "^3.1.8",
@ -5402,7 +5407,6 @@
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
} }
@ -5831,7 +5835,6 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"yallist": "^4.0.0" "yallist": "^4.0.0"
}, },
@ -6019,6 +6022,7 @@
"resolved": "https://registry.npmjs.org/next/-/next-15.3.0.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.3.0.tgz",
"integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==", "integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@next/env": "15.3.0", "@next/env": "15.3.0",
"@swc/counter": "0.1.3", "@swc/counter": "0.1.3",
@ -6311,8 +6315,7 @@
"version": "0.9.15", "version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -6329,7 +6332,6 @@
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
} }
@ -6458,7 +6460,6 @@
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
"integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": "^10.13.0 || >=12.0.0" "node": "^10.13.0 || >=12.0.0"
} }
@ -6745,7 +6746,6 @@
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"pretty-format": "^3.8.0" "pretty-format": "^3.8.0"
}, },
@ -6776,19 +6776,19 @@
"version": "3.8.0", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/prisma": { "node_modules/prisma": {
"version": "6.17.0", "version": "6.17.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz",
"integrity": "sha512-rcvldz98r+2bVCs0MldQCBaaVJRCj9Ew4IqphLATF89OJcSzwRQpwnKXR+W2+2VjK7/o2x3ffu5+2N3Muu6Dbw==", "integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@prisma/config": "6.17.0", "@prisma/config": "6.17.1",
"@prisma/engines": "6.17.0" "@prisma/engines": "6.17.1"
}, },
"bin": { "bin": {
"prisma": "build/index.js" "prisma": "build/index.js"
@ -6902,6 +6902,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -6911,6 +6912,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@ -6980,8 +6982,7 @@
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
@ -7638,7 +7639,8 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
"integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==", "integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.1",
@ -7695,6 +7697,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -7920,6 +7923,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -8175,8 +8179,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC", "license": "ISC"
"peer": true
}, },
"node_modules/yn": { "node_modules/yn": {
"version": "3.1.1", "version": "3.1.1",

View File

@ -21,7 +21,7 @@
"@fortawesome/fontawesome-free": "^7.0.0", "@fortawesome/fontawesome-free": "^7.0.0",
"@preline/dropdown": "^3.0.1", "@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0", "@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.17.0", "@prisma/client": "^6.17.1",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"csgo-sharecode": "^3.1.2", "csgo-sharecode": "^3.1.2",
@ -66,7 +66,7 @@
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.0", "eslint-config-next": "15.3.0",
"prisma": "^6.17.0", "prisma": "^6.17.1",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.19.4", "tsx": "^4.19.4",

View File

@ -2,7 +2,7 @@
'use client' 'use client'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState, useRef } from 'react'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import TeamCard from './TeamCard' import TeamCard from './TeamCard'
import type { Team, Player } from '../../../types/team' import type { Team, Player } from '../../../types/team'
@ -89,18 +89,41 @@ export default function NoTeamView({ initialTeams, initialInvitationMap }: Props
if (currentSteamId && !isConnected) connect(currentSteamId) if (currentSteamId && !isConnected) connect(currentSteamId)
}, [currentSteamId, isConnected, connect]) }, [currentSteamId, isConnected, connect])
useEffect(() => { fetchTeamsAndInvitations() }, []) useEffect(() => {
// Nur nachladen, falls keine Initialdaten übergeben wurden
if (!initialTeams?.length) fetchTeamsAndInvitations()
}, [])
const teamsRef = useRef(teams)
useEffect(() => { teamsRef.current = teams }, [teams])
const lastSigRef = useRef<string | null>(null)
useEffect(() => { useEffect(() => {
if (!lastEvent) return if (!lastEvent) return
// Signatur: Typ + die paar Payload-Felder, die sich bei uns ändern
const sig = JSON.stringify({
t: lastEvent.type,
tid: lastEvent.payload?.teamId ?? null,
jp: lastEvent.payload?.joinPolicy ?? null,
f: lastEvent.payload?.filename ?? null,
v: lastEvent.payload?.version ?? null,
})
if (lastSigRef.current === sig) return
lastSigRef.current = sig
const { type, payload } = lastEvent const { type, payload } = lastEvent
if (TEAM_EVENTS.has(type)) { if (TEAM_EVENTS.has(type)) {
if (!payload?.teamId || teams.some(t => t.id === payload.teamId)) fetchTeamsAndInvitations() if (!payload?.teamId || teamsRef.current.some(t => t.id === payload.teamId)) {
fetchTeamsAndInvitations()
}
return return
} }
if (INVITE_EVENTS.has(type)) fetchTeamsAndInvitations() if (INVITE_EVENTS.has(type)) {
// eslint-disable-next-line react-hooks/exhaustive-deps fetchTeamsAndInvitations()
}, [lastEvent, teams]) }
}, [lastEvent])
const visibleTeams = useMemo(() => { const visibleTeams = useMemo(() => {
const q = query.trim().toLowerCase() const q = query.trim().toLowerCase()

View File

@ -1,7 +1,7 @@
// /src/app/components/TeamCard.tsx // /src/app/components/TeamCard.tsx
'use client' 'use client'
import { useState, useMemo, useEffect } from 'react' import { useState, useMemo, useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import Button from './Button' import Button from './Button'
import TeamPremierRankBadge from './TeamPremierRankBadge' import TeamPremierRankBadge from './TeamPremierRankBadge'
@ -36,11 +36,10 @@ export default function TeamCard({
// ⬇️ NEU: lokale, “wirksame” Policy startet mit Prop // ⬇️ NEU: lokale, “wirksame” Policy startet mit Prop
const [effectivePolicy, setEffectivePolicy] = useState<TeamJoinPolicy>(team.joinPolicy) const [effectivePolicy, setEffectivePolicy] = useState<TeamJoinPolicy>(team.joinPolicy)
const sseWinsUntil = useRef(0)
// Wenn sich Props ändern (neues Team oder Server-Refetch), Policy nachziehen const lastHandledKeyRef = useRef('')
useEffect(() => {
setEffectivePolicy(team.joinPolicy) const lastSeenTsRef = useRef<number | null>(null)
}, [team.id, team.joinPolicy])
// SSE-Verbindung herstellen // SSE-Verbindung herstellen
useEffect(() => { useEffect(() => {
@ -48,19 +47,39 @@ export default function TeamCard({
if (!isConnected) connect(currentUserSteamId) if (!isConnected) connect(currentUserSteamId)
}, [currentUserSteamId, isConnected, connect]) }, [currentUserSteamId, isConnected, connect])
// Auf team-updated hören und ggf. Policy übernehmen // ⬇️ Jede 'team-updated'-Änderung verarbeiten, robust entpacken, per ts deduplizieren
useEffect(() => { useEffect(() => {
if (!lastEvent || !isSseEventType(lastEvent.type)) return const ev = lastEvent
if (lastEvent.type !== 'team-updated') return if (!ev || ev.type !== 'team-updated') return
const payload = lastEvent.payload ?? {} // payload kann entweder direkt die Felder haben … oder unter payload liegen
if (payload.teamId !== team.id) return const p = (ev.payload && typeof ev.payload === 'object' && 'payload' in ev.payload)
? ev.payload.payload
: ev.payload
const jp = payload.joinPolicy as TeamJoinPolicy | undefined const tid = p?.teamId
const jp = p?.joinPolicy as TeamJoinPolicy | undefined
if (tid !== team.id) return
if (jp !== 'REQUEST' && jp !== 'INVITE_ONLY') return
// Dedupe an der Ereignis-Identität (ts stammt aus dem Store)
if (ev.ts && lastSeenTsRef.current === ev.ts) return
lastSeenTsRef.current = ev.ts ?? Date.now()
// kurzes Fenster, in dem Props-Refetch nicht wieder überschreibt
sseWinsUntil.current = Date.now() + 1500
setEffectivePolicy(jp)
}, [lastEvent?.ts, lastEvent, team.id])
// ⬇️ Props nur übernehmen, wenn kein frisches SSE dazwischenfunkt
useEffect(() => {
const jp = team.joinPolicy as TeamJoinPolicy | undefined
if (Date.now() < sseWinsUntil.current) return
if (jp === 'REQUEST' || jp === 'INVITE_ONLY') { if (jp === 'REQUEST' || jp === 'INVITE_ONLY') {
setEffectivePolicy(jp) setEffectivePolicy(prev => (prev === jp ? prev : jp))
} }
}, [lastEvent, team.id]) }, [team.id, team.joinPolicy])
// ── Stati ableiten (jetzt von effectivePolicy!) // ── Stati ableiten (jetzt von effectivePolicy!)
const isInviteOnly = effectivePolicy === 'INVITE_ONLY' const isInviteOnly = effectivePolicy === 'INVITE_ONLY'

View File

@ -1,3 +1,4 @@
// /src/app/[locale]/components/TeamMemberView.tsx // /src/app/[locale]/components/TeamMemberView.tsx
'use client' 'use client'
@ -42,8 +43,52 @@ type Props = {
adminMode?: boolean adminMode?: boolean
} }
export default function TeamMemberView({ /**
team: teamProp, * Wrapper-Komponente:
* - spiegelt optionales team-Prop in den Store
* - rendert Body erst, wenn team + Berechtigungen vorhanden sind
* Dadurch bleiben Hooks-Reihenfolgen stabil.
*/
export default function TeamMemberView(props: Props) {
const { team: storeTeam, setTeam } = useTeamStore()
// Prop -> Store spiegeln: auch bei gleicher ID relevante Felder patchen
useEffect(() => {
if (!props.team) return
const curr = useTeamStore.getState().team
if (!curr || curr.id !== props.team.id) {
setTeam(props.team as Team)
return
}
// gleiche ID → selektiv patchen
const next = props.team as Team
const diff: Partial<Team> = {}
if (curr.name !== next.name) diff.name = next.name
if (curr.logo !== next.logo) diff.logo = next.logo
if ((curr.leader?.steamId ?? null) !== (next.leader?.steamId ?? null)) diff.leader = next.leader
if (typeof next.joinPolicy === 'string' && curr.joinPolicy !== next.joinPolicy) {
diff.joinPolicy = next.joinPolicy as any
}
if (Object.keys(diff).length) setTeam({ ...curr, ...diff } as Team)
}, [props.team, setTeam])
// Guards dürfen im Wrapper stehen (kein Hook darunter bricht ab)
if (!props.adminMode && !props.currentUserSteamId) return null
const team = props.team ?? storeTeam ?? null
if (!team) return null
// Ab hier nur noch Body rendern dort gibt es keine frühen Returns mehr vor Hooks
return <TeamMemberViewBody {...props} team={team} />
}
/**
* Body-Komponente:
* - enthält ALLE übrigen Hooks in fester Reihenfolge
* - hier ist team garantiert vorhanden (nicht null)
*/
function TeamMemberViewBody({
team,
activeDragItem, activeDragItem,
isDragging, isDragging,
showLeaveModal, showLeaveModal,
@ -54,11 +99,9 @@ export default function TeamMemberView({
setActiveDragItem, setActiveDragItem,
setIsDragging, setIsDragging,
adminMode = false, adminMode = false,
}: Props) { }: Props & { team: Team }) {
const { team: storeTeam, setTeam } = useTeamStore() const { setTeam } = useTeamStore()
const team = teamProp ?? storeTeam
if (!team) return null
const teamId = team.id const teamId = team.id
const teamLeaderSteamId = team.leader?.steamId ?? '' const teamLeaderSteamId = team.leader?.steamId ?? ''
@ -88,8 +131,9 @@ export default function TeamMemberView({
const [saveSuccess, setSaveSuccess] = useState(false) const [saveSuccess, setSaveSuccess] = useState(false)
const [joinPolicy, setJoinPolicy] = useState<TeamJoinPolicy>( const [joinPolicy, setJoinPolicy] = useState<TeamJoinPolicy>(
(team as any).joinPolicy ?? 'REQUEST' (team.joinPolicy as TeamJoinPolicy) ?? 'REQUEST'
) )
const policyChangedAtRef = useRef<number | null>(null)
const [savingPolicy, setSavingPolicy] = useState(false) const [savingPolicy, setSavingPolicy] = useState(false)
const [policySaved, setPolicySaved] = useState(false) const [policySaved, setPolicySaved] = useState(false)
@ -128,14 +172,26 @@ export default function TeamMemberView({
const bb = b.map(p=>p.steamId).join(',') const bb = b.map(p=>p.steamId).join(',')
return aa === bb return aa === bb
} }
const eqSetByIds = (a: {steamId:string}[], b: {steamId:string}[]) => {
if (a.length !== b.length) return false
const sa = [...a.map(p => p.steamId)].sort()
const sb = [...b.map(p => p.steamId)].sort()
for (let i = 0; i < sa.length; i++) {
if (sa[i] !== sb[i]) return false
}
return true
}
useEffect(() => { useEffect(() => {
setJoinPolicy(((team as any)?.joinPolicy ?? 'REQUEST') as TeamJoinPolicy) // Nur setzen, wenn der Server wirklich einen Wert liefert.
}, [team?.id, (team as any)?.joinPolicy]) if (typeof team.joinPolicy === 'string') {
setJoinPolicy(team.joinPolicy as TeamJoinPolicy)
}
}, [team.id, team.joinPolicy])
// Team-Listen lokal synchronisieren // Team-Listen lokal synchronisieren
useEffect(() => { useEffect(() => {
if (!team) return
const nextActive = (team.activePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name)) const nextActive = (team.activePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
const nextInactive = (team.inactivePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name)) const nextInactive = (team.inactivePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
const nextInvited = Array.from(new Map((team.invitedPlayers ?? []).map(p => [p.steamId, p])).values()) const nextInvited = Array.from(new Map((team.invitedPlayers ?? []).map(p => [p.steamId, p])).values())
@ -160,32 +216,50 @@ export default function TeamMemberView({
// Relevante SSE-Events // Relevante SSE-Events
useEffect(() => { useEffect(() => {
if (!lastEvent || !teamId) return if (!lastEvent || !team.id) return
if (!isSseEventType(lastEvent.type)) return if (!isSseEventType(lastEvent.type)) return
const payload = lastEvent.payload ?? {} const payload = lastEvent.payload ?? {}
const now = Date.now()
// ► Spezialfall: nur Logo aktualisieren (ohne komplettes Reload) // nur joinPolicy geändert → minimal patchen
if (lastEvent.type === 'team-updated' && payload.teamId === team.id) {
return
}
// Nach lokalem Speichern kommt oft ein generisches team-updated ohne joinPolicy.
// Das würde ein Reload triggern → 1x kurz ignorieren.
if (lastEvent.type === 'team-updated' && payload.teamId === team.id) {
if (policyChangedAtRef.current && (now - policyChangedAtRef.current) < 2000) {
policyChangedAtRef.current = null
return
}
}
// nur Logo geändert → minimal patchen
if (lastEvent.type === 'team-logo-updated') { if (lastEvent.type === 'team-logo-updated') {
if (payload.teamId && payload.teamId !== team.id) return if (payload.teamId && payload.teamId !== team.id) return
const curr = useTeamStore.getState().team
const current = useTeamStore.getState().team if (payload?.filename && curr) setTeam({ ...curr, logo: payload.filename })
if (payload?.filename && current) {
setTeam({ ...current, logo: payload.filename })
}
if (payload?.version) setLogoVersion(payload.version) if (payload?.version) setLogoVersion(payload.version)
return return
} }
// andere Team/Self-Events // Rest: reload + remount NUR wenn Listen wirklich anders sind
if (!RELEVANT.has(lastEvent.type)) return if (!RELEVANT.has(lastEvent.type)) return
if (payload.teamId && payload.teamId !== team.id) return if (payload.teamId && payload.teamId !== team.id) return
;(async () => { ;(async () => {
const updated = await reloadTeam(teamId) const updated = await reloadTeam(team.id)
if (!updated) return if (!updated) return
setTeam(updated) setTeam(updated)
setEditedName(updated.name || '') setEditedName(updated.name || '')
if (typeof (updated as any).joinPolicy === 'string') {
setJoinPolicy((updated as any).joinPolicy as TeamJoinPolicy)
}
const nextActive = (updated.activePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name)) const nextActive = (updated.activePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
const nextInactive = (updated.inactivePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name)) const nextInactive = (updated.inactivePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
const nextInvited = Array.from(new Map((updated.invitedPlayers ?? []).map(p => [p.steamId, p])).values()) const nextInvited = Array.from(new Map((updated.invitedPlayers ?? []).map(p => [p.steamId, p])).values())
@ -195,12 +269,33 @@ export default function TeamMemberView({
setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited }) setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited })
return return
} }
setActivePlayers(nextActive)
setInactivePlayers(nextInactive) // 1) Set-Vergleich (Inhalt)
setInvitedPlayers(nextInvited) const contentChanged =
setRemountKey(k => k + 1) !eqSetByIds(activePlayers, nextActive) ||
!eqSetByIds(inactivePlayers, nextInactive) ||
!eqSetByIds(invitedPlayers, nextInvited)
// 2) Reihenfolge-Vergleich (nur Order)
const orderChanged =
!eqByIds(activePlayers, nextActive) ||
!eqByIds(inactivePlayers, nextInactive) ||
!eqByIds(invitedPlayers, nextInvited)
if (contentChanged) {
// IDs haben sich geändert → Listen setzen + DnD remounten (Keys bleiben!)
setActivePlayers(nextActive)
setInactivePlayers(nextInactive)
setInvitedPlayers(nextInvited)
setRemountKey(k => k + 1)
} else if (orderChanged) {
// Nur Reihenfolge/Sichtung anders → Listen setzen, aber KEIN remount
setActivePlayers(nextActive)
setInactivePlayers(nextInactive)
setInvitedPlayers(nextInvited)
}
})() })()
}, [lastEvent, teamId, setTeam]) }, [lastEvent, team.id, setTeam, activePlayers, inactivePlayers, invitedPlayers])
const handleDragStart = (event: any) => { const handleDragStart = (event: any) => {
const id = event.active.id as string const id = event.active.id as string
@ -226,19 +321,31 @@ export default function TeamMemberView({
useEffect(() => { useEffect(() => {
if (!showPolicyMenu) return if (!showPolicyMenu) return
const onDocClick = (e: MouseEvent) => {
const onOutside = (e: PointerEvent) => {
if (!policyMenuRef.current) return if (!policyMenuRef.current) return
if (!policyMenuRef.current.contains(e.target as Node)) setShowPolicyMenu(false) if (!policyMenuRef.current.contains(e.target as Node)) {
// Klick außerhalb: Menü schließen + Navigation/Drag verhindern
e.preventDefault()
e.stopPropagation()
setShowPolicyMenu(false)
}
} }
const onEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') setShowPolicyMenu(false) } const onEsc = (e: KeyboardEvent) => {
document.addEventListener('mousedown', onDocClick) if (e.key === 'Escape') setShowPolicyMenu(false)
}
// Capture-Phase, damit wir VOR Links/Drag reagieren
document.addEventListener('pointerdown', onOutside, { capture: true })
document.addEventListener('keydown', onEsc) document.addEventListener('keydown', onEsc)
return () => { return () => {
document.removeEventListener('mousedown', onDocClick) document.removeEventListener('pointerdown', onOutside, { capture: true })
document.removeEventListener('keydown', onEsc) document.removeEventListener('keydown', onEsc)
} }
}, [showPolicyMenu]) }, [showPolicyMenu])
const updateTeamMembers = async (teamId: string, active: Player[], inactive: Player[]) => { const updateTeamMembers = async (teamId: string, active: Player[], inactive: Player[]) => {
try { try {
const res = await fetch('/api/team/update-players', { const res = await fetch('/api/team/update-players', {
@ -396,24 +503,36 @@ export default function TeamMemberView({
const prev = joinPolicy const prev = joinPolicy
try { try {
setSavingPolicy(true) setSavingPolicy(true)
const res = await fetch('/api/team/update-join-policy', { const res = await fetch('/api/team/update-join-policy', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin', // oder 'include' same-origin reicht bei relativer URL credentials: 'same-origin',
cache: 'no-store', cache: 'no-store',
body: JSON.stringify({ teamId, joinPolicy: next }), body: JSON.stringify({ teamId, joinPolicy: next }), // teamId aus dem Body-Scope
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json().catch(() => ({})) const data = await res.json().catch(() => ({}))
throw new Error(data?.message ?? `Speichern fehlgeschlagen (${res.status})`) throw new Error(data?.message ?? `Speichern fehlgeschlagen (${res.status})`)
} }
const { joinPolicy: serverPolicy } = await res.json().catch(() => ({}))
const patched = (serverPolicy ?? next) as TeamJoinPolicy
setJoinPolicy(patched)
// Store patchen
const curr = useTeamStore.getState().team
if (curr && curr.id === teamId && curr.joinPolicy !== patched) {
setTeam({ ...curr, joinPolicy: patched })
}
policyChangedAtRef.current = Date.now()
setPolicySaved(true) setPolicySaved(true)
setTimeout(() => setPolicySaved(false), 2000) setTimeout(() => setPolicySaved(false), 2000)
const updated = await reloadTeam(teamId)
if (updated) setTeam(updated)
} catch (e) { } catch (e) {
// 🔙 Optimistisches Set zurückrollen
setJoinPolicy(prev) setJoinPolicy(prev)
console.error(e) console.error(e)
alert((e as Error).message || 'Speichern fehlgeschlagen') alert((e as Error).message || 'Speichern fehlgeschlagen')
@ -575,8 +694,6 @@ export default function TeamMemberView({
}) })
} }
if (!adminMode && !currentUserSteamId) return null
const manageSteam: string = adminMode ? teamLeaderSteamId : currentUserSteamId const manageSteam: string = adminMode ? teamLeaderSteamId : currentUserSteamId
const renderMemberList = (players: Player[]) => ( const renderMemberList = (players: Player[]) => (
@ -784,7 +901,9 @@ export default function TeamMemberView({
<div className="relative" ref={policyMenuRef}> <div className="relative" ref={policyMenuRef}>
<button <button
type="button" type="button"
onClick={() => setShowPolicyMenu(v => !v)} onPointerDownCapture={(e) => { e.stopPropagation(); }} // verhindert Drag/Link schon sehr früh
onMouseDown={(e) => e.stopPropagation()} // fallback
onClick={(e) => { e.stopPropagation(); setShowPolicyMenu(v => !v) }}
className="h-[32px] px-2.5 rounded-full text-xs border border-gray-300 dark:border-neutral-600 className="h-[32px] px-2.5 rounded-full text-xs border border-gray-300 dark:border-neutral-600
bg-white dark:bg-neutral-800 text-gray-700 dark:text-neutral-200 bg-white dark:bg-neutral-800 text-gray-700 dark:text-neutral-200
hover:bg-gray-100 hover:dark:bg-neutral-700 inline-flex items-center gap-1" hover:bg-gray-100 hover:dark:bg-neutral-700 inline-flex items-center gap-1"
@ -807,34 +926,44 @@ export default function TeamMemberView({
</button> </button>
{showPolicyMenu && ( {showPolicyMenu && (
<div className="absolute right-0 z-10 mt-1 w-56 rounded-md border border-gray-200 dark:border-neutral-700 <>
bg-white dark:bg-neutral-800 shadow-lg p-1"> <div
<button className="absolute right-0 z-[60] mt-1 w-56 rounded-md border border-gray-200
onClick={() => applyPolicy('REQUEST')} dark:border-neutral-700 bg-white dark:bg-neutral-800 shadow-lg p-1"
className={`w-full text-left px-2.5 py-2 rounded-md text-sm onPointerDownCapture={(e) => e.stopPropagation()} // Klicks bleiben im Menü
${joinPolicy === 'REQUEST' onClick={(e) => e.stopPropagation()}
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-200'
: 'hover:bg-gray-100 dark:hover:bg-neutral-700 text-gray-800 dark:text-neutral-200'}`}
> >
<div className="font-medium">Mit Genehmigung</div> <button
<div className="text-xs text-gray-500 dark:text-neutral-400"> type="button"
Spieler stellen eine Anfrage; Leader entscheidet. onPointerDownCapture={(e) => e.stopPropagation()}
</div> onClick={(e) => { e.stopPropagation(); applyPolicy('REQUEST') }}
</button> className={`w-full text-left px-2.5 py-2 rounded-md text-sm
${joinPolicy === 'REQUEST'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-200'
: 'hover:bg-gray-100 dark:hover:bg-neutral-700 text-gray-800 dark:text-neutral-200'}`}
>
<div className="font-medium">Mit Genehmigung</div>
<div className="text-xs text-gray-500 dark:text-neutral-400">
Spieler stellen eine Anfrage; Leader entscheidet.
</div>
</button>
<button <button
onClick={() => applyPolicy('INVITE_ONLY')} type="button"
className={`w-full text-left px-2.5 py-2 rounded-md text-sm onPointerDownCapture={(e) => e.stopPropagation()}
${joinPolicy === 'INVITE_ONLY' onClick={(e) => { e.stopPropagation(); applyPolicy('INVITE_ONLY') }}
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-200' className={`w-full text-left px-2.5 py-2 rounded-md text-sm
: 'hover:bg-gray-100 dark:hover:bg-neutral-700 text-gray-800 dark:text-neutral-200'}`} ${joinPolicy === 'INVITE_ONLY'
> ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-200'
<div className="font-medium">Nur Einladung</div> : 'hover:bg-gray-100 dark:hover:bg-neutral-700 text-gray-800 dark:text-neutral-200'}`}
<div className="text-xs text-gray-500 dark:text-neutral-400"> >
Beitritt nur per Einladung. <div className="font-medium">Nur Einladung</div>
</div> <div className="text-xs text-gray-500 dark:text-neutral-400">
</button> Beitritt nur per Einladung.
</div> </div>
</button>
</div>
</>
)} )}
</div> </div>
{/* 🔼 Ende Policy-Pill */} {/* 🔼 Ende Policy-Pill */}

View File

@ -68,7 +68,7 @@ export default function TeamPageClient() {
} }
return ( return (
<Card maxWidth="auto"> <Card maxWidth="auto" bodyScrollable>
<TeamCardComponent <TeamCardComponent
initialTeams={teams} initialTeams={teams}
initialInvitationMap={invitationMap} initialInvitationMap={invitationMap}

View File

@ -2,27 +2,26 @@
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth' import { sessionAuthOptions } from '@/lib/auth' // ⬅️ hier umstellen
import { sendServerSSEMessage } from '@/lib/sse-server-client' import { sendServerSSEMessage } from '@/lib/sse-server-client'
import type { TeamJoinPolicy } from '@/types/team' import type { TeamJoinPolicy } from '@/types/team'
export const runtime = 'nodejs' // ✅ Prisma-kompatibel export const runtime = 'nodejs'
export const dynamic = 'force-dynamic' // (nur Vorsicht, POST ist eh dynamisch) export const dynamic = 'force-dynamic'
const ALLOWED = ['REQUEST', 'INVITE_ONLY'] as const const ALLOWED = ['REQUEST', 'INVITE_ONLY'] as const
type AllowedPolicy = (typeof ALLOWED)[number] type AllowedPolicy = (typeof ALLOWED)[number]
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
// ─ Session ─ // ⬇️ statt getServerSession(authOptions(req))
const session = await getServerSession(authOptions(req)) const session = await getServerSession(sessionAuthOptions)
const meId = session?.user?.steamId const meId = session?.user?.steamId
if (!meId) { if (!meId) {
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 }) return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
} }
// ─ Input ─
const body = await req.json().catch(() => ({} as any)) const body = await req.json().catch(() => ({} as any))
const teamId: string | undefined = body?.teamId const teamId: string | undefined = body?.teamId
const joinPolicy: TeamJoinPolicy | undefined = body?.joinPolicy const joinPolicy: TeamJoinPolicy | undefined = body?.joinPolicy
@ -34,15 +33,10 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ message: 'Ungültige joinPolicy' }, { status: 400 }) return NextResponse.json({ message: 'Ungültige joinPolicy' }, { status: 400 })
} }
// ─ Daten ─
const [team, me] = await Promise.all([ const [team, me] = await Promise.all([
prisma.team.findUnique({ prisma.team.findUnique({
where: { id: teamId }, where: { id: teamId },
select: { select: { id: true, leaderId: true, joinPolicy: true },
id: true,
leaderId: true,
joinPolicy: true,
},
}), }),
prisma.user.findUnique({ prisma.user.findUnique({
where: { steamId: meId }, where: { steamId: meId },
@ -70,8 +64,7 @@ export async function POST(req: NextRequest) {
select: { id: true, joinPolicy: true }, select: { id: true, joinPolicy: true },
}) })
// ─ SSE (nicht blockierend!) ─ // Fire-and-forget SSE
// Kein await → Request-Antwort wird NICHT aufgehalten
Promise.resolve().then(() => Promise.resolve().then(() =>
sendServerSSEMessage({ sendServerSSEMessage({
type: 'team-updated', type: 'team-updated',

View File

@ -35,12 +35,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.17.0 * Prisma Client JS version: 6.16.3
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a * Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.17.0", client: "6.16.3",
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a" engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
} }
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError; Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@ -418,7 +418,7 @@ const config = {
"value": "prisma-client-js" "value": "prisma-client-js"
}, },
"output": { "output": {
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma", "value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
"fromEnvVar": null "fromEnvVar": null
}, },
"config": { "config": {
@ -432,7 +432,7 @@ const config = {
} }
], ],
"previewFeatures": [], "previewFeatures": [],
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma", "sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"isCustomOutput": true "isCustomOutput": true
}, },
"relativeEnvPaths": { "relativeEnvPaths": {
@ -440,8 +440,8 @@ const config = {
"schemaEnvPath": "../../../.env" "schemaEnvPath": "../../../.env"
}, },
"relativePath": "../../../prisma", "relativePath": "../../../prisma",
"clientVersion": "6.17.0", "clientVersion": "6.16.3",
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a", "engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
"datasourceNames": [ "datasourceNames": [
"db" "db"
], ],

View File

@ -20,12 +20,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.17.0 * Prisma Client JS version: 6.16.3
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a * Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.17.0", client: "6.16.3",
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a" engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
} }
Prisma.PrismaClientKnownRequestError = () => { Prisma.PrismaClientKnownRequestError = () => {

View File

@ -499,8 +499,8 @@ export namespace Prisma {
export import Exact = $Public.Exact export import Exact = $Public.Exact
/** /**
* Prisma Client JS version: 6.17.0 * Prisma Client JS version: 6.16.3
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a * Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/ */
export type PrismaVersion = { export type PrismaVersion = {
client: string client: string

View File

@ -35,12 +35,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.17.0 * Prisma Client JS version: 6.16.3
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a * Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.17.0", client: "6.16.3",
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a" engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
} }
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError; Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@ -419,7 +419,7 @@ const config = {
"value": "prisma-client-js" "value": "prisma-client-js"
}, },
"output": { "output": {
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma", "value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
"fromEnvVar": null "fromEnvVar": null
}, },
"config": { "config": {
@ -433,7 +433,7 @@ const config = {
} }
], ],
"previewFeatures": [], "previewFeatures": [],
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma", "sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"isCustomOutput": true "isCustomOutput": true
}, },
"relativeEnvPaths": { "relativeEnvPaths": {
@ -441,8 +441,8 @@ const config = {
"schemaEnvPath": "../../../.env" "schemaEnvPath": "../../../.env"
}, },
"relativePath": "../../../prisma", "relativePath": "../../../prisma",
"clientVersion": "6.17.0", "clientVersion": "6.16.3",
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a", "engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
"datasourceNames": [ "datasourceNames": [
"db" "db"
], ],

View File

@ -151,7 +151,7 @@
}, },
"./*": "./*" "./*": "./*"
}, },
"version": "6.17.0", "version": "6.16.3",
"sideEffects": false, "sideEffects": false,
"imports": { "imports": {
"#wasm-engine-loader": { "#wasm-engine-loader": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3062,7 +3062,6 @@ declare type QueryPlanNode = {
args: { args: {
from: QueryPlanNode; from: QueryPlanNode;
to: QueryPlanNode; to: QueryPlanNode;
fields: string[];
}; };
} | { } | {
type: 'initializeRecord'; type: 'initializeRecord';

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -35,12 +35,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.17.0 * Prisma Client JS version: 6.16.3
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a * Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.17.0", client: "6.16.3",
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a" engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
} }
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError; Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@ -418,7 +418,7 @@ const config = {
"value": "prisma-client-js" "value": "prisma-client-js"
}, },
"output": { "output": {
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma", "value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
"fromEnvVar": null "fromEnvVar": null
}, },
"config": { "config": {
@ -432,7 +432,7 @@ const config = {
} }
], ],
"previewFeatures": [], "previewFeatures": [],
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma", "sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"isCustomOutput": true "isCustomOutput": true
}, },
"relativeEnvPaths": { "relativeEnvPaths": {
@ -440,8 +440,8 @@ const config = {
"schemaEnvPath": "../../../.env" "schemaEnvPath": "../../../.env"
}, },
"relativePath": "../../../prisma", "relativePath": "../../../prisma",
"clientVersion": "6.17.0", "clientVersion": "6.16.3",
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a", "engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
"datasourceNames": [ "datasourceNames": [
"db" "db"
], ],

View File

@ -21,6 +21,7 @@ function guessTzFromCountry(cc?: string | null): string | null {
return null return null
} }
// 👉 Login-/Auth-Factory mit echtem Request (für /api/auth/... Routen, SignIn etc.)
export const authOptions = (req: NextRequest): NextAuthOptions => ({ export const authOptions = (req: NextRequest): NextAuthOptions => ({
secret: process.env.NEXTAUTH_SECRET, secret: process.env.NEXTAUTH_SECRET,
providers: [ providers: [
@ -32,7 +33,6 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
const steamProfile = profile as SteamProfile const steamProfile = profile as SteamProfile
const location = steamProfile.loccountrycode ?? null const location = steamProfile.loccountrycode ?? null
// create/update User (wie gehabt)
const existing = await prisma.user.findUnique({ const existing = await prisma.user.findUnique({
where: { steamId: steamProfile.steamid }, where: { steamId: steamProfile.steamid },
select: { timeZone: true }, select: { timeZone: true },
@ -68,7 +68,6 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
token.image = steamProfile.avatarfull token.image = steamProfile.avatarfull
} }
// DB laden & Flags
const userInDb = await prisma.user.findUnique({ const userInDb = await prisma.user.findUnique({
where: { steamId: token.steamId || token.sub || '' }, where: { steamId: token.steamId || token.sub || '' },
select: { teamId: true, isAdmin: true, steamId: true, timeZone: true }, select: { teamId: true, isAdmin: true, steamId: true, timeZone: true },
@ -82,7 +81,6 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
token.timeZone = undefined token.timeZone = undefined
} }
// 🎯 Faceit-Sync ausgelagert
if (token.steamId) { if (token.steamId) {
await syncFaceitProfile(prisma, token.steamId) await syncFaceitProfile(prisma, token.steamId)
} }
@ -97,10 +95,12 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
steamId: token.steamId, steamId: token.steamId,
name: token.name, name: token.name,
image: token.image, image: token.image,
team: token.team ?? null, team: (token as any).team ?? null,
isAdmin: token.isAdmin ?? false, isAdmin: (token as any).isAdmin ?? false,
timeZone: (token as any).timeZone ?? null, timeZone: (token as any).timeZone ?? null,
} as typeof session.user & { steamId: string; team: string | null; isAdmin: boolean; timeZone: string | null } } as typeof session.user & {
steamId: string; team: string | null; isAdmin: boolean; timeZone: string | null
}
return session return session
}, },
@ -112,7 +112,31 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
return url.startsWith(baseUrl) ? url : baseUrl; return url.startsWith(baseUrl) ? url : baseUrl;
}, },
}, },
session: { strategy: 'jwt' },
}) })
// Base config // ⚠️ NEU: Minimal-Options NUR für getServerSession() in App-Routen/Server Actions
export const baseAuthOptions: NextAuthOptions = authOptions({} as NextRequest) // → Kein Provider nötig (wir lesen nur die vorhandene Session)
export const sessionAuthOptions: NextAuthOptions = {
secret: process.env.NEXTAUTH_SECRET,
providers: [], // ✅ satisfies NextAuthOptions, triggert keine Provider-Init
session: { strategy: 'jwt' },
callbacks: {
async session({ session, token }) {
if (!token.steamId) throw new Error('steamId is missing in token')
session.user = {
...session.user,
steamId: token.steamId,
name: token.name,
image: token.image,
team: (token as any).team ?? null,
isAdmin: (token as any).isAdmin ?? false,
timeZone: (token as any).timeZone ?? null,
} as any
return session
},
},
}
// ❌ WEG DAMIT: Das verursacht den Crash beim Import!
// export const baseAuthOptions: NextAuthOptions = authOptions({} as NextRequest)

View File

@ -1,18 +1,18 @@
// middleware.ts
import {NextResponse} from 'next/server'; import {NextResponse} from 'next/server';
import type {NextRequest} from 'next/server'; import type {NextRequest} from 'next/server';
import createIntlMiddleware from 'next-intl/middleware'; import createIntlMiddleware from 'next-intl/middleware';
import {getToken} from 'next-auth/jwt'; import {getToken} from 'next-auth/jwt';
import {routing} from './i18n/routing'; import {routing} from './i18n/routing';
// 1) i18n-Middleware vorbereiten // 1) i18n-Middleware vorbereiten (aber NICHT für /api etc. benutzen)
const handleI18n = createIntlMiddleware(routing); const handleI18n = createIntlMiddleware(routing);
// Helpers // Helpers (deine bleiben unverändert)
function getCurrentLocaleFromPath(pathname: string, locales: readonly string[], fallback: string) { function getCurrentLocaleFromPath(pathname: string, locales: readonly string[], fallback: string) {
const first = pathname.split('/')[1]; const first = pathname.split('/')[1];
return locales.includes(first) ? first : fallback; return locales.includes(first) ? first : fallback;
} }
function stripLeadingLocale(pathname: string, locales: readonly string[]) { function stripLeadingLocale(pathname: string, locales: readonly string[]) {
const parts = pathname.split('/'); const parts = pathname.split('/');
const first = parts[1]; const first = parts[1];
@ -22,33 +22,44 @@ function stripLeadingLocale(pathname: string, locales: readonly string[]) {
} }
return pathname; return pathname;
} }
function isProtectedPath(pathnameNoLocale: string) { function isProtectedPath(pathnameNoLocale: string) {
return ( return (
pathnameNoLocale.startsWith('/dashboard') || pathnameNoLocale.startsWith('/dashboard') ||
pathnameNoLocale.startsWith('/settings') || pathnameNoLocale.startsWith('/settings') ||
pathnameNoLocale.startsWith('/matches') || pathnameNoLocale.startsWith('/matches') ||
pathnameNoLocale.startsWith('/team') || // ← hinzugefügt pathnameNoLocale.startsWith('/team') ||
pathnameNoLocale.startsWith('/admin') pathnameNoLocale.startsWith('/admin')
); );
} }
export default async function middleware(req: NextRequest) { export default async function middleware(req: NextRequest) {
// 2) Erst i18n arbeiten lassen const { pathname } = req.nextUrl;
const i18nRes = handleI18n(req);
// Wenn i18n bereits redirect/rewrite auslöst, direkt zurückgeben // 🚫 0) Harte Früh-Rückgaben: API & Statics NIE lokalisieren / autorisieren
if (
pathname.startsWith('/api') ||
pathname.startsWith('/trpc') ||
pathname.startsWith('/_next') ||
pathname.startsWith('/_vercel') ||
pathname.startsWith('/assets') ||
pathname === '/favicon.ico' ||
pathname === '/robots.txt' ||
pathname === '/sitemap.xml' ||
/\.[^/]+$/.test(pathname) // irgend eine Dateiendung
) {
return NextResponse.next();
}
// ✅ 1) i18n nur für *echte* Seiten laufen lassen
const i18nRes = handleI18n(req);
if (i18nRes.headers.get('location') || i18nRes.headers.get('x-middleware-rewrite')) { if (i18nRes.headers.get('location') || i18nRes.headers.get('x-middleware-rewrite')) {
return i18nRes; return i18nRes;
} }
// ✅ 2) Ab hier deine Auth-Logik nur für geschützte Seiten
const {locales, defaultLocale} = routing; const {locales, defaultLocale} = routing;
const url = req.nextUrl; const url = req.nextUrl;
const pathname = url.pathname;
const currentLocale = getCurrentLocaleFromPath(pathname, locales, defaultLocale);
const pathnameNoLocale = stripLeadingLocale(pathname, locales); const pathnameNoLocale = stripLeadingLocale(pathname, locales);
// 3) Nur für geschützte Pfade Auth prüfen
if (!isProtectedPath(pathnameNoLocale)) { if (!isProtectedPath(pathnameNoLocale)) {
return i18nRes; return i18nRes;
} }
@ -58,6 +69,7 @@ export default async function middleware(req: NextRequest) {
// Adminschutz // Adminschutz
if (pathnameNoLocale.startsWith('/admin')) { if (pathnameNoLocale.startsWith('/admin')) {
if (!token || !(token as any).isAdmin) { if (!token || !(token as any).isAdmin) {
const currentLocale = getCurrentLocaleFromPath(pathname, locales, defaultLocale);
const redirectUrl = url.clone(); const redirectUrl = url.clone();
redirectUrl.pathname = `/${currentLocale}/dashboard`; redirectUrl.pathname = `/${currentLocale}/dashboard`;
return NextResponse.redirect(redirectUrl); return NextResponse.redirect(redirectUrl);
@ -67,15 +79,16 @@ export default async function middleware(req: NextRequest) {
// Allgemeiner Auth-Schutz // Allgemeiner Auth-Schutz
if (!token) { if (!token) {
const loginUrl = new URL('/api/auth/signin', req.url); const loginUrl = new URL('/api/auth/signin', req.url);
loginUrl.searchParams.set('callbackUrl', url.toString()); // komplette Ziel-URL inkl. Locale loginUrl.searchParams.set('callbackUrl', url.toString());
return NextResponse.redirect(loginUrl); return NextResponse.redirect(loginUrl);
} }
// Alles gut → weiter
return i18nRes; return i18nRes;
} }
// Matcher robuster halten (gut so ich würde assets & favicon ergänzen)
export const config = { export const config = {
// Standard: alles außer /api, _next, statische Dateien matcher: [
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)' '/((?!api|trpc|_next|_vercel|assets|favicon.ico|robots.txt|sitemap.xml|.*\\..*).*)'
]
}; };

View File

@ -38,4 +38,6 @@ export type Team = {
inactivePlayers: Player[] inactivePlayers: Player[]
invitedPlayers: InvitedPlayer[] invitedPlayers: InvitedPlayer[]
players?: MatchPlayer[] players?: MatchPlayer[]
logoUpdatedAt?: string | Date | null
updatedAt?: string | Date | null
} }