Saltar a contenido

08 — Modelo de datos, persistencia, seguridad y logging

1. Entidades del dominio

User 1───0..1 Player ───* PlayerModel (uno por Player; versiones)
               │
Bot  1───────0..1 Player          (Player = quien ocupa un asiento: humano o bot)
               │
Team *──2 Player   (por partida)
               │
Game 1──2 Team, 1──4 Seat(Player)
Game 1──* Round(jugada) 1──4 Lance 1──* Action
Game 1──* GameEvent
Game *──0..1 Tournament
Player 1──* Statistics(snapshot por periodo)
Agent: configuración dentro de un Bot (no es una tabla de dominio)
Card: objeto de valor (no es una tabla)
Entidad Descripción
User Cuenta humana (login). Datos mínimos.
Player Identidad de juego: humano (user_id) o bot (bot_id).
Bot Bot registrado: nombre, versión, autor, nivel, entrypoint.
Team Pareja en una partida concreta.
Game Partida: config, seed, estado, resultado.
Round Jugada: mano, dealer, número.
Lance Lance dentro de una jugada: resultado, tantos.
Action Decisión de un jugador con su contexto público.
GameEvent Evento del log (doc 04).
PlayerModel Modelo estadístico serializado (JSON) y versionado.
Statistics Agregados por jugador y periodo.
Tournament Ejecución del torneo: seed, participantes, clasificación.

2. Esquema relacional (SQLite → PostgreSQL)

users(id PK, username UNIQUE, password_hash, avatar_path NULL, created_at, settings_json)
bots(id PK, name, version, author, level, entrypoint, active, UNIQUE(name, version))
players(id PK, kind CHECK(kind IN ('human','bot')), user_id FK NULL, bot_id FK NULL, display_name)
games(id PK, mode, status, seed, config_json, target, winner_team NULL,
      created_by FK users NULL, started_at, finished_at, tournament_id FK NULL)
game_seats(game_id FK, seat 0..3, team 0..1, player_id FK, PRIMARY KEY(game_id, seat))
rounds(id PK, game_id FK, round_no, dealer_seat, mano_seat, score_a_after, score_b_after)
actions(id PK, game_id FK, round_id FK, seq, seat, lance, action_json, context_json, outcome_json NULL, ts)
game_events(game_id FK, seq, kind, visibility, recipients, payload_json, ts, PRIMARY KEY(game_id, seq))
player_models(player_id FK, version, model_json, sample_size, updated_at, PRIMARY KEY(player_id, version))
statistics(player_id FK, period, stats_json, PRIMARY KEY(player_id, period))
tournaments(id PK, seed, started_at, finished_at, config_json)
tournament_results(tournament_id FK, bot_id FK, games, wins, losses, points_for, points_against, errors, elo)
achievements(user_id FK, code, earned_at, PRIMARY KEY(user_id, code))
  • Acceso a través de repositorios (persistence.repositories); el dominio no conoce SQLAlchemy.
  • Migraciones con Alembic desde el primer día, para que el paso a PostgreSQL sea cambiar la URL.
  • game_events con visibilidad PRIVATE o SYSTEM solo se sirven al dueño, al acabar la partida, o en modo dev local.

3. Seguridad

Amenaza Medida
Robo de contraseñas argon2id; nunca texto plano ni en logs; política de longitud mínima
Robo de sesión Cookie HttpOnly, Secure, SameSite=Lax; token de sesión aleatorio (secrets) guardado como hash; expiración
CSRF SameSite + token CSRF en formularios que cambian estado
Inyección / entrada malformada Pydantic en todas las entradas; acciones como unión discriminada; ORM con parámetros
Acción fuera de turno o ajena El servidor deriva el asiento de la sesión, no del cliente; el RulesEngine vuelve a validar
Fuga de información Solo Observation; tests de fuga; el modo dev no está en el build de producción
Acceso a datos ajenos Autorización por recurso: /players/{id}/statistics solo para el dueño (o perfil público opcional)
Abuso Rate limiting por IP/usuario en login y acciones
Bots maliciosos doc 05 §3
Fotografías Se procesan para generar el avatar; por defecto no se guarda el original (minimización); tamaño y tipo validados
Secretos Variables de entorno; nunca en el repositorio; .env en .gitignore

Endpoints (conceptuales)

Método Ruta Auth Descripción
POST /auth/register, /auth/login, /auth/logout —
POST /games opcional Crear partida (modo, bots, config)
GET /games/{id} asiento o espectador Observation del que llama
POST /games/{id}/action asiento Enviar una acción (también por WS)
WS /games/{id}/ws asiento Observaciones en tiempo real
GET /games/{id}/replay participante, partida terminada Eventos visibles tras el final
GET /bots, /bots/{id} — Catálogo
GET /players/{id}/statistics dueño Estadísticas
GET /scoreboard — Clasificación del torneo

4. Logging

  • logging estándar con logs estructurados (JSON) y niveles DEBUG / INFO / WARNING / ERROR.
  • Dos canales: musst.public (eventos públicos, operativa) y musst.secret (estado completo, desactivado salvo en modo dev local, nunca en producción ni en artefactos del CI público).
  • Un filtro RedactSecretsFilter elimina campos hand, deck, cards de cualquier registro fuera de musst.secret. Un test lo comprueba.
  • Logs del torneo: solo resultados y tipo de error del bot, no sus cartas.

5. Robustez

Fallo Respuesta
Bot lanza una excepción / acción ilegal / timeout BotFailure → Fallback (partida con humanos) o derrota (torneo)
Acción ilegal de un humano 422 con el motivo; el estado no cambia
Desconexión de un humano PAUSED; el estado se reconstruye desde el log; reconexión con su sesión; tras X min, abandono configurable
Error interno ERROR, se guarda la partida y se registra el traceback (sin secretos)
Reinicio del servidor Las partidas en curso se reconstruyen con seed + acciones desde la BD