From 27b8b78d2d8338c0a8f269c6423439486fcd84a9 Mon Sep 17 00:00:00 2001 From: Adrien Pensart Date: Mon, 16 Feb 2026 12:21:50 +0100 Subject: [PATCH 1/2] backend admin interface --- .gitignore | 3 + package-lock.json | 295 ++++++++++++++++--------------- package.json | 2 +- public/backend.html | 118 +++++++++++++ public/index_backend.js | 48 ++++++ public/score.html | 335 +++++++++++++++++++----------------- public/score.js | 68 +++++++- scripts/update-admin.js | 57 ++++++ src/db/schema.ts | 6 +- src/durable.ts | 30 +++- src/index.ts | 28 +++ src/services/UserService.ts | 162 +++++++++++++++-- worker-configuration.d.ts | 66 ++++++- 13 files changed, 890 insertions(+), 328 deletions(-) create mode 100644 scripts/update-admin.js diff --git a/.gitignore b/.gitignore index 893de69..12f0d0a 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,6 @@ dist .wrangler/ users.json + +# Localflare generated files +.localflare/ diff --git a/package-lock.json b/package-lock.json index 7658980..a531dd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "eslint": "^9.39.1", "typescript": "^5.5.2", "vitest": "~3.2.0", - "wrangler": "^4.60.0" + "wrangler": "^4.65.0" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -680,9 +680,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260120.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260120.0.tgz", - "integrity": "sha512-JLHx3p5dpwz4wjVSis45YNReftttnI3ndhdMh5BUbbpdreN/g0jgxNt5Qp9tDFqEKl++N63qv+hxJiIIvSLR+Q==", + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260212.0.tgz", + "integrity": "sha512-kLxuYutk88Wlo7edp8mlkN68TgZZ9237SUnuX9kNaD5jcOdblUqiBctMRZeRcPsuoX/3g2t0vS4ga02NBEVRNg==", "cpu": [ "x64" ], @@ -697,9 +697,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260120.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260120.0.tgz", - "integrity": "sha512-1Md2tCRhZjwajsZNOiBeOVGiS3zbpLPzUDjHr4+XGTXWOA6FzzwScJwQZLa0Doc28Cp4Nr1n7xGL0Dwiz1XuOA==", + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260212.0.tgz", + "integrity": "sha512-fqoqQWMA1D0ZzDOD8sp0allREM2M8GHdpxMXQ8EdZpZ70z5bJbJ9Vr4qe35++FNIZJspsDHfTw3Xm/M4ELm/dQ==", "cpu": [ "arm64" ], @@ -714,9 +714,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260120.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260120.0.tgz", - "integrity": "sha512-O0mIfJfvU7F8N5siCoRDaVDuI12wkz2xlG4zK6/Ct7U9c9FiE0ViXNFWXFQm5PPj+qbkNRyhjUwhP+GCKTk5EQ==", + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260212.0.tgz", + "integrity": "sha512-bCSQoZzDzV5MSh4ueWo1DgmOn4Hf3QBu4Yo3eQFXA2llYFIu/sZgRtkEehw1X2/SY5Sn6O0EMCqxJYRf82Wdeg==", "cpu": [ "x64" ], @@ -731,9 +731,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260120.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260120.0.tgz", - "integrity": "sha512-aRHO/7bjxVpjZEmVVcpmhbzpN6ITbFCxuLLZSW0H9O0C0w40cDCClWSi19T87Ax/PQcYjFNT22pTewKsupkckA==", + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260212.0.tgz", + "integrity": "sha512-GPvp1iiKQodtbUDi6OmR5I0vD75lawB54tdYGtmypuHC7ZOI2WhBmhb3wCxgnQNOG1z7mhCQrzRCoqrKwYbVWQ==", "cpu": [ "arm64" ], @@ -748,9 +748,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260120.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260120.0.tgz", - "integrity": "sha512-ASZIz1E8sqZQqQCgcfY1PJbBpUDrxPt8NZ+lqNil0qxnO4qX38hbCsdDF2/TDAuq0Txh7nu8ztgTelfNDlb4EA==", + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260212.0.tgz", + "integrity": "sha512-wHRI218Xn4ndgWJCUHH4Zx0YlU5q/o6OmcxXkcw95tJOsQn4lDrhppioPh4eScxJZALf2X+ODeZcyQTCq5exGw==", "cpu": [ "x64" ], @@ -5044,9 +5044,9 @@ } }, "node_modules/workerd": { - "version": "1.20260120.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260120.0.tgz", - "integrity": "sha512-R6X/VQOkwLTBGLp4VRUwLQZZVxZ9T9J8pGiJ6GQUMaRkY7TVWrCSkVfoNMM1/YyFsY5UYhhPoQe5IehnhZ3Pdw==", + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260212.0.tgz", + "integrity": "sha512-4B9BoZUzKSRv3pVZGEPh7OX+Q817hpUqAUtz5O0TxJVqo4OsYJAUA/sY177Q5ha/twjT9KaJt2DtQzE+oyCOzw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -5057,28 +5057,28 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260120.0", - "@cloudflare/workerd-darwin-arm64": "1.20260120.0", - "@cloudflare/workerd-linux-64": "1.20260120.0", - "@cloudflare/workerd-linux-arm64": "1.20260120.0", - "@cloudflare/workerd-windows-64": "1.20260120.0" + "@cloudflare/workerd-darwin-64": "1.20260212.0", + "@cloudflare/workerd-darwin-arm64": "1.20260212.0", + "@cloudflare/workerd-linux-64": "1.20260212.0", + "@cloudflare/workerd-linux-arm64": "1.20260212.0", + "@cloudflare/workerd-windows-64": "1.20260212.0" } }, "node_modules/wrangler": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.60.0.tgz", - "integrity": "sha512-n4kibm/xY0Qd5G2K/CbAQeVeOIlwPNVglmFjlDRCCYk3hZh8IggO/rg8AXt/vByK2Sxsugl5Z7yvgWxrUbmS6g==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.65.0.tgz", + "integrity": "sha512-R+n3o3tlGzLK9I4fGocPReOuvcnjhtOL2aCVKkHMeuEwt9pPbOO4FxJtx/ec5cIUG/otRyJnfQGCAr9DplBVng==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", - "@cloudflare/unenv-preset": "2.11.0", + "@cloudflare/unenv-preset": "2.12.1", "blake3-wasm": "2.1.5", - "esbuild": "0.27.0", - "miniflare": "4.20260120.0", + "esbuild": "0.27.3", + "miniflare": "4.20260212.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260120.0" + "workerd": "1.20260212.0" }, "bin": { "wrangler": "bin/wrangler.js", @@ -5091,7 +5091,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20260120.0" + "@cloudflare/workers-types": "^4.20260212.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -5110,9 +5110,9 @@ } }, "node_modules/wrangler/node_modules/@cloudflare/unenv-preset": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.11.0.tgz", - "integrity": "sha512-z3hxFajL765VniNPGV0JRStZolNz63gU3B3AktwoGdDlnQvz5nP+Ah4RL04PONlZQjwmDdGHowEStJ94+RsaJg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.12.1.tgz", + "integrity": "sha512-tP/Wi+40aBJovonSNJSsS7aFJY0xjuckKplmzDs2Xat06BJ68B6iG7YDUWXJL8gNn0gqW7YC5WhlYhO3QbugQA==", "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { @@ -5126,9 +5126,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -5143,9 +5143,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -5160,9 +5160,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -5177,9 +5177,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -5194,9 +5194,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", - "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -5211,9 +5211,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -5228,9 +5228,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -5245,9 +5245,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -5262,9 +5262,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -5279,9 +5279,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -5296,9 +5296,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -5313,9 +5313,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -5330,9 +5330,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -5347,9 +5347,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -5364,9 +5364,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -5381,9 +5381,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -5398,9 +5398,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/linux-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -5415,9 +5415,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -5432,9 +5432,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -5449,9 +5449,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -5466,9 +5466,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -5483,9 +5483,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -5500,9 +5500,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -5517,9 +5517,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -5534,9 +5534,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -5551,9 +5551,9 @@ } }, "node_modules/wrangler/node_modules/@esbuild/win32-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -5948,9 +5948,9 @@ } }, "node_modules/wrangler/node_modules/esbuild": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", - "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5961,48 +5961,47 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.0", - "@esbuild/android-arm": "0.27.0", - "@esbuild/android-arm64": "0.27.0", - "@esbuild/android-x64": "0.27.0", - "@esbuild/darwin-arm64": "0.27.0", - "@esbuild/darwin-x64": "0.27.0", - "@esbuild/freebsd-arm64": "0.27.0", - "@esbuild/freebsd-x64": "0.27.0", - "@esbuild/linux-arm": "0.27.0", - "@esbuild/linux-arm64": "0.27.0", - "@esbuild/linux-ia32": "0.27.0", - "@esbuild/linux-loong64": "0.27.0", - "@esbuild/linux-mips64el": "0.27.0", - "@esbuild/linux-ppc64": "0.27.0", - "@esbuild/linux-riscv64": "0.27.0", - "@esbuild/linux-s390x": "0.27.0", - "@esbuild/linux-x64": "0.27.0", - "@esbuild/netbsd-arm64": "0.27.0", - "@esbuild/netbsd-x64": "0.27.0", - "@esbuild/openbsd-arm64": "0.27.0", - "@esbuild/openbsd-x64": "0.27.0", - "@esbuild/openharmony-arm64": "0.27.0", - "@esbuild/sunos-x64": "0.27.0", - "@esbuild/win32-arm64": "0.27.0", - "@esbuild/win32-ia32": "0.27.0", - "@esbuild/win32-x64": "0.27.0" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/wrangler/node_modules/miniflare": { - "version": "4.20260120.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260120.0.tgz", - "integrity": "sha512-XXZyE2pDKMtP5OLuv0LPHEAzIYhov4jrYjcqrhhqtxGGtXneWOHvXIPo+eV8sqwqWd3R7j4DlEKcyb+87BR49Q==", + "version": "4.20260212.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260212.0.tgz", + "integrity": "sha512-Lgxq83EuR2q/0/DAVOSGXhXS1V7GDB04HVggoPsenQng8sqEDR3hO4FigIw5ZI2Sv2X7kIc30NCzGHJlCFIYWg==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", - "workerd": "1.20260120.0", + "workerd": "1.20260212.0", "ws": "8.18.0", - "youch": "4.1.0-beta.10", - "zod": "^3.25.76" + "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" diff --git a/package.json b/package.json index c178b47..1394fad 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "eslint": "^9.39.1", "typescript": "^5.5.2", "vitest": "~3.2.0", - "wrangler": "^4.60.0" + "wrangler": "^4.65.0" }, "dependencies": { "bcrypt-ts": "^7.1.0", diff --git a/public/backend.html b/public/backend.html index 3ab3c6e..9f24dae 100644 --- a/public/backend.html +++ b/public/backend.html @@ -16,6 +16,7 @@

Tirage automatique des tables dans {{vm.getTimerRendering(vm.timer)}}

+
@@ -78,8 +79,125 @@

+ +
+
+

Gestion des utilisateurs

+

Créer, éditer ou supprimer des comptes (admin requis).

+
+
+

Créer un utilisateur

+
+ + + + + + + +
+ +
+ +
+

Utilisateurs existants

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PseudoEmailAdminPrêtTarotDeux tablesValidité tokenDernière activitéNouveau mot de passeActions
{{vm.formatTimestamp(user.tokenValidity)}}{{vm.formatTimestamp(user.lastActiveAt)}} + + +
+
+
Frontend Liste utilisateurs
+ + diff --git a/public/index_backend.js b/public/index_backend.js index dc5ea28..cbc92fa 100644 --- a/public/index_backend.js +++ b/public/index_backend.js @@ -37,6 +37,8 @@ angular.module('meltdownAdmin', []) vm.username = ''; vm.pseudosSelected = []; vm.tables = []; + vm.users = []; + vm.newUser = { pseudo: '', email: '', password: '', admin: false, ready: false, canPlayTarot: false, canPlayTwoTables: false }; vm.authToken = (localStorage.getItem('token') || '').trim(); vm.timer = -1; if (!vm.authToken) { @@ -141,6 +143,51 @@ angular.module('meltdownAdmin', []) }) }; + vm.loadUsers = function () { + return $http.get('/admin/users/full').then((resp) => { + vm.users = resp.data.map((user) => ({ ...user, newPassword: '' })); + }); + }; + + vm.createUser = function () { + const payload = { ...vm.newUser }; + $http.post('/admin/users/create', payload).then(() => { + vm.newUser = { pseudo: '', email: '', password: '', admin: false, ready: false, canPlayTarot: false, canPlayTwoTables: false }; + vm.loadUsers(); + }); + }; + + vm.saveUser = function (user) { + const payload = { + pseudo: user.pseudo, + email: user.email, + admin: user.admin, + ready: user.ready, + canPlayTarot: user.canPlayTarot, + canPlayTwoTables: user.canPlayTwoTables, + newPassword: user.newPassword || undefined + }; + $http.post('/admin/users/update?userId=' + encodeURIComponent(user.id), payload).then(() => { + user.newPassword = ''; + vm.loadUsers(); + }); + }; + + vm.deleteUserAdmin = function (user) { + if (!window.confirm(`Supprimer ${user.pseudo} ?`)) { + return; + } + $http.delete('/admin/users/delete?userId=' + encodeURIComponent(user.id)).then(() => { + vm.loadUsers(); + }); + }; + + vm.formatTimestamp = function (value) { + if (!value) return '-'; + const date = new Date(value); + return isNaN(date.getTime()) ? '-' : date.toLocaleString(); + }; + vm.toggleUser = function (userName, checked) { if (checked) { if (!vm.pseudosSelected.includes(userName)) { @@ -158,6 +205,7 @@ angular.module('meltdownAdmin', []) vm.refreshTables().then(() => { vm.connectWebsocket(); }); + vm.loadUsers(); }); vm.refreshTimer = function () { diff --git a/public/score.html b/public/score.html index e5967ae..35d5da7 100644 --- a/public/score.html +++ b/public/score.html @@ -11,182 +11,194 @@
-

🃏 Compteur de Points Belote

- - -
-

Mode de jeu

-
- - -
+

🃏 Compteur de Points

+ +
+

Chargement de ta table...

- -
-

Nouvelle Manche

- - -
- -
- - +
+

Tu n'es sur aucune table. Rejoins une table (hors panama) pour utiliser le compteur.

+ Retour à l'accueil +
+ +
+ +
+

Mode de jeu

+
+ +
- -
- -
- + +
+

Nouvelle Manche

+ + +
+ +
+ + +
-
- - - + + +
+ +
+ +
+
+ + + +
-
- -
-
-
-

Nous

- - -
- + +
+
+
+

Nous

+ + +
+ +
-
-
-

Eux

- - -
- +
+

Eux

+ + +
+ +
-
- -
- -
- - - + +
+ +
+ + + +
-
- -
-

Résultat de la Manche

-
-
- Nous - - {{ctrl.getPreviewResult().nousScore}} - + +
+

Résultat de la Manche

+
+
+ Nous + + {{ctrl.getPreviewResult().nousScore}} + +
+
vs
+
+ Eux + + {{ctrl.getPreviewResult().euxScore}} + +
-
vs
-
- Eux - - {{ctrl.getPreviewResult().euxScore}} - +
+ {{ctrl.getPreviewResult().message}}
-
- {{ctrl.getPreviewResult().message}} -
-
- -
- - -
-

Tableau des Scores

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ManchePreneurNousEuxRésultat
{{$index + 1}} - {{round.contractTeam === 'nous' ? 'Nous' : 'Eux'}} - ({{round.contractValue}}) - {{round.nousScore}}{{round.euxScore}} - - {{round.contractSuccess ? 'Contrat réussi' : 'Chute'}} - - - -
TOTAL{{ctrl.totalNous}}{{ctrl.totalEux}} - Nous menons! - Eux mènent! - Égalité -
- -
- +
-
- -
-

📖 Rappel des Règles

-
-
Points totaux par manche: 162 points (sans bonus)
-
Belote/Rebelote: +20 points (Roi + Dame d'atout annoncés)
-
Capot: 252 points (162 + 90 bonus) pour l'équipe qui fait toutes les levées
-
- Contrat réussi (Belote): L'équipe preneuse doit faire plus de points que l'adversaire + +
+

Tableau des Scores

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ManchePreneurNousEuxRésultat
{{$index + 1}} + {{round.contractTeam === 'nous' ? 'Nous' : 'Eux'}} + ({{round.contractValue}}) + {{round.nousScore}}{{round.euxScore}} + + {{round.contractSuccess ? 'Contrat réussi' : 'Chute'}} + + + +
TOTAL{{ctrl.totalNous}}{{ctrl.totalEux}} + Nous menons! + Eux mènent! + Égalité +
+ +
+
-
Chute: L'équipe adverse marque tous les points (162 + bonus)
-
- Coinche: Les points sont multipliés (×2 coinché, ×4 surcoinché) +
+ + +
+

📖 Rappel des Règles

+
+
Points totaux par manche: 162 points (sans bonus)
+
Belote/Rebelote: +20 points (Roi + Dame d'atout annoncés)
+
Capot: 252 points (162 + 90 bonus) pour l'équipe qui fait toutes les levées
+
+ Contrat réussi (Belote): L'équipe preneuse doit faire plus de points que l'adversaire +
+
Chute: L'équipe adverse marque tous les points (162 + bonus) +
+
+ Coinche: Les points sont multipliés (×2 coinché, ×4 surcoinché) +
@@ -544,6 +556,21 @@

📖 Rappel des Règles

font-weight: bold; } + .status-banner { + margin: 12px 0; + padding: 12px; + background: var(--surface-dark); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + text-align: center; + } + + .status-link { + display: inline-block; + margin-top: 8px; + color: var(--gold); + } + @media (max-width: 600px) { .team-points { flex-direction: column; diff --git a/public/score.js b/public/score.js index da77f17..3a73475 100644 --- a/public/score.js +++ b/public/score.js @@ -1,7 +1,36 @@ angular.module('meltdownApp', []) - .controller('ScoreCtrl', ['$scope', function ($scope) { + .factory('myHttpInterceptor', function ($q) { + return { + request: function (config) { + config.headers['Authorization'] = (localStorage.getItem('token') || '').trim(); + return config; + }, + response: function (response) { + return response; + }, + responseError: function (rejection) { + console.error('Error response intercepted:', rejection); + if (rejection.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/login'; + } + return $q.reject(rejection); + } + }; + }) + .config(function ($httpProvider) { + $httpProvider.interceptors.push('myHttpInterceptor'); + }) + .controller('ScoreCtrl', ['$scope', '$http', function ($scope, $http) { const vm = this; + vm.isOnTable = false; + vm.loadingTableStatus = true; + vm.authToken = (localStorage.getItem('token') || '').trim(); + if (!vm.authToken) { + window.location.href = '/login'; + } + // Game mode: 'belote' or 'coinche' vm.gameMode = 'belote'; @@ -315,6 +344,43 @@ angular.module('meltdownApp', []) } }; + vm.refreshTablePresence = function () { + vm.isOnTable = false; + return $http.get('/tables').then((resp) => { + const tablesData = resp.data; + tablesData.forEach((fullTable) => { + let users = []; + for (var team of fullTable.teams) { + users = [...users, ...team.users.map((user) => { + return { + ...user, + team: team.name + }; + })]; + } + const onThatTable = users.find((elem) => elem.pseudo === vm.user.pseudo) !== undefined; + if (!fullTable.table.panama && onThatTable) { + vm.isOnTable = true; + } + }); + }).catch((error) => { + console.error('Failed to refresh table presence', error); + }).finally(() => { + vm.loadingTableStatus = false; + }); + }; + + vm.initAuth = function () { + $http.get('/me').then((response) => { + vm.user = response.data; + return vm.refreshTablePresence(); + }).catch((error) => { + console.error('Failed to load user information', error); + vm.loadingTableStatus = false; + }); + }; + // Initialize vm.loadGame(); + vm.initAuth(); }]); diff --git a/scripts/update-admin.js b/scripts/update-admin.js new file mode 100644 index 0000000..a4fc1af --- /dev/null +++ b/scripts/update-admin.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +// Simple helper to flip a user's admin flag via the existing /admin/users/update endpoint. +// Usage: BASE_URL=https://your-host TOKEN= node scripts/update-admin.js --userId 12 --admin true + +const args = require('node:util').parseArgs({ + options: { + userId: { type: 'string' }, + admin: { type: 'string' }, + } +}); + +const baseUrl = process.env.BASE_URL || 'http://localhost:8787'; +const token = process.env.TOKEN; + +if (!token) { + console.error('Missing TOKEN env var (base64 auth token).'); + process.exit(1); +} + +if (!args.values.userId || args.values.admin === undefined) { + console.error('Usage: BASE_URL=... TOKEN=... node scripts/update-admin.js --userId --admin true|false'); + process.exit(1); +} + +const adminFlag = args.values.admin.toLowerCase(); +if (adminFlag !== 'true' && adminFlag !== 'false') { + console.error('--admin must be true or false'); + process.exit(1); +} + +async function run() { + const url = `${baseUrl}/admin/users/update?userId=${encodeURIComponent(args.values.userId)}`; + const body = { + admin: adminFlag === 'true' + }; + + const resp = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': token.trim() + }, + body: JSON.stringify(body) + }); + + const text = await resp.text(); + if (!resp.ok) { + console.error(`Request failed (${resp.status}): ${text}`); + process.exit(1); + } + console.log(`Success (${resp.status}): ${text}`); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/db/schema.ts b/src/db/schema.ts index 6f6364a..82e20f1 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -81,9 +81,9 @@ export const tablesUsers = sqliteTable( .default(false), team: text("team"), }, - (t: any) => ({ - pk: primaryKey({ columns: [t.tableId, t.userId] }) - }) + (t: any) => [ + primaryKey({ columns: [t.tableId, t.userId] }) + ] ); diff --git a/src/durable.ts b/src/durable.ts index a58f2d0..b9823b3 100644 --- a/src/durable.ts +++ b/src/durable.ts @@ -42,6 +42,18 @@ export class MyDurableObject extends DurableObject { return await this.gameService.getHistory(user, limit); } + async adminCreateUser(request: Request) { + return await this.userService.adminCreateUser(request); + } + + async adminUpdateUser(request: Request, userId: number) { + return await this.userService.adminUpdateUser(request, userId); + } + + async adminDeleteUser(userId: number) { + return await this.userService.adminDeleteUser(userId); + } + async passwordChange(request: Request, pseudo:string, admin: boolean): Promise { return this.userService.passwordChange(request, pseudo,admin); } @@ -51,7 +63,7 @@ export class MyDurableObject extends DurableObject { async authenticate(request: Request): Promise { const user = await this.userService.authenticate(request); if (!user) { - return new Response('you need to login', { + return new Response('you need to login (authentication)', { status: 401, }); } @@ -60,13 +72,14 @@ export class MyDurableObject extends DurableObject { const response = new Response(token, { status: 200, }); - response.headers.set('Authorization',token); + response.headers.set('Authorization', token); return response; } - async validateToken(token: string | undefined, admin: Boolean = false): Promise { - return this.userService.validateToken(token,admin); + async validateToken(token: string | undefined, admin: boolean = false): Promise { + await this.userService.genMissingTokens(); + return this.userService.validateToken(token, admin); } - async changeUserState(request: Request, pseudo:string) { + async changeUserState(request: Request, pseudo: string) { await this.userService.changeUserState(request, pseudo); } async getGameModes(): Promise { @@ -86,8 +99,6 @@ export class MyDurableObject extends DurableObject { return await this.gameService.deleteTable(tableId); } - - // for admin exclusively async addTimer(request: Request) { const body: {minutes: number} = await request.json(); @@ -163,6 +174,11 @@ export class MyDurableObject extends DurableObject { async getUserList(){ return await this.userService.getUserList(); } + + async getFullUserList(){ + return await this.userService.adminGetFullUserList(); + } + async adminGenerateTables() { await this.gameService.generateTables(); } diff --git a/src/index.ts b/src/index.ts index 3f35c91..6190d82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -125,6 +125,34 @@ export default { await stub.removeTimer(); await stub.notifyAll(`Removed alarm`); } + case '/admin/users/full': { + return new Response(JSON.stringify(await stub.getFullUserList()), { status: 200 }); + } + case '/admin/users/create': { + return await stub.adminCreateUser(request); + } + case '/admin/users/update': { + const userIdParam = url.searchParams.get('userId'); + if (!userIdParam) { + return new Response(JSON.stringify({ message: 'missing userId' }), { status: 400 }); + } + const userId = parseInt(userIdParam); + if (Number.isNaN(userId)) { + return new Response(JSON.stringify({ message: 'invalid userId' }), { status: 400 }); + } + return await stub.adminUpdateUser(request, userId); + } + case '/admin/users/delete': { + const userIdParam = url.searchParams.get('userId'); + if (!userIdParam) { + return new Response(JSON.stringify({ message: 'missing userId' }), { status: 400 }); + } + const userId = parseInt(userIdParam); + if (Number.isNaN(userId)) { + return new Response(JSON.stringify({ message: 'invalid userId' }), { status: 400 }); + } + return await stub.adminDeleteUser(userId); + } case '/admin/users': { return new Response(JSON.stringify(await stub.getUserList()), { status: 200 }); } diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 72f9fb1..581a2e0 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -1,5 +1,5 @@ import { User } from '../db/schema.types'; -import { eq, or } from "drizzle-orm"; +import { and, eq, isNull, or } from "drizzle-orm"; import { DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite'; import { lower, users } from "../db/schema"; // your schema file import { v4 as uuidv4 } from 'uuid'; @@ -109,13 +109,11 @@ export class UserService { return userResult; } - async validateToken(requestToken: string | undefined, admin: Boolean = false): Promise { - const mustLoginResponse = new Response('you need to login', { - status: 401, - }); - + async validateToken(requestToken: string | undefined, admin: boolean = false): Promise { if (!requestToken) { - return mustLoginResponse; + return new Response('you need to login (no token provided)', { + status: 401, + }); } const token = Buffer.from(requestToken, 'base64').toString(); @@ -124,19 +122,31 @@ export class UserService { .from(users) .where(eq(users.token, token)).get(); - if (!userResult || !userResult.tokenValidity) { - return mustLoginResponse; + if (!userResult) { + return new Response(`user not found with token ${token}`, { + status: 401, + }); + } + + if (!userResult.tokenValidity) { + return new Response('invalid token', { + status: 401, + }); } + if (admin && !userResult.admin) { return new Response('you are not admin', { status: 403, - });; + }); } + const now = new Date(); const validity = new Date(); validity.setTime(userResult.tokenValidity); if (isNaN(validity.getTime()) || validity.getTime() < now.getTime()) { - return mustLoginResponse; + return new Response('you need to login (bad validity)', { + status: 401, + }); } const tokenValidity = new Date(); @@ -180,6 +190,28 @@ export class UserService { return user.id; } } + + async genMissingTokens() { + const missingTokenAdminUsers = await this.db + .select() + .from(users) + .where(and(eq(users.admin, true), isNull(users.token))); + + for await (const user of missingTokenAdminUsers) { + let newToken = uuidv4(); + const tokenValidity = new Date(); + tokenValidity.setDate(tokenValidity.getDate() + 1); + + await this.db.update(users) + .set({ token: newToken, tokenValidity: tokenValidity.getTime() }) + .where(eq(users.id, user.id)); + } + + for await (const user of await this.adminGetFullUserList()) { + console.log(JSON.stringify(user)); + } + } + async getUserList() { return await this.db .select({ @@ -188,4 +220,112 @@ export class UserService { }) .from(users).all(); } + + async adminGetFullUserList() { + return await this.db + .select() + .from(users) + .all(); + } + + async adminCreateUser(request: Request) { + const body: { pseudo?: string; email?: string; password?: string; admin?: boolean; ready?: boolean; canPlayTarot?: boolean; canPlayTwoTables?: boolean } = await request.json(); + if (!body.pseudo || !body.email || !body.password) { + return new Response('missing required fields', { status: 400 }); + } + + const existingPseudo = await this.db + .select() + .from(users) + .where(eq(lower(users.pseudo), body.pseudo.toLowerCase())) + .get(); + if (existingPseudo) { + return new Response('existing user', { status: 400 }); + } + + const existingEmail = await this.db + .select() + .from(users) + .where(eq(lower(users.email), body.email.toLowerCase())) + .get(); + if (existingEmail) { + return new Response('existing email', { status: 400 }); + } + + await this.db + .insert(users) + .values({ + pseudo: body.pseudo, + email: body.email, + password: await hash(body.password, saltRounds), + ready: body.ready ?? false, + admin: body.admin ?? false, + canPlayTarot: body.canPlayTarot ?? false, + canPlayTwoTables: body.canPlayTwoTables ?? false, + token: null, + tokenValidity: null, + lastActiveAt: null + }) + .returning(); + + return new Response(JSON.stringify({ message: 'user created' }), { status: 200 }); + } + + async adminUpdateUser(request: Request, userId: number) { + const body: { pseudo?: string; email?: string; admin?: boolean; ready?: boolean; canPlayTarot?: boolean; canPlayTwoTables?: boolean; newPassword?: string } = await request.json(); + const user = await this.db.select().from(users).where(eq(users.id, userId)).get(); + if (!user) { + return new Response('user not found', { status: 404 }); + } + + if (body.pseudo && body.pseudo.toLowerCase() !== user.pseudo.toLowerCase()) { + const existingPseudo = await this.db + .select() + .from(users) + .where(eq(lower(users.pseudo), body.pseudo.toLowerCase())) + .get(); + if (existingPseudo) { + return new Response('existing user', { status: 400 }); + } + } + + if (body.email && body.email.toLowerCase() !== user.email.toLowerCase()) { + const existingEmail = await this.db + .select() + .from(users) + .where(eq(lower(users.email), body.email.toLowerCase())) + .get(); + if (existingEmail) { + return new Response('existing email', { status: 400 }); + } + } + + const updates: any = {}; + if (body.pseudo !== undefined) updates.pseudo = body.pseudo; + if (body.email !== undefined) updates.email = body.email; + if (body.admin !== undefined) updates.admin = body.admin; + if (body.ready !== undefined) updates.ready = body.ready; + if (body.canPlayTarot !== undefined) updates.canPlayTarot = body.canPlayTarot; + if (body.canPlayTwoTables !== undefined) updates.canPlayTwoTables = body.canPlayTwoTables; + if (body.newPassword) { + updates.password = await hash(body.newPassword, saltRounds); + } + + if (Object.keys(updates).length === 0) { + return new Response(JSON.stringify({ message: 'nothing to update' }), { status: 200 }); + } + + await this.db.update(users).set(updates).where(eq(users.id, user.id)); + + return new Response(JSON.stringify({ message: 'user updated' }), { status: 200 }); + } + + async adminDeleteUser(userId: number) { + const user = await this.db.select().from(users).where(eq(users.id, userId)).get(); + if (!user) { + return new Response('user not found', { status: 404 }); + } + await this.db.delete(users).where(eq(users.id, userId)); + return new Response(JSON.stringify({ message: 'user deleted' }), { status: 200 }); + } } diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index f975203..ec31a44 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,6 +1,6 @@ /* eslint-disable */ // Generated by Wrangler by running `wrangler types` (hash: 7f476d0d71ab342e563f2308a3bdefde) -// Runtime types generated with workerd@1.20260120.0 2025-10-29 global_fetch_strictly_public,nodejs_compat +// Runtime types generated with workerd@1.20260212.0 2025-10-29 global_fetch_strictly_public,nodejs_compat declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/index"); @@ -1400,6 +1400,12 @@ declare abstract class PromiseRejectionEvent extends Event { */ declare class FormData { constructor(); + /** + * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) + */ + append(name: string, value: string | Blob): void; /** * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist. * @@ -1436,6 +1442,12 @@ declare class FormData { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has) */ has(name: string): boolean; + /** + * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) + */ + set(name: string, value: string | Blob): void; /** * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist. * @@ -1762,7 +1774,7 @@ interface Request> e * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/signal) */ signal: AbortSignal; - cf: Cf | undefined; + cf?: Cf; /** * The **`integrity`** read-only property of the Request interface contains the subresource integrity value of the request. * @@ -9444,6 +9456,10 @@ interface D1Meta { * The region of the database instance that executed the query. */ served_by_region?: string; + /** + * The three letters airport code of the colo that executed the query. + */ + served_by_colo?: string; /** * True if-and-only-if the database instance that executed the query was the primary. */ @@ -9590,11 +9606,42 @@ interface ForwardableEmailMessage extends EmailMessage { */ reply(message: EmailMessage): Promise; } +/** A file attachment for an email message */ +type EmailAttachment = { + disposition: 'inline'; + contentId: string; + filename: string; + type: string; + content: string | ArrayBuffer | ArrayBufferView; +} | { + disposition: 'attachment'; + contentId?: undefined; + filename: string; + type: string; + content: string | ArrayBuffer | ArrayBufferView; +}; +/** An Email Address */ +interface EmailAddress { + name: string; + email: string; +} /** * A binding that allows a Worker to send email messages. */ interface SendEmail { send(message: EmailMessage): Promise; + send(builder: { + from: string | EmailAddress; + to: string | string[]; + subject: string; + replyTo?: string | EmailAddress; + cc?: string | string[]; + bcc?: string | string[]; + headers?: Record; + text?: string; + html?: string; + attachments?: EmailAttachment[]; + }): Promise; } declare abstract class EmailEvent extends ExtendableEvent { readonly message: ForwardableEmailMessage; @@ -10241,6 +10288,7 @@ declare namespace CloudflareWorkersModule { timeout?: WorkflowTimeoutDuration | number; }): Promise>; } + export type WorkflowInstanceStatus = 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting' | 'waitingForPause' | 'unknown'; export abstract class WorkflowEntrypoint | unknown = unknown> implements Rpc.WorkflowEntrypointBranded { [Rpc.__WORKFLOW_ENTRYPOINT_BRAND]: never; protected ctx: ExecutionContext; @@ -10274,12 +10322,14 @@ type MarkdownDocument = { blob: Blob; }; type ConversionResponse = { + id: string; name: string; mimeType: string; format: 'markdown'; tokens: number; data: string; } | { + id: string; name: string; mimeType: string; format: 'error'; @@ -10297,6 +10347,7 @@ type ConversionOptions = { images?: EmbeddedImageConversionOptions & { convertOGImage?: boolean; }; + hostname?: string; }; docx?: { images?: EmbeddedImageConversionOptions; @@ -10434,6 +10485,15 @@ declare namespace TailStream { readonly level: "debug" | "error" | "info" | "log" | "warn"; readonly message: object; } + interface DroppedEventsDiagnostic { + readonly diagnosticsType: "droppedEvents"; + readonly count: number; + } + interface StreamDiagnostic { + readonly type: 'streamDiagnostic'; + // To add new diagnostic types, define a new interface and add it to this union type. + readonly diagnostic: DroppedEventsDiagnostic; + } // This marks the worker handler return information. // This is separate from Outcome because the worker invocation can live for a long time after // returning. For example - Websockets that return an http upgrade response but then continue @@ -10450,7 +10510,7 @@ declare namespace TailStream { readonly type: "attributes"; readonly info: Attribute[]; } - type EventType = Onset | Outcome | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | Return | Attributes; + type EventType = Onset | Outcome | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | StreamDiagnostic | Return | Attributes; // Context in which this trace event lives. interface SpanContext { // Single id for the entire top-level invocation From 0549afe954cf6c85a5b14ea3f7084262130c2d01 Mon Sep 17 00:00:00 2001 From: Adrien Pensart Date: Mon, 16 Feb 2026 15:19:29 +0100 Subject: [PATCH 2/2] removed user list --- public/backend.html | 1 - public/userList.html | 25 ------------------------- public/userList.js | 38 -------------------------------------- 3 files changed, 64 deletions(-) delete mode 100644 public/userList.html delete mode 100644 public/userList.js diff --git a/public/backend.html b/public/backend.html index 9f24dae..792dcf6 100644 --- a/public/backend.html +++ b/public/backend.html @@ -137,7 +137,6 @@

Utilisateurs existants

Frontend - Liste utilisateurs