diff --git a/.gitignore b/.gitignore index 3659134..f641195 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ bin db build.sh .chess_history -*.wasm \ No newline at end of file +*.wasm +catalog.txt diff --git a/go.mod b/go.mod index 4a6f019..1cd6779 100644 --- a/go.mod +++ b/go.mod @@ -1,34 +1,33 @@ module chess -go 1.25.4 +go 1.26.0 require ( github.com/go-playground/validator/v10 v10.30.1 - github.com/gofiber/fiber/v2 v2.52.11 + github.com/gofiber/fiber/v2 v2.52.12 github.com/google/uuid v1.6.0 github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226 - github.com/mattn/go-sqlite3 v1.14.33 - golang.org/x/term v0.39.0 + github.com/mattn/go-sqlite3 v1.14.34 + golang.org/x/term v0.40.0 ) require ( github.com/andybalholm/brotli v1.2.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect - github.com/klauspost/compress v1.18.3 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/tinylib/msgp v1.6.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect ) diff --git a/go.sum b/go.sum index 0600983..c1ce92f 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,9 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= -github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= -github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -18,24 +12,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= -github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= -github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= -github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= +github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= -github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226 h1:c7wfyZGdy6RkM/b6mIazoYrAS+3qDL7d9M1CFm2e1VA= @@ -44,46 +30,32 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= -github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tinylib/msgp v1.5.0 h1:GWnqAE54wmnlFazjq2+vgr736Akg58iiHImh+kPY2pc= -github.com/tinylib/msgp v1.5.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok= -github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/server/webserver/chess-client-web/app.js b/internal/server/webserver/chess-client-web/app.js index 46264ff..0fdb10b 100644 --- a/internal/server/webserver/chess-client-web/app.js +++ b/internal/server/webserver/chess-client-web/app.js @@ -5,12 +5,16 @@ let gameState = { turn: 'w', isPlayerWhite: true, isLocked: false, - pollInterval: null, + polling: false, + pollController: null, apiUrl: '', selectedSquare: null, healthCheckInterval: null, networkError: false, moveList: [], + authToken: null, + userId: null, + username: null, }; // Chess piece Unicode: all black pieces for better fill, white pawn due to inability to override emoji variant display @@ -42,9 +46,218 @@ document.addEventListener('DOMContentLoaded', async () => { // Don't auto-show modal on load }); +document.getElementById('auth-indicator').addEventListener('click', handleAuthClick); +document.querySelectorAll('.auth-tab').forEach(tab => { + tab.addEventListener('click', (e) => switchAuthTab(e.target.dataset.tab)); +}); +document.getElementById('login-submit-btn').addEventListener('click', handleLogin); +document.getElementById('register-submit-btn').addEventListener('click', handleRegister); +document.getElementById('auth-cancel-btn').addEventListener('click', hideAuthModal); +document.getElementById('auth-cancel-btn-2').addEventListener('click', hideAuthModal); + +// Check for existing session on load +restoreAuthSession(); + +// Auth functions +function restoreAuthSession() { + const token = localStorage.getItem('authToken'); + if (token) { + gameState.authToken = token; + validateSession(); + } +} + +async function validateSession() { + try { + const response = await authFetch(`${gameState.apiUrl}/api/v1/auth/me`); + if (response.ok) { + const user = await response.json(); + gameState.userId = user.userId; + gameState.username = user.username; + updateAuthIndicator(true); + } else { + clearAuthState(); + } + } catch { + clearAuthState(); + } +} + +function clearAuthState() { + gameState.authToken = null; + gameState.userId = null; + gameState.username = null; + localStorage.removeItem('authToken'); + updateAuthIndicator(false); +} + +function updateAuthIndicator(authenticated) { + const indicator = document.getElementById('auth-indicator'); + const light = indicator.querySelector('.light'); + if (authenticated) { + light.setAttribute('data-status', 'authenticated'); + indicator.setAttribute('data-status', gameState.username); + } else { + light.setAttribute('data-status', 'anonymous'); + indicator.setAttribute('data-status', 'anonymous'); + } +} + +function handleAuthClick() { + if (gameState.authToken) { + // Logged in - confirm logout + if (confirm(`Logout ${gameState.username}?`)) { + handleLogout(); + } + } else { + showAuthModal(); + } +} + +function showAuthModal() { + document.getElementById('auth-modal-overlay').classList.add('show'); + document.getElementById('login-identifier').focus(); + document.addEventListener('keydown', handleAuthModalKeydown); +} + +function hideAuthModal() { + document.getElementById('auth-modal-overlay').classList.remove('show'); + document.querySelectorAll('.auth-form input').forEach(input => input.value = ''); + document.removeEventListener('keydown', handleAuthModalKeydown); +} + +function handleAuthModalKeydown(e) { + const modal = document.getElementById('auth-modal-overlay'); + if (!modal.classList.contains('show')) return; + + if (e.key === 'Escape') { + e.preventDefault(); + hideAuthModal(); + } else if (e.key === 'Enter') { + e.preventDefault(); + const loginForm = document.getElementById('login-form'); + if (loginForm.style.display !== 'none') { + handleLogin(); + } else { + handleRegister(); + } + } +} + +function switchAuthTab(tab) { + document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active')); + document.querySelector(`[data-tab="${tab}"]`).classList.add('active'); + + document.getElementById('login-form').style.display = tab === 'login' ? 'block' : 'none'; + document.getElementById('register-form').style.display = tab === 'register' ? 'block' : 'none'; +} + +async function handleLogin() { + const identifier = document.getElementById('login-identifier').value.trim(); + const password = document.getElementById('login-password').value; + + if (!identifier || !password) { + flashErrorMessage('Fill all fields'); + return; + } + + try { + const response = await fetch(`${gameState.apiUrl}/api/v1/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ identifier, password }) + }); + + if (!response.ok) { + const err = await response.json(); + flashErrorMessage(err.error || 'Login failed'); + return; + } + + const auth = await response.json(); + gameState.authToken = auth.token; + gameState.userId = auth.userId; + gameState.username = auth.username; + localStorage.setItem('authToken', auth.token); + updateAuthIndicator(true); + hideAuthModal(); + } catch (error) { + flashErrorMessage('Connection failed'); + } +} + +async function handleRegister() { + const username = document.getElementById('register-username').value.trim(); + const email = document.getElementById('register-email').value.trim(); + const password = document.getElementById('register-password').value; + + if (!username || !password) { + flashErrorMessage('Username and password required'); + return; + } + + if (password.length < 8) { + flashErrorMessage('Password min 8 chars'); + return; + } + + try { + const body = { username, password }; + if (email) body.email = email; + + const response = await fetch(`${gameState.apiUrl}/api/v1/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + const err = await response.json(); + flashErrorMessage(err.details || err.error || 'Registration failed'); + return; + } + + const auth = await response.json(); + gameState.authToken = auth.token; + gameState.userId = auth.userId; + gameState.username = auth.username; + localStorage.setItem('authToken', auth.token); + updateAuthIndicator(true); + hideAuthModal(); + } catch (error) { + flashErrorMessage('Connection failed'); + } +} + +async function handleLogout() { + if (gameState.authToken) { + try { + await authFetch(`${gameState.apiUrl}/api/v1/auth/logout`, { method: 'POST' }); + } catch { + // Ignore - clear local state regardless + } + } + clearAuthState(); +} + +// Wrapper for authenticated requests +function authFetch(url, options = {}) { + if (gameState.authToken) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${gameState.authToken}` + }; + } + return fetch(url, options); +} + async function getConfig() { try { const response = await fetch('/config'); + const contentType = response.headers.get('content-type') || ''; + if (!response.ok || !contentType.includes('application/json')) { + throw new Error(`unexpected response: ${response.status} ${contentType}`); + } return await response.json(); } catch (error) { console.error('Failed to get config:', error); @@ -266,13 +479,11 @@ async function startNewGame() { } try { - const response = await fetch(`${gameState.apiUrl}/api/v1/games`, { + const response = await authFetch(`${gameState.apiUrl}/api/v1/games`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); - if (!response.ok) throw new Error('Failed to create game'); - if (!response.ok) { const errorInfo = handleApiError('create game', null, response); throw new Error(errorInfo.statusMessage); @@ -404,7 +615,7 @@ async function handleHumanMove(from, to) { const toEl = document.querySelector(`[data-square="${to}"]`); try { - const response = await fetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/moves`, { + const response = await authFetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/moves`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ move }) @@ -441,7 +652,7 @@ async function handleHumanMove(from, to) { async function triggerComputerMove() { lockBoard(); try { - const response = await fetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/moves`, { + const response = await authFetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/moves`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ move: 'cccc' }) @@ -462,43 +673,64 @@ async function triggerComputerMove() { } function startPolling() { - gameState.pollInterval = setInterval(async () => { - try { - const response = await fetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}`); - if (!response.ok) { - // Use error handler but continue polling for 404 (game might be deleted) - const errorInfo = handleApiError('poll game state', null, response); - if (response.status === 404) { - stopPolling(); - unlockBoard(); - flashErrorMessage('Game no longer exists'); - gameState.gameId = null; - return; - } - // For other errors, display but keep polling - handleApiError('poll game state', null, response); - return; - } - - const game = await response.json(); - if (game.state !== 'pending') { - stopPolling(); - updateGameDisplay(game); - unlockBoard(); - } - gameState.networkError = false; - updateServerIndicator('healthy'); - } catch (error) { - handleApiError('poll game state', error); - stopPolling(); - unlockBoard(); - } - }, 1500); + gameState.polling = true; + pollOnce(); } function stopPolling() { - clearInterval(gameState.pollInterval); - gameState.pollInterval = null; + gameState.polling = false; + if (gameState.pollController) { + gameState.pollController.abort(); + gameState.pollController = null; + } +} + +async function pollOnce() { + if (!gameState.polling || !gameState.gameId) return; + + const moveCount = (gameState.moveList || []).length; + gameState.pollController = new AbortController(); + + try { + const response = await fetch( + `${gameState.apiUrl}/api/v1/games/${gameState.gameId}?wait=true&moveCount=${moveCount}`, + { signal: gameState.pollController.signal } + ); + + if (!gameState.polling) return; + + if (!response.ok) { + const errorInfo = handleApiError('poll game state', null, response); + if (response.status === 404) { + stopPolling(); + unlockBoard(); + flashErrorMessage('Game no longer exists'); + gameState.gameId = null; + return; + } + // Retry after delay for transient errors + setTimeout(pollOnce, 2000); + return; + } + + const game = await response.json(); + gameState.networkError = false; + updateServerIndicator('healthy'); + + if (game.state !== 'pending') { + stopPolling(); + updateGameDisplay(game); + unlockBoard(); + } else { + // Still pending, long-poll again + pollOnce(); + } + } catch (error) { + if (error.name === 'AbortError') return; + handleApiError('poll game state', error); + stopPolling(); + unlockBoard(); + } } function lockBoard() { @@ -753,5 +985,5 @@ function flashErrorMessage(message) { // Auto-hide after animation completes setTimeout(() => { overlay.classList.remove('show'); - }, 500); + }, 1500); } \ No newline at end of file diff --git a/internal/server/webserver/chess-client-web/index.html b/internal/server/webserver/chess-client-web/index.html index 4d78ed9..e421675 100644 --- a/internal/server/webserver/chess-client-web/index.html +++ b/internal/server/webserver/chess-client-web/index.html @@ -37,6 +37,9 @@