Backend completo del progetto AW1 2024/25 – Esame #3 "Indovina la Frase".
Server Express.js con API REST sicure, autenticazione Passport, sistema monete intelligente, timer automatici e protezioni anti-cheat complete.
Questo server implementa il backend completo per il gioco "Indovina la Frase" con le seguenti caratteristiche principali:
- Due modalità di gioco: utenti autenticati con sistema monete vs modalità ospite gratuita
- Sistema costi intelligente: lettere con prezzi variabili basati su frequenza linguistica
- Timer automatico: 60 secondi per partita con gestione timeout server-side
- Sicurezza anti-cheat: validazioni rigorose, controlli ownership, no leak informazioni
- Database transazionale: SQLite con integrità referenziale e operazioni atomiche
- Architettura RESTful: API semantiche con autenticazione basata su sessione
# 1. Posizionati nella cartella server
cd server
# 2. Installa dipendenze
npm install
# 3. Inizializza database con utenti demo e frasi
npm run initdb
# 4. Avvia server in modalità sviluppo
nodemon index.mjsServer attivo su: http://localhost:3001
Comando principale: cd server; nodemon index.mjs
Username: novice | Password: NoviceUser123! | Monete: 100 (nuovo utente)
Username: empty | Password: EmptyCoins456@ | Monete: 0 (test monete esaurite)
Username: player | Password: PlayerGame789# | Monete: 180 (con storico partite)
server/
├─ index.mjs # Entry point: Express app, middleware, routing
├─ nodemon.json # Configurazione nodemon per sviluppo
├─ package.json # Dipendenze, script, metadata progetto
│
├─ api/ # Router API REST
│ ├─ sessions.js # Autenticazione: login, logout, stato sessione
│ ├─ games.js # Partite utenti autenticati (con monete)
│ ├─ guest.js # Partite modalità ospite (senza monete)
│ └─ meta.js # Metadati di sistema (costi lettere)
│
├─ auth/ # Sistema autenticazione
│ └─ passport.js # Strategia Local, middleware, serializzazione
│
├─ lib/ # Librerie e utilities
│ ├─ db.js # Database: wrapper SQLite con Promise e transazioni
│ └─ costs.js # Sistema costi lettere, logica frequenze linguistiche
│
├─ db/ # Database SQLite
│ ├─ schema.sql # Schema tabelle, indici, vincoli
│ ├─ seed.sql # Frasi del gioco (30 autenticati + 3 ospiti)
│ └─ aw1.db # File database (auto-generato)
│
├─ scripts/ # Script utility
│ └─ initdb.mjs # Inizializzazione database completa
│
└─ tests/ # Suite di test e debugging
├─ *.http # Test API con REST Client
├─ *.mjs # Script di test automatizzati
└─ *_GUIDE.md # Guide per testing e debugging
- Node.js 22.x LTS — Runtime JavaScript ad alte prestazioni
- Express 4.21+ — Framework web minimalista e veloce
- ES6 Modules — Import/export moderni per codice pulito
- SQLite 3 — Database embedded, zero-configuration
- sqlite3 5.1+ — Driver Node.js nativo e performante
- Transazioni ACID — Operazioni atomiche garantite
- Foreign Keys — Integrità referenziale completa
- Passport.js 0.7 — Framework autenticazione modulare
- passport-local — Strategia username/password locale
- express-session — Gestione sessioni sicure con cookie
- bcryptjs — Hash password con salt (10 rounds)
- CORS configurato — Solo origin autorizzato con credentials
- express-validator — Validazione e sanitizzazione robusta
- Morgan — Logging richieste HTTP con colori per debugging
- Architettura "Due Server": Client React (5173) ↔ API Server (3001)
- RESTful API Design: Endpoints semantici, verbi HTTP appropriati
- Session-based Auth: Cookie HttpOnly, SameSite=Lax, Secure in prod
Note Generali:
- Tutte le risposte sono in formato JSON
- Timestamp in millisecondi (
timeLeft,startedAt,expiresAt)- Rotte autenticate richiedono cookie di sessione valido
- Headers richiesti:
Content-Type: application/jsonper POST requests- Validazione rigorosa: tutti gli input vengono validati con express-validator
- Sanitizzazione automatica: prevenzione XSS e SQL injection
- Rate limiting: protezione contro spam e attacchi DoS
- Errori 500: Tutte le API possono restituire
500 Internal Server Errorper errori server non gestiti
Autentica un utente e crea una sessione sicura con cookie HttpOnly.
Endpoint: POST /api/sessions
Auth: Non richiesta
Content-Type: application/json
Request Body:
{
"username": "novice",
"password": "NoviceUser123!"
}Request Schema:
username(string, required): Nome utente, minimo 1 caratterepassword(string, required): Password utente, minimo 1 carattere
Response 200 - Success:
{
"id": 1,
"username": "novice",
"coins": 100
}Imposta automaticamente cookie di sessione aw1.sid HttpOnly
Response 400 - Bad Request:
{
"error": "Invalid payload",
"details": [
{ "msg": "username required", "param": "username" },
{ "msg": "password required", "param": "password" }
]
}Response 401 - Unauthorized:
{
"error": "Credenziali non valide"
}Possibili Errori di Validazione:
- Username vuoto
- Password vuota
- Credenziali errate
- Utente non esistente
Controlla se l'utente è attualmente autenticato e restituisce i suoi dati.
Endpoint: GET /api/sessions/current
Auth: Non richiesta (verifica sessione)
Query Parameters: Nessuno
Response 200 - Utente Autenticato:
{
"authenticated": true,
"user": {
"id": 1,
"username": "novice",
"coins": 95
}
}Response 200 - Utente Non Autenticato:
{
"authenticated": false
}Invalida la sessione corrente e rimuove il cookie di autenticazione.
Endpoint: DELETE /api/sessions/current
Auth: Cookie di sessione
Body: Nessuno
Response 204 - No Content: Nessun body di risposta, cookie rimosso
Regole Sistema Monete:
- Costo lettere: Vocali 10 monete (max 1), Consonanti 1-5 monete per frequenza
- Penalità miss: Costo raddoppiato per lettere sbagliate
- Premi: +100 monete per vittoria, -20 monete per timeout, 0 per abbandono
// Vocali (10 monete base, max 1 per partita)
VOCALI: A, E, I, O, U → 10 monete base
// Consonanti per frequenza linguistica (costo base + raddoppio se miss)
COSTO 5: T, N, H, S // Lettere più frequenti
COSTO 4: R, L, D, C // Alta frequenza
COSTO 3: M, W, Y, F // Media frequenza
COSTO 2: G, P, B, V // Bassa frequenza
COSTO 1: K, J, X, Q, Z // Lettere rare
// Esempi calcolo finale:
// Lettera T presente: 5 monete
// Lettera T assente: 10 monete (5 × 2)
// Vocale A presente: 10 monete
// Vocale A assente: 20 monete (10 × 2)Crea una nuova partita per l'utente autenticato con frase casuale.
Endpoint: POST /api/games
Auth: Cookie di sessione richiesto
Content-Type: application/json
Body: Nessuno
Prerequisiti:
- Utente autenticato con sessione valida
- Saldo minimo: 1 moneta (per permettere almeno un tentativo)
Response 201 - Created:
{
"gameId": 123,
"status": "running",
"phraseLength": 42,
"spaces": [8, 14, 26, 35],
"revealed": [],
"revealedLetters": {},
"hasUsedVowel": false,
"timeLeft": 60000,
"coins": 95
}Response Schema:
gameId(number): ID univoco partita creatastatus(string): Stato partita, sempre "running" alla creazionephraseLength(number): Lunghezza totale frase in caratterispaces(array): Indici posizioni spazi nella fraserevealed(array): Indici lettere rivelate (vuoto all'inizio)revealedLetters(object): Mappa posizione → lettera (vuoto all'inizio)hasUsedVowel(boolean): Flag uso vocale (false all'inizio)timeLeft(number): Millisecondi rimanenti (60000 = 60s)coins(number): Saldo monete aggiornato dell'utente
Response 403 - Forbidden:
{
"error": "💸 Non hai abbastanza monete per iniziare una partita! 🎮"
}Restituisce lo stato attuale di una partita specifica dell'utente.
Endpoint: GET /api/games/:id
Auth: Cookie di sessione richiesto
Path Parameters:
id(number, required): ID univoco della partita
Response 200 - Partita In Corso:
{
"gameId": 123,
"status": "running",
"phraseLength": 42,
"spaces": [8, 14, 26, 35],
"revealed": [0, 15, 16, 17],
"revealedLetters": {
"0": "B",
"15": "T",
"16": "H",
"17": "E"
},
"hasUsedVowel": true,
"timeLeft": 45230,
"coins": 85
}Response 200 - Partita Terminata:
{
"gameId": 123,
"status": "won",
"phraseLength": 42,
"spaces": [8, 14, 26, 35],
"revealed": [0, 2, 4, 8, 15, 16, 17, 22, 24, 28, 35],
"revealedLetters": {},
"hasUsedVowel": true,
"timeLeft": 0,
"coins": 195,
"phrase": "be kind to yourself every single day"
}Comportamenti Automatici:
- Timeout automatico: Se
timeLeft ≤ 0, applica status "timeout" e penalità -20 monete - Frase nascosta: Campo
phrasepresente solo se status ≠ "running" (sicurezza anti-cheat) - revealedLetters: Mappa vuota se partita terminata (ottimizzazione)
Response 400 - Bad Request:
{
"error": "Invalid game id"
}Response 403 - Forbidden:
{
"error": "Forbidden"
}Response 404 - Not Found:
{
"error": "Game not found"
}Response 409 - Conflict:
{
"error": "Game already ended (timeout)",
"status": "timeout",
"phrase": "be kind to yourself every single day"
}Acquista e tenta una lettera specifica nella partita corrente.
Endpoint: POST /api/games/:id/guess-letter
Auth: Cookie di sessione richiesto
Content-Type: application/json
Path Parameters:
id(number, required): ID univoco della partita
Request Body:
{
"letter": "A"
}Request Schema:
letter(string, required): Singolo carattere A-Z (case insensitive)
Response 200 - Lettera Trovata (Hit):
{
"revealedIndexes": [5, 12, 28],
"revealed": [0, 5, 12, 15, 16, 17, 28],
"revealedLetters": {
"0": "B",
"5": "A",
"12": "A",
"15": "T",
"16": "H",
"17": "E",
"28": "A"
},
"costApplied": 10,
"coins": 85,
"hasUsedVowel": true,
"timeLeft": 43100
}Response 200 - Lettera Non Trovata (Miss):
{
"revealedIndexes": [],
"revealed": [0, 15, 16, 17],
"revealedLetters": {
"0": "B",
"15": "T",
"16": "H",
"17": "E"
},
"costApplied": 20,
"coins": 65,
"hasUsedVowel": true,
"timeLeft": 41800
}Response 200 - Lettera Già Utilizzata:
{
"revealedIndexes": [],
"revealed": [0, 15, 16, 17],
"revealedLetters": {
"0": "B",
"15": "T",
"16": "H",
"17": "E"
},
"costApplied": 0,
"coins": 85,
"hasUsedVowel": true,
"timeLeft": 43100
}Response Schema:
revealedIndexes(array): Nuove posizioni rivelate da questa letterarevealed(array): Tutte le posizioni rivelate fino ad orarevealedLetters(object): Mappa completa posizione → lettera rivelatacostApplied(number): Monete effettivamente spese per questo tentativocoins(number): Saldo monete aggiornato dell'utentehasUsedVowel(boolean): Flag aggiornato uso vocaletimeLeft(number): Millisecondi rimanenti
Logica Costi Applicata:
- Lettera nuova presente: Costo base (1-10 monete)
- Lettera nuova assente: Costo base × 2 (penalità miss)
- Lettera già usata: Costo 0 (nessun addebito)
- Monete insufficienti: Addebita tutto il saldo residuo
Response 400 - Bad Request:
{
"error": "🚫 Carattere mancante! Inserisci una lettera valida (A-Z) 📝"
}{
"error": "⚠️ Inserisci solo un carattere alla volta! 📝"
}{
"error": "🚫 Carattere non valido! Usa solo lettere A-Z 🔤"
}{
"error": "Solo una vocale per partita"
}Response 403 - Forbidden:
{
"error": "Forbidden"
}{
"error": "No coins left for letters"
}Response 404 - Not Found:
{
"error": "Game not found"
}Response 409 - Conflict:
{
"error": "Game already ended (timeout)",
"status": "timeout",
"phrase": "be kind to yourself every single day"
}{
"error": "Game already ended (won)"
}Tenta di indovinare l'intera frase per vincere immediatamente la partita.
Endpoint: POST /api/games/:id/guess-phrase
Auth: Cookie di sessione richiesto
Content-Type: application/json
Path Parameters:
id(number, required): ID univoco della partita
Request Body:
{
"attempt": "be kind to yourself every single day"
}Request Schema:
attempt(string, required): Tentativo frase completa, 1-100 caratteri
Response 200 - Frase Corretta (Vittoria):
{
"result": "win",
"status": "won",
"coinsDelta": 100,
"phrase": "be kind to yourself every single day",
"coins": 195
}Response 200 - Frase Errata:
{
"result": "wrong",
"status": "running",
"message": "Not the correct phrase",
"timeLeft": 35600,
"coins": 85
}Response Schema (Vittoria):
result(string): "win" per vittoriastatus(string): "won" stato finale partitacoinsDelta(number): Monete guadagnate (+100)phrase(string): Frase completa rivelatacoins(number): Saldo finale aggiornato
Response Schema (Errore):
result(string): "wrong" per tentativo erratostatus(string): "running" partita continuamessage(string): Messaggio descrittivotimeLeft(number): Millisecondi rimanenticoins(number): Saldo attuale (nessuna penalità)
Comportamento:
- Confronto case-insensitive: "Hello World" = "hello world"
- Nessuna penalità: Tentativi errati non costano monete
- Vittoria immediata: +100 monete e status "won"
- Partita continua: Se sbagliato, permette altri tentativi
Response 400 - Bad Request:
{
"error": "Invalid input"
}Response 403 - Forbidden:
{
"error": "Forbidden"
}Response 404 - Not Found:
{
"error": "Game not found"
}Response 409 - Conflict:
{
"error": "Game already ended (timeout)",
"status": "timeout",
"phrase": "be kind to yourself every single day"
}{
"error": "Game already ended (won)"
}Termina volontariamente la partita corrente senza penalità monetarie.
Endpoint: POST /api/games/:id/abandon
Auth: Cookie di sessione richiesto
Content-Type: application/json
Path Parameters:
id(number, required): ID univoco della partita
Body: Nessuno
Response 200 - Abbandono Confermato:
{
"status": "abandoned",
"phrase": "be kind to yourself every single day"
}Response 200 - Partita Già Terminata:
{
"status": "won",
"phrase": "be kind to yourself every single day"
}Response Schema:
status(string): Stato finale partita ("abandoned", "won", "timeout")phrase(string): Frase completa rivelata
Comportamento:
- Nessuna penalità: Abbandono volontario non costa monete
- Stato finale: Partita impostata su "abandoned"
- Frase rivelata: Mostra soluzione completa
- Idempotente: Chiamate multiple su partita già terminata restituiscono stato attuale
Response 400 - Bad Request:
{
"error": "Invalid game id"
}Response 403 - Forbidden:
{
"error": "Forbidden"
}Response 404 - Not Found:
{
"error": "Game not found"
}Caratteristiche Modalità Guest:
- Accesso: Solo utenti NON autenticati (redirect se logged in)
- Costi: Tutte le lettere gratuite, nessuna limitazione vocali
- Timer: Identico modalità auth (60 secondi)
- Pool frasi: 3 frasi semplificate dedicate agli ospiti
- Premi: Nessun guadagno monetario (solo soddisfazione personale)
Crea una nuova partita gratuita per utenti non registrati.
Endpoint: POST /api/guest/games
Auth: Nessuna (utente deve essere NON autenticato)
Content-Type: application/json
Body: Nessuno
Prerequisiti:
- Utente NON deve essere autenticato
- Pool frasi guest disponibili
Response 201 - Created:
{
"gameId": 456,
"status": "running",
"phraseLength": 35,
"spaces": [5, 9, 15, 27],
"revealed": [],
"timeLeft": 60000
}Response Schema:
gameId(number): ID univoco partita gueststatus(string): Stato partita, sempre "running" alla creazionephraseLength(number): Lunghezza totale frase in caratterispaces(array): Indici posizioni spazi nella fraserevealed(array): Indici lettere rivelate (vuoto all'inizio)timeLeft(number): Millisecondi rimanenti (60000 = 60s)
Campi Non Presenti (differenze da modalità auth):
coins: Non applicabile per ospitihasUsedVowel: Nessun limite vocali per guestrevealedLetters: Semplificazione per guest
Response 403 - Forbidden:
{
"error": "Guest mode not available for authenticated users"
}Recupera lo stato attuale di una partita guest specifica.
Endpoint: GET /api/guest/games/:id
Auth: Nessuna (utente deve essere NON autenticato)
Path Parameters:
id(number, required): ID univoco della partita guest
Response 200 - Partita In Corso:
{
"gameId": 456,
"status": "running",
"phraseLength": 35,
"spaces": [5, 9, 15, 27],
"revealed": [0, 2, 15, 22],
"revealedLetters": {
"0": "E",
"2": "E",
"15": "C",
"22": "G"
},
"timeLeft": 42300
}Response 200 - Partita Terminata:
{
"gameId": 456,
"status": "won",
"phraseLength": 35,
"spaces": [5, 9, 15, 27],
"revealed": [0, 2, 4, 5, 8, 15, 18, 22, 26, 28, 32],
"revealedLetters": {},
"timeLeft": 0,
"phrase": "every day is a new chance to grow"
}Comportamenti Automatici:
- Timeout automatico: Se
timeLeft ≤ 0, applica status "timeout" (nessuna penalità) - Frase nascosta: Campo
phrasepresente solo se status ≠ "running" - revealedLetters: Mappa vuota se partita terminata
Response 400 - Bad Request:
{
"error": "Invalid game id"
}Response 403 - Forbidden:
{
"error": "Guest mode not available for authenticated users"
}Response 404 - Not Found:
{
"error": "Guest game not found"
}Response 409 - Conflict:
{
"error": "Game already ended (timeout)",
"status": "timeout",
"phrase": "every day is a new chance to grow"
}Tenta una lettera specifica nella partita guest (sempre gratuito).
Endpoint: POST /api/guest/games/:id/guess-letter
Auth: Nessuna (utente deve essere NON autenticato)
Content-Type: application/json
Path Parameters:
id(number, required): ID univoco della partita guest
Request Body:
{
"letter": "E"
}Request Schema:
letter(string, required): Singolo carattere A-Z (case insensitive)
Response 200 - Lettera Trovata:
{
"revealedIndexes": [0, 2, 26],
"revealed": [0, 2, 15, 22, 26],
"revealedLetters": {
"0": "E",
"2": "E",
"15": "C",
"22": "G",
"26": "E"
},
"costApplied": 0,
"timeLeft": 40800
}Response 200 - Lettera Non Trovata:
{
"revealedIndexes": [],
"revealed": [0, 2, 15, 22],
"revealedLetters": {
"0": "E",
"2": "E",
"15": "C",
"22": "G"
},
"costApplied": 0,
"timeLeft": 39200
}Response 200 - Lettera Già Utilizzata:
{
"revealedIndexes": [],
"revealed": [0, 2, 15, 22],
"revealedLetters": {
"0": "E",
"2": "E",
"15": "C",
"22": "G"
},
"costApplied": 0,
"timeLeft": 40800
}Response Schema:
revealedIndexes(array): Nuove posizioni rivelate da questa letterarevealed(array): Tutte le posizioni rivelate fino ad orarevealedLetters(object): Mappa completa posizione → lettera rivelatacostApplied(number): Sempre 0 per modalità guesttimeLeft(number): Millisecondi rimanenti
Caratteristiche Guest:
- Sempre gratuito:
costAppliedsempre 0 - Nessun limite vocali: Possibili tentativi illimitati A,E,I,O,U
- Stessa validazione: Controllo carattere A-Z identico a modalità auth
- Nessuna penalità miss: Stesso costo per hit/miss
Response 400 - Bad Request:
{
"error": "🚫 Carattere mancante! Inserisci una lettera valida (A-Z) 📝"
}{
"error": "⚠️ Inserisci solo un carattere alla volta! 📝"
}{
"error": "🚫 Carattere non valido! Usa solo lettere A-Z 🔤"
}Response 403 - Forbidden:
{
"error": "Guest mode not available for authenticated users"
}Response 404 - Not Found:
{
"error": "Guest game not found"
}Response 409 - Conflict:
{
"error": "Game already ended (timeout)",
"status": "timeout",
"phrase": "every day is a new chance to grow"
}Tenta di indovinare l'intera frase per completare la partita guest.
Endpoint: POST /api/guest/games/:id/guess-phrase
Auth: Nessuna (utente deve essere NON autenticato)
Content-Type: application/json
Path Parameters:
id(number, required): ID univoco della partita guest
Request Body:
{
"attempt": "every day is a new chance to grow"
}Request Schema:
attempt(string, required): Tentativo frase completa, 1-100 caratteri
Response 200 - Frase Corretta (Vittoria):
{
"result": "win",
"status": "won",
"phrase": "every day is a new chance to grow"
}Response 200 - Frase Errata:
{
"result": "wrong",
"status": "running",
"message": "Not the correct phrase",
"timeLeft": 35600
}Response Schema (Vittoria):
result(string): "win" per vittoriastatus(string): "won" stato finale partitaphrase(string): Frase completa rivelata
Response Schema (Errore):
result(string): "wrong" per tentativo erratostatus(string): "running" partita continuamessage(string): Messaggio descrittivotimeLeft(number): Millisecondi rimanenti
Differenze da Modalità Auth:
- Nessun guadagno monete: Campo
coinsDeltanon presente - Nessun costo: Tentativi sempre gratuiti
- Stesso comportamento: Logica vittoria/errore identica
Response 400 - Bad Request:
{
"error": "Invalid input"
}Response 403 - Forbidden:
{
"error": "Guest mode not available for authenticated users"
}Response 404 - Not Found:
{
"error": "Guest game not found"
}Response 409 - Conflict:
{
"error": "Game already ended (timeout)",
"status": "timeout",
"phrase": "every day is a new chance to grow"
}Termina volontariamente la partita guest corrente.
Endpoint: POST /api/guest/games/:id/abandon
Auth: Nessuna (utente deve essere NON autenticato)
Content-Type: application/json
Path Parameters:
id(number, required): ID univoco della partita guest
Body: Nessuno
Response 200 - Abbandono Confermato:
{
"status": "abandoned",
"phrase": "every day is a new chance to grow"
}Response 200 - Partita Già Terminata:
{
"status": "won",
"phrase": "every day is a new chance to grow"
}Response Schema:
status(string): Stato finale partita ("abandoned", "won", "timeout")phrase(string): Frase completa rivelata
Comportamento:
- Nessuna penalità: Modalità guest non ha sistema monetario
- Stato finale: Partita impostata su "abandoned"
- Frase rivelata: Mostra soluzione completa
- Idempotente: Chiamate multiple restituiscono stato attuale
Response 400 - Bad Request:
{
"error": "Invalid game id"
}Response 403 - Forbidden:
{
"error": "Guest mode not available for authenticated users"
}Response 404 - Not Found:
{
"error": "Guest game not found"
}Restituisce la tabella completa dei costi per tutte le lettere dell'alfabeto.
Endpoint: GET /api/meta/letter-costs
Auth: Non richiesta
Query Parameters: Nessuno
Response 200 - Success:
{
"letters": [
{ "letter": "A", "type": "vowel", "baseCost": 10 },
{ "letter": "B", "type": "consonant", "baseCost": 2 },
{ "letter": "C", "type": "consonant", "baseCost": 4 },
{ "letter": "D", "type": "consonant", "baseCost": 4 },
{ "letter": "E", "type": "vowel", "baseCost": 10 },
{ "letter": "F", "type": "consonant", "baseCost": 3 },
{ "letter": "G", "type": "consonant", "baseCost": 2 },
{ "letter": "H", "type": "consonant", "baseCost": 5 },
{ "letter": "I", "type": "vowel", "baseCost": 10 },
{ "letter": "J", "type": "consonant", "baseCost": 1 },
{ "letter": "K", "type": "consonant", "baseCost": 1 },
{ "letter": "L", "type": "consonant", "baseCost": 4 },
{ "letter": "M", "type": "consonant", "baseCost": 3 },
{ "letter": "N", "type": "consonant", "baseCost": 5 },
{ "letter": "O", "type": "vowel", "baseCost": 10 },
{ "letter": "P", "type": "consonant", "baseCost": 2 },
{ "letter": "Q", "type": "consonant", "baseCost": 1 },
{ "letter": "R", "type": "consonant", "baseCost": 4 },
{ "letter": "S", "type": "consonant", "baseCost": 5 },
{ "letter": "T", "type": "consonant", "baseCost": 5 },
{ "letter": "U", "type": "vowel", "baseCost": 10 },
{ "letter": "V", "type": "consonant", "baseCost": 2 },
{ "letter": "W", "type": "consonant", "baseCost": 3 },
{ "letter": "X", "type": "consonant", "baseCost": 1 },
{ "letter": "Y", "type": "consonant", "baseCost": 3 },
{ "letter": "Z", "type": "consonant", "baseCost": 1 }
]
}Response Schema:
letters(array): Lista completa lettere con metadatiletter(string): Lettera maiuscola A-Ztype(string): Tipo lettera ("vowel" o "consonant")baseCost(number): Costo base in monete
Utilizzo:
- Frontend: Visualizzazione costi nella tastiera virtuale
- Sviluppo: Debug e testing sistema costi
- Documentazione: Riferimento completo pricing
Logica Costi Ricorda:
- Costo finale hit:
baseCost(se lettera presente) - Costo finale miss:
baseCost × 2(se lettera assente) - Modalità guest: Sempre gratuito indipendentemente da
baseCost
Database: SQLite v3 (file
db/aw1.db)
Configurazione: Foreign Keys abilitate, transazioni ACID complete Validazione: Vincoli referenziali e controlli di integrità a livello database
Scopo: Gestisce gli account degli utenti autenticati con sistema monetario integrato per acquistare lettere durante le partite.
Struttura Completa:
id(INTEGER PRIMARY KEY AUTOINCREMENT): Identificatore univoco dell'utente, chiave primaria auto-incrementaleusername(TEXT UNIQUE NOT NULL): Nome utente univoco per login, utilizzato per l'autenticazionepassword_hash(TEXT NOT NULL): Hash bcrypt della password utente (saltRounds=10), mai memorizzata in chiarocoins(INTEGER NOT NULL DEFAULT 100 CHECK (coins >= 0)): Saldo monete per acquistare lettere, con vincolo non-negativo
Validazioni e Vincoli:
- Username univoco tramite constraint UNIQUE per evitare duplicati
- Saldo monete non può mai essere negativo (CHECK constraint)
- Password sempre hashata con bcrypt per sicurezza
- Valore di default 100 monete per nuovi utenti
Scopo: Contiene tutte le frasi disponibili per il gioco, suddivise per modalità autenticata e guest con difficoltà differenziata.
Struttura Completa:
id(INTEGER PRIMARY KEY AUTOINCREMENT): Identificatore univoco della frase, chiave primaria auto-incrementaletext(TEXT NOT NULL): Testo completo della frase da indovinare, solo lettere maiuscole e spazimode(TEXT NOT NULL CHECK (mode IN ('auth','guest'))): Modalità di gioco per cui la frase è destinata
Validazioni e Vincoli:
- Lunghezza frase obbligatoria tra 30 e 50 caratteri (CHECK constraint)
- Mode limitato a 'auth' o 'guest' tramite CHECK constraint
- Contenuto: 30 frasi motivazionali per utenti autenticati + 3 semplificate per ospiti
- Frasi auth più complesse e varie, frasi guest più semplici e accessibili
Scopo: Traccia tutte le partite attive e completate, gestendo timer automatico, economia delle monete e stati di gioco per utenti registrati e ospiti.
Struttura Completa:
id(INTEGER PRIMARY KEY AUTOINCREMENT): Identificatore univoco della partita, chiave primaria auto-incrementaleuserId(INTEGER NULL, FK → users.id): Collegamento all'utente proprietario, NULL per partite guestphraseId(INTEGER NOT NULL, FK → phrases.id): Riferimento alla frase da indovinare per questa partitastatus(TEXT NOT NULL CHECK (status IN ('running','won','timeout','abandoned','ended'))): Stato corrente della partitarunning: Partita in corso, timer attivowon: Partita vinta dall'utente, +100 monete se authtimeout: Scaduto il timer (60s), -20 monete penalty se authabandoned: Abbandonata volontariamente, nessuna penaltyended: Stato generico di chiusura
startedAt(INTEGER NOT NULL): Timestamp Unix (millisecondi) di inizio partitaexpiresAt(INTEGER NOT NULL): Timestamp Unix (millisecondi) di scadenza automatica (startedAt + 60000ms)hasUsedVowel(INTEGER NOT NULL DEFAULT 0 CHECK (hasUsedVowel IN (0,1))): Flag booleano uso vocale (0=no, 1=sì), limite 1 vocale per partita authcoinsSpent(INTEGER NOT NULL DEFAULT 0): Totale monete spese per lettere durante questa partitacoinsDelta(INTEGER NOT NULL DEFAULT 0): Variazione finale saldo utente (+100 vittoria, -20 timeout, 0 abbandono)
Logica Timer e Economia:
- Timer fisso 60 secondi per tutte le partite
- Sistema monetario solo per utenti autenticati (userId NOT NULL)
- Partite guest (userId NULL) sempre gratuite
- Controllo automatico scadenza quando
Date.now() > expiresAt
Scopo: Mantiene cronologia completa di ogni lettera tentata in ogni partita, con tracciamento costi applicati e risultati per audit e prevenzione duplicati.
Struttura Completa:
id(INTEGER PRIMARY KEY AUTOINCREMENT): Identificatore univoco del tentativo lettera, chiave primaria auto-incrementalegameId(INTEGER NOT NULL, FK → games.id): Riferimento alla partita di appartenenzaletter(TEXT NOT NULL CHECK (length(letter)=1 AND letter BETWEEN 'A' AND 'Z'))`: Lettera tentata, singolo carattere maiuscolo A-ZwasHit(INTEGER NOT NULL CHECK (wasHit IN (0,1)))`: Risultato tentativo (0=miss, 1=hit), indica se la lettera era presente nella frasecostApplied(INTEGER NOT NULL)`: Monete effettivamente addebitate per questo tentativo (può essere 0 per duplicati o guest)
Protezioni e Validazioni:
- Constraint UNIQUE(gameId, letter) impedisce acquisti duplicati della stessa lettera
- Validazione lettera singola maiuscola A-Z
- wasHit booleano per tracking accurato hit/miss rate
- costApplied traccia il costo effettivo (baseCost per hit, baseCost×2 per miss, 0 per duplicati/guest)
users (1) ←→ (0..n) games # Un utente può avere molte partite (NULL per guest)
phrases (1) ←→ (0..n) games # Una frase può essere usata in molte partite simultanee
games (1) ←→ (0..n) game_letters # Una partita traccia molte lettere tentate
Relazioni Dettagliate:
- users → games: Relazione 1:N con supporto guest (userId NULL), ON DELETE comportamento definito da business logic
- phrases → games: Relazione 1:N permettendo riutilizzo frasi in partite multiple, distribuzione casuale
- games → game_letters: Relazione 1:N con cascading per cronologia completa, vincolo univocità lettera/partita
Indici di Performance:
idx_users_username— Ottimizza ricerca utenti durante login e autenticazioneidx_phrases_mode— Accelera filtro frasi per modalità ('auth'/'guest') nella selezione casualeidx_games_userId— Migliora query cronologia partite per utente specificoidx_games_phraseId— Ottimizza analisi utilizzo frasi e statisticheidx_game_letters_gameId— Accelera recupero lettere tentate per partita specifica
session({
name: 'aw1.sid',
secret: 'production-secret-key',
cookie: {
httpOnly: true, // Impedisce accesso JavaScript client-side
sameSite: 'lax', // Protezione CSRF
secure: true, // HTTPS obbligatorio in produzione
}
})cors({
origin: 'http://localhost:5173', // Solo client autorizzato
credentials: true, // Cookie inclusi nelle richieste
methods: ['GET', 'POST', 'DELETE'], // Metodi limitati
allowedHeaders: ['Content-Type'] // Header limitati
})// Login - validazione credenziali (api/sessions.js)
body('username')
.isString()
.trim()
.isLength({ min: 1 })
.withMessage('username required'),
body('password')
.isString()
.isLength({ min: 1 })
.withMessage('password required')
// Lettera - validazione formato (api/games.js, api/guest.js)
body('letter')
.isString()
.isLength({ min: 1, max: 1 })
.matches(/^[A-Za-z]$/)
.withMessage('Carattere non valido! Usa solo lettere A-Z')
// Frase - validazione tentativo (api/games.js, api/guest.js)
body('attempt')
.isString()
.trim()
.isLength({ min: 1 })
.withMessage('Tentativo frase non può essere vuoto')
// Parametri URL - validazione ID partita
param('id')
.isInt({ min: 1 })
.withMessage('Game ID deve essere intero positivo')Il codice implementa messaggi di errore specifici e user-friendly:
// Validazione lettera con messaggi emoji (games.js, guest.js)
if (req.body.letter === undefined || req.body.letter === null) {
return res.status(400).json({
error: '🚫 Carattere mancante! Inserisci una lettera valida (A-Z) 📝'
});
}
if (typeof req.body.letter !== 'string' || req.body.letter.length !== 1) {
return res.status(400).json({
error: '⚠️ Inserisci solo un carattere alla volta! 📝'
});
}
if (!/^[A-Za-z]$/.test(req.body.letter)) {
return res.status(400).json({
error: '🚫 Carattere non valido! Usa solo lettere A-Z 🔤'
});
}- Partite utenti: accesso solo alle proprie tramite
userIdin sessione - Partite guest: validazione che sia effettivamente guest (
userId IS NULL) - Cross-user protection: impossibile accedere a partite di altri utenti
- Frase nascosta: mai inviata mentre
status='running' - Rivelazione graduale: solo lettere effettivamente acquistate
- Validazione server-side: tutti i controlli ripetuti lato server
- No shortcuts: impossibile bypassare costi o limiti dal client
- Controlli transazionali: tutte le operazioni monete in transazione atomica
- Saldi non negativi: vincolo database
coins >= 0sempre rispettato - Doppio controllo: validazione monete prima e dopo operazioni
- Audit trail: ogni movimento monete tracciato in
coinsDelta
📚 Per informazioni complete su testing e metodologie:
🧪 TESTING_GUIDE.md - Guida completa al sistema di testing
| Codice | Significato | Quando | Esempio |
|---|---|---|---|
| 200 | Success | Operazione completata | Lettera trovata, frase indovinata |
| 201 | Created | Risorsa creata | Nuova partita avviata |
| 204 | No Content | Logout completato | Cookie rimosso |
| 400 | Bad Request | Input non valido | "🚫 Carattere non valido A-Z 🔤" |
| 401 | Unauthorized | Login richiesto | "Not authenticated" |
| 403 | Forbidden | Accesso negato | "💸 Monete insufficienti! 🎮" |
| 404 | Not Found | Risorsa inesistente | "Game not found" |
| 409 | Conflict | Stato inconsistente | "Game already ended (timeout)" |
| 500 | Internal Error | Errore server | "Internal Server Error" |
{ "error": "Descrizione user-friendly" }{
"error": "Game already ended (timeout)",
"status": "timeout",
"phrase": "frase completa"
}{ "error": "🚫 Carattere mancante! Inserisci A-Z 📝" }
{ "error": "Solo una vocale per partita" }
{ "error": "Invalid game id" }{ "error": "💸 Non hai abbastanza monete!" }
{ "error": "Guest mode not available for authenticated users" }
{ "error": "Forbidden" }{ "error": "Game already ended (won)" }
{ "error": "Game already ended (abandoned)" }- Username:
novice| Password:NoviceUser123!| Monete: 100 - Scopo: Test primo utilizzo, esperienza utente nuovo
- Username:
empty| Password:EmptyCoins456@| Monete: 0 - Scopo: Verifica comportamento senza monete, blocchi accesso
- Username:
player| Password:PlayerGame789#| Monete: 180 - Scopo: Test funzionalità complete, gestione partite multiple
Frasi motivazionali da 30-50 caratteri:
"be kind to yourself every single day""small steps lead to big changes ahead""today is a good day to start fresh"
Frasi semplificate per visitatori:
"every day is a new chance to grow""be happy with who you are becoming""be happy about the person you are today"
Il database viene inizializzato con partite di esempio per l'utente player:
- Partita vittoriosa: Status
won, +100 monete - Partita scaduta: Status
timeout, -20 monete
{
"scripts": {
"dev": "nodemon index.mjs", // Sviluppo con auto-restart
"initdb": "node ./scripts/initdb.mjs", // Inizializzazione database
"test": "echo \"Error: no test specified\" && exit 1" // Placeholder per test
}
}Lo script initdb.mjs esegue:
- Creazione schema completo con vincoli e indici
- Popolamento frasi per entrambe le modalità
- Creazione utenti con password hash bcrypt
- Partite di esempio per testing avanzato
- Output credenziali per sviluppatori
Esecuzione: npm run initdb
Output tipico:
Recreating schema...
Seeding phrases...
Seeding users with bcrypt hashes...
CREDENZIALI UTENTI DI TEST:
Username: novice | Password: NoviceUser123! | Monete: 100
Username: empty | Password: EmptyCoins456@ | Monete: 0
Username: player | Password: PlayerGame789# | Monete: 180
Creating sample games for "player"...
Done. DB ready at db/aw1.db
┌─────────────────┐ ┌─────────────────┐
│ Client React │ HTTP │ Server API │
│ (Vite - 5173) │ ←────→ │ (Express - 3001)│
│ │ CORS │ │
│ • UI Components │ │ • REST Routes │
│ • State Mgmt │ │ • Auth Sessions │
│ • API Calls │ │ • DB Operations │
└─────────────────┘ └─────────────────┘
│
┌─────────────┐
│ SQLite │
│ (aw1.db) │
└─────────────┘
1. Client → POST /api/sessions (username, password)
2. Server → Valida credenziali con bcrypt
3. Server → Crea sessione Passport
4. Server → Imposta cookie HttpOnly
5. Client → Riceve dati utente + cookie
6. Client → Include cookie in richieste successive
7. Server → Deserializza utente da sessione
8. Server → Autorizza operazioni per utente
La cartella tests/ contiene:
- 🧪 TESTING_GUIDE.md — Guida completa al sistema di testing, codici errore e metodologie di validazione