Compare commits
3 Commits
99b37b5456
...
main
| Author | SHA256 | Date | |
|---|---|---|---|
|
f82091a9bd
|
|||
|
16fc069a8f
|
|||
|
fc8a6ba6a6
|
@ -30,8 +30,8 @@ func runClient() (restart bool) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
s := &session.Session{
|
s := &session.Session{
|
||||||
APIBaseURL: "http://localhost:8080",
|
APIBaseURL: defaultAPIBase,
|
||||||
Client: api.New("http://localhost:8080"),
|
Client: api.New(defaultAPIBase),
|
||||||
Verbose: false,
|
Verbose: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,4 +132,5 @@ func buildPrompt(s *session.Session) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return display.Prompt(b.String())
|
return display.Prompt(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
cmd/chess-client-cli/url_native.go
Normal file
5
cmd/chess-client-cli/url_native.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//go:build !js
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
const defaultAPIBase = "http://localhost:8080"
|
||||||
11
cmd/chess-client-cli/url_wasm.go
Normal file
11
cmd/chess-client-cli/url_wasm.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "syscall/js"
|
||||||
|
|
||||||
|
// Derive base URL from the page's own origin at runtime
|
||||||
|
// When served via nginx at domain.com, origin = "https://domain.com"
|
||||||
|
// and the chess API proxy lives at /chess — so BaseURL = "https://comain.com/chess".
|
||||||
|
// Works correctly for any deployment domain without rebuilding
|
||||||
|
var defaultAPIBase = js.Global().Get("location").Get("origin").String() + "/chess"
|
||||||
@ -75,6 +75,9 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to initialize storage: %v", err)
|
log.Fatalf("Failed to initialize storage: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := store.InitDB(); err != nil {
|
||||||
|
log.Fatalf("Failed to initialize schema: %v", err)
|
||||||
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := store.Close(); err != nil {
|
if err := store.Close(); err != nil {
|
||||||
log.Printf("Warning: failed to close storage cleanly: %v", err)
|
log.Printf("Warning: failed to close storage cleanly: %v", err)
|
||||||
@ -189,4 +192,5 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Servers exited")
|
log.Println("Servers exited")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
|||||||
module chess
|
module chess
|
||||||
|
|
||||||
go 1.25.4
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-playground/validator/v10 v10.30.1
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@ -93,6 +94,13 @@ func (h *HTTPHandler) RegisterHandler(c *fiber.Ctx) error {
|
|||||||
// Create user (temp by default via API)
|
// Create user (temp by default via API)
|
||||||
user, err := h.svc.CreateUser(req.Username, req.Email, req.Password, false)
|
user, err := h.svc.CreateUser(req.Username, req.Email, req.Password, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, service.ErrAtCapacity) || errors.Is(err, service.ErrPermanentSlotsFull) {
|
||||||
|
return c.Status(fiber.StatusServiceUnavailable).JSON(core.ErrorResponse{
|
||||||
|
Error: "registration temporarily unavailable",
|
||||||
|
Code: core.ErrResourceLimit,
|
||||||
|
Details: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
if strings.Contains(err.Error(), "already exists") {
|
if strings.Contains(err.Error(), "already exists") {
|
||||||
return c.Status(fiber.StatusConflict).JSON(core.ErrorResponse{
|
return c.Status(fiber.StatusConflict).JSON(core.ErrorResponse{
|
||||||
Error: "user already exists",
|
Error: "user already exists",
|
||||||
@ -100,13 +108,6 @@ func (h *HTTPHandler) RegisterHandler(c *fiber.Ctx) error {
|
|||||||
Details: "username or email already taken",
|
Details: "username or email already taken",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if strings.Contains(err.Error(), "limit") || strings.Contains(err.Error(), "capacity") {
|
|
||||||
return c.Status(fiber.StatusServiceUnavailable).JSON(core.ErrorResponse{
|
|
||||||
Error: "registration temporarily unavailable",
|
|
||||||
Code: core.ErrResourceLimit,
|
|
||||||
Details: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(core.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(core.ErrorResponse{
|
||||||
Error: "failed to create user",
|
Error: "failed to create user",
|
||||||
Code: core.ErrInternalError,
|
Code: core.ErrInternalError,
|
||||||
@ -261,4 +262,5 @@ func (h *HTTPHandler) LogoutHandler(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(fiber.Map{"message": "logged out"})
|
return c.JSON(fiber.Map{"message": "logged out"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,9 +36,9 @@ func NewFiberApp(proc *processor.Processor, svc *service.Service, devMode bool)
|
|||||||
// Initialize Fiber app
|
// Initialize Fiber app
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
ErrorHandler: customErrorHandler,
|
ErrorHandler: customErrorHandler,
|
||||||
ReadTimeout: 10 * time.Second,
|
ReadTimeout: 15 * time.Second,
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 35 * time.Second,
|
||||||
IdleTimeout: 30 * time.Second,
|
IdleTimeout: 60 * time.Second,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Global middleware (order matters)
|
// Global middleware (order matters)
|
||||||
@ -517,4 +517,5 @@ func (h *HTTPHandler) GetBoard(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(resp.Data)
|
return c.JSON(resp.Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,7 @@ func (q *EngineQueue) worker(id int) {
|
|||||||
// Send result if receiver still listening
|
// Send result if receiver still listening
|
||||||
select {
|
select {
|
||||||
case task.Response <- result:
|
case task.Response <- result:
|
||||||
case <-time.After(100 * time.Millisecond):
|
case <-time.After(15 * time.Millisecond):
|
||||||
// Receiver abandoned, discard result
|
// Receiver abandoned, discard result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,4 +205,5 @@ func (q *EngineQueue) Shutdown(timeout time.Duration) error {
|
|||||||
case <-time.After(timeout):
|
case <-time.After(timeout):
|
||||||
return fmt.Errorf("shutdown timeout exceeded")
|
return fmt.Errorf("shutdown timeout exceeded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -11,6 +12,12 @@ import (
|
|||||||
"github.com/lixenwraith/auth"
|
"github.com/lixenwraith/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrStorageDisabled = errors.New("storage disabled")
|
||||||
|
ErrAtCapacity = errors.New("at capacity")
|
||||||
|
ErrPermanentSlotsFull = errors.New("permanent slots full")
|
||||||
|
)
|
||||||
|
|
||||||
// User represents a registered user account
|
// User represents a registered user account
|
||||||
type User struct {
|
type User struct {
|
||||||
UserID string
|
UserID string
|
||||||
@ -24,13 +31,13 @@ type User struct {
|
|||||||
// CreateUser creates new user with registration limits enforcement
|
// CreateUser creates new user with registration limits enforcement
|
||||||
func (s *Service) CreateUser(username, email, password string, permanent bool) (*User, error) {
|
func (s *Service) CreateUser(username, email, password string, permanent bool) (*User, error) {
|
||||||
if s.store == nil {
|
if s.store == nil {
|
||||||
return nil, fmt.Errorf("storage disabled")
|
return nil, ErrStorageDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check registration limits
|
// Check registration limits
|
||||||
total, permCount, _, err := s.store.GetUserCounts()
|
total, permCount, _, err := s.store.GetUserCounts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to check user limits: %w", err)
|
return nil, fmt.Errorf("failed to get user count: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine account type
|
// Determine account type
|
||||||
@ -39,7 +46,7 @@ func (s *Service) CreateUser(username, email, password string, permanent bool) (
|
|||||||
|
|
||||||
if permanent {
|
if permanent {
|
||||||
if permCount >= PermanentSlots {
|
if permCount >= PermanentSlots {
|
||||||
return nil, fmt.Errorf("permanent user slots full (%d/%d)", permCount, PermanentSlots)
|
return nil, fmt.Errorf("%w (%d/%d)", ErrPermanentSlotsFull, permCount, PermanentSlots)
|
||||||
}
|
}
|
||||||
accountType = "permanent"
|
accountType = "permanent"
|
||||||
} else {
|
} else {
|
||||||
@ -50,7 +57,7 @@ func (s *Service) CreateUser(username, email, password string, permanent bool) (
|
|||||||
// Handle capacity - remove oldest temp user if at max
|
// Handle capacity - remove oldest temp user if at max
|
||||||
if total >= MaxUsers {
|
if total >= MaxUsers {
|
||||||
if err := s.removeOldestTempUser(); err != nil {
|
if err := s.removeOldestTempUser(); err != nil {
|
||||||
return nil, fmt.Errorf("at capacity and cannot make room: %w", err)
|
return nil, fmt.Errorf("%w: %v", ErrAtCapacity, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,4 +282,5 @@ func (s *Service) CreateUserSession(userID string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sessionID, nil
|
return sessionID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// WaitTimeout is the maximum time a client can wait for notifications
|
// WaitTimeout is the maximum time a client can wait for notifications
|
||||||
WaitTimeout = 25 * time.Second
|
WaitTimeout = 30 * time.Second
|
||||||
|
|
||||||
// WaitChannelBuffer size for notification channels
|
// WaitChannelBuffer size for notification channels
|
||||||
WaitChannelBuffer = 1
|
WaitChannelBuffer = 1
|
||||||
@ -174,4 +174,5 @@ func (w *WaitRegistry) removeWaiter(gameID string, req *WaitRequest) {
|
|||||||
|
|
||||||
// Stop timer if still running
|
// Stop timer if still running
|
||||||
req.Timer.Stop()
|
req.Timer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,9 +26,9 @@ func DefaultUserLimits() UserLimits {
|
|||||||
// GetUserCounts returns current user counts by type
|
// GetUserCounts returns current user counts by type
|
||||||
func (s *Store) GetUserCounts() (total, permanent, temp int, err error) {
|
func (s *Store) GetUserCounts() (total, permanent, temp int, err error) {
|
||||||
query := `SELECT
|
query := `SELECT
|
||||||
COUNT(*) as total,
|
COUNT(*) as total,
|
||||||
SUM(CASE WHEN account_type = 'permanent' THEN 1 ELSE 0 END) as permanent,
|
COALESCE(SUM(CASE WHEN account_type = 'permanent' THEN 1 ELSE 0 END), 0) as permanent,
|
||||||
SUM(CASE WHEN account_type = 'temp' THEN 1 ELSE 0 END) as temp
|
COALESCE(SUM(CASE WHEN account_type = 'temp' THEN 1 ELSE 0 END), 0) as temp
|
||||||
FROM users`
|
FROM users`
|
||||||
|
|
||||||
err = s.db.QueryRow(query).Scan(&total, &permanent, &temp)
|
err = s.db.QueryRow(query).Scan(&total, &permanent, &temp)
|
||||||
@ -38,6 +38,7 @@ func (s *Store) GetUserCounts() (total, permanent, temp int, err error) {
|
|||||||
// GetOldestTempUser returns the oldest temporary user for replacement
|
// GetOldestTempUser returns the oldest temporary user for replacement
|
||||||
func (s *Store) GetOldestTempUser() (*UserRecord, error) {
|
func (s *Store) GetOldestTempUser() (*UserRecord, error) {
|
||||||
var user UserRecord
|
var user UserRecord
|
||||||
|
var email sql.NullString
|
||||||
query := `SELECT user_id, username, email, password_hash, account_type, created_at, expires_at, last_login_at
|
query := `SELECT user_id, username, email, password_hash, account_type, created_at, expires_at, last_login_at
|
||||||
FROM users
|
FROM users
|
||||||
WHERE account_type = 'temp'
|
WHERE account_type = 'temp'
|
||||||
@ -45,13 +46,14 @@ func (s *Store) GetOldestTempUser() (*UserRecord, error) {
|
|||||||
LIMIT 1`
|
LIMIT 1`
|
||||||
|
|
||||||
err := s.db.QueryRow(query).Scan(
|
err := s.db.QueryRow(query).Scan(
|
||||||
&user.UserID, &user.Username, &user.Email,
|
&user.UserID, &user.Username, &email,
|
||||||
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
||||||
&user.ExpiresAt, &user.LastLoginAt,
|
&user.ExpiresAt, &user.LastLoginAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
user.Email = email.String
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,14 +167,16 @@ func (s *Store) GetAllUsers() ([]UserRecord, error) {
|
|||||||
var users []UserRecord
|
var users []UserRecord
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var user UserRecord
|
var user UserRecord
|
||||||
|
var email sql.NullString
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&user.UserID, &user.Username, &user.Email,
|
&user.UserID, &user.Username, &email,
|
||||||
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
||||||
&user.ExpiresAt, &user.LastLoginAt,
|
&user.ExpiresAt, &user.LastLoginAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
user.Email = email.String
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,51 +196,57 @@ func (s *Store) UpdateUserLastLoginSync(userID string, loginTime time.Time) erro
|
|||||||
// GetUserByUsername retrieves user by username with case-insensitive matching
|
// GetUserByUsername retrieves user by username with case-insensitive matching
|
||||||
func (s *Store) GetUserByUsername(username string) (*UserRecord, error) {
|
func (s *Store) GetUserByUsername(username string) (*UserRecord, error) {
|
||||||
var user UserRecord
|
var user UserRecord
|
||||||
|
var email sql.NullString
|
||||||
query := `SELECT user_id, username, email, password_hash, account_type, created_at, expires_at, last_login_at
|
query := `SELECT user_id, username, email, password_hash, account_type, created_at, expires_at, last_login_at
|
||||||
FROM users WHERE username = ? COLLATE NOCASE`
|
FROM users WHERE username = ? COLLATE NOCASE`
|
||||||
|
|
||||||
err := s.db.QueryRow(query, username).Scan(
|
err := s.db.QueryRow(query, username).Scan(
|
||||||
&user.UserID, &user.Username, &user.Email,
|
&user.UserID, &user.Username, &email,
|
||||||
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
||||||
&user.ExpiresAt, &user.LastLoginAt,
|
&user.ExpiresAt, &user.LastLoginAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
user.Email = email.String
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail retrieves user by email with case-insensitive matching
|
// GetUserByEmail retrieves user by email with case-insensitive matching
|
||||||
func (s *Store) GetUserByEmail(email string) (*UserRecord, error) {
|
func (s *Store) GetUserByEmail(email string) (*UserRecord, error) {
|
||||||
var user UserRecord
|
var user UserRecord
|
||||||
|
var emailNull sql.NullString
|
||||||
query := `SELECT user_id, username, email, password_hash, account_type, created_at, expires_at, last_login_at
|
query := `SELECT user_id, username, email, password_hash, account_type, created_at, expires_at, last_login_at
|
||||||
FROM users WHERE email = ? COLLATE NOCASE`
|
FROM users WHERE email = ? COLLATE NOCASE`
|
||||||
|
|
||||||
err := s.db.QueryRow(query, email).Scan(
|
err := s.db.QueryRow(query, email).Scan(
|
||||||
&user.UserID, &user.Username, &user.Email,
|
&user.UserID, &user.Username, &emailNull,
|
||||||
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
||||||
&user.ExpiresAt, &user.LastLoginAt,
|
&user.ExpiresAt, &user.LastLoginAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
user.Email = emailNull.String
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByID retrieves user by unique user ID
|
// GetUserByID retrieves user by unique user ID
|
||||||
func (s *Store) GetUserByID(userID string) (*UserRecord, error) {
|
func (s *Store) GetUserByID(userID string) (*UserRecord, error) {
|
||||||
var user UserRecord
|
var user UserRecord
|
||||||
|
var email sql.NullString
|
||||||
query := `SELECT user_id, username, email, password_hash, account_type, created_at, expires_at, last_login_at
|
query := `SELECT user_id, username, email, password_hash, account_type, created_at, expires_at, last_login_at
|
||||||
FROM users WHERE user_id = ?`
|
FROM users WHERE user_id = ?`
|
||||||
|
|
||||||
err := s.db.QueryRow(query, userID).Scan(
|
err := s.db.QueryRow(query, userID).Scan(
|
||||||
&user.UserID, &user.Username, &user.Email,
|
&user.UserID, &user.Username, &email,
|
||||||
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
&user.PasswordHash, &user.AccountType, &user.CreatedAt,
|
||||||
&user.ExpiresAt, &user.LastLoginAt,
|
&user.ExpiresAt, &user.LastLoginAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
user.Email = email.String
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,4 +267,4 @@ func (s *Store) DeleteUser(userID string) error {
|
|||||||
log.Printf("Storage write queue full, dropping user deletion")
|
log.Printf("Storage write queue full, dropping user deletion")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
gameState.apiUrl = config.apiUrl;
|
gameState.apiUrl = config.apiUrl;
|
||||||
|
|
||||||
|
// Check for existing session on load
|
||||||
|
restoreAuthSession();
|
||||||
|
|
||||||
document.getElementById('new-game-btn').addEventListener('click', showNewGameModal);
|
document.getElementById('new-game-btn').addEventListener('click', showNewGameModal);
|
||||||
document.getElementById('undo-btn').addEventListener('click', undoMoves);
|
document.getElementById('undo-btn').addEventListener('click', undoMoves);
|
||||||
document.getElementById('start-game-btn').addEventListener('click', startNewGame);
|
document.getElementById('start-game-btn').addEventListener('click', startNewGame);
|
||||||
@ -55,8 +58,6 @@ document.getElementById('register-submit-btn').addEventListener('click', handleR
|
|||||||
document.getElementById('auth-cancel-btn').addEventListener('click', hideAuthModal);
|
document.getElementById('auth-cancel-btn').addEventListener('click', hideAuthModal);
|
||||||
document.getElementById('auth-cancel-btn-2').addEventListener('click', hideAuthModal);
|
document.getElementById('auth-cancel-btn-2').addEventListener('click', hideAuthModal);
|
||||||
|
|
||||||
// Check for existing session on load
|
|
||||||
restoreAuthSession();
|
|
||||||
|
|
||||||
// Auth functions
|
// Auth functions
|
||||||
function restoreAuthSession() {
|
function restoreAuthSession() {
|
||||||
@ -117,6 +118,8 @@ function handleAuthClick() {
|
|||||||
function showAuthModal() {
|
function showAuthModal() {
|
||||||
document.getElementById('auth-modal-overlay').classList.add('show');
|
document.getElementById('auth-modal-overlay').classList.add('show');
|
||||||
document.getElementById('login-identifier').focus();
|
document.getElementById('login-identifier').focus();
|
||||||
|
// Remove first to prevent duplicate registrations
|
||||||
|
document.removeEventListener('keydown', handleAuthModalKeydown);
|
||||||
document.addEventListener('keydown', handleAuthModalKeydown);
|
document.addEventListener('keydown', handleAuthModalKeydown);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,6 +155,15 @@ function switchAuthTab(tab) {
|
|||||||
document.getElementById('register-form').style.display = tab === 'register' ? 'block' : 'none';
|
document.getElementById('register-form').style.display = tab === 'register' ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared helper: safely parse error response regardless of Content-Type
|
||||||
|
async function parseErrorResponse(response) {
|
||||||
|
try {
|
||||||
|
return await response.json();
|
||||||
|
} catch {
|
||||||
|
return { error: `Server error (${response.status})`, details: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
const identifier = document.getElementById('login-identifier').value.trim();
|
const identifier = document.getElementById('login-identifier').value.trim();
|
||||||
const password = document.getElementById('login-password').value;
|
const password = document.getElementById('login-password').value;
|
||||||
@ -161,6 +173,9 @@ async function handleLogin() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitBtn = document.getElementById('login-submit-btn');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${gameState.apiUrl}/api/v1/auth/login`, {
|
const response = await fetch(`${gameState.apiUrl}/api/v1/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -169,8 +184,8 @@ async function handleLogin() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const err = await response.json();
|
const err = await parseErrorResponse(response);
|
||||||
flashErrorMessage(err.error || 'Login failed');
|
flashErrorMessage(err.details || err.error || 'Login failed', 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +198,8 @@ async function handleLogin() {
|
|||||||
hideAuthModal();
|
hideAuthModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
flashErrorMessage('Connection failed');
|
flashErrorMessage('Connection failed');
|
||||||
|
} finally {
|
||||||
|
submitBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,11 +212,17 @@ async function handleRegister() {
|
|||||||
flashErrorMessage('Username and password required');
|
flashErrorMessage('Username and password required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
flashErrorMessage('Password min 8 chars');
|
flashErrorMessage('Password min 8 chars');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) {
|
||||||
|
flashErrorMessage('Password needs a letter and number');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitBtn = document.getElementById('register-submit-btn');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = { username, password };
|
const body = { username, password };
|
||||||
@ -212,8 +235,8 @@ async function handleRegister() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const err = await response.json();
|
const err = await parseErrorResponse(response);
|
||||||
flashErrorMessage(err.details || err.error || 'Registration failed');
|
flashErrorMessage(err.details || err.error || 'Registration failed', 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +249,8 @@ async function handleRegister() {
|
|||||||
hideAuthModal();
|
hideAuthModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
flashErrorMessage('Connection failed');
|
flashErrorMessage('Connection failed');
|
||||||
|
} finally {
|
||||||
|
submitBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,17 +277,7 @@ function authFetch(url, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getConfig() {
|
async function getConfig() {
|
||||||
try {
|
return { apiUrl: '/chess' };
|
||||||
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);
|
|
||||||
return { apiUrl: 'http://localhost:8080' };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startHealthCheck() {
|
function startHealthCheck() {
|
||||||
@ -972,18 +987,17 @@ function handleApiError(action, error, response = null) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function flashErrorMessage(message) {
|
function flashErrorMessage(message, duration = 1500) {
|
||||||
const overlay = document.getElementById('error-flash-overlay');
|
const overlay = document.getElementById('error-flash-overlay');
|
||||||
const messageEl = document.getElementById('error-flash-message');
|
const messageEl = document.getElementById('error-flash-message');
|
||||||
|
|
||||||
// Set message text
|
|
||||||
messageEl.textContent = message;
|
messageEl.textContent = message;
|
||||||
|
|
||||||
// Show overlay
|
|
||||||
overlay.classList.add('show');
|
overlay.classList.add('show');
|
||||||
|
|
||||||
// Auto-hide after animation completes
|
// Clear any pending timeout to avoid premature hide on rapid calls
|
||||||
setTimeout(() => {
|
if (overlay._flashTimeout) clearTimeout(overlay._flashTimeout);
|
||||||
|
overlay._flashTimeout = setTimeout(() => {
|
||||||
overlay.classList.remove('show');
|
overlay.classList.remove('show');
|
||||||
}, 1500);
|
overlay._flashTimeout = null;
|
||||||
}
|
}, duration);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user