docs: añade documentación completa del proyecto
- README.md: descripción general, stack, instalación rápida - docs/API.md: referencia completa de API REST y WebSocket - docs/ARCHITECTURE.md: arquitectura del sistema con diagramas - docs/INSTALLATION.md: guía detallada de instalación - backend/.env.example: plantilla de configuración Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
492
docs/API.md
Normal file
492
docs/API.md
Normal file
@@ -0,0 +1,492 @@
|
||||
# API Reference - Trivy
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
http://localhost:8000/api
|
||||
```
|
||||
|
||||
## Autenticación
|
||||
|
||||
El panel de administración usa autenticación básica HTTP.
|
||||
|
||||
```bash
|
||||
# Ejemplo con curl
|
||||
curl -u admin:admin123 http://localhost:8000/api/admin/questions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints REST
|
||||
|
||||
### Game
|
||||
|
||||
#### GET /game/categories
|
||||
|
||||
Obtiene todas las categorías disponibles.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Nintendo",
|
||||
"icon": "🍄",
|
||||
"color": "#E60012"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Xbox",
|
||||
"icon": "🎮",
|
||||
"color": "#107C10"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### GET /game/today-questions
|
||||
|
||||
Obtiene las preguntas activas para hoy (sin respuestas).
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"1": [
|
||||
{
|
||||
"id": 1,
|
||||
"category_id": 1,
|
||||
"question_text": "¿En qué año se lanzó la NES?",
|
||||
"difficulty": 1,
|
||||
"points": 100,
|
||||
"time_seconds": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Admin - Preguntas
|
||||
|
||||
#### GET /admin/questions
|
||||
|
||||
Lista todas las preguntas.
|
||||
|
||||
**Query Parameters:**
|
||||
- `status` (optional): `pending`, `approved`, `used`
|
||||
- `category_id` (optional): ID de categoría
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"category_id": 1,
|
||||
"question_text": "¿En qué año se lanzó la NES?",
|
||||
"correct_answer": "1983",
|
||||
"alt_answers": ["1984"],
|
||||
"difficulty": 1,
|
||||
"points": 100,
|
||||
"time_seconds": 15,
|
||||
"date_active": "2024-01-26",
|
||||
"status": "approved",
|
||||
"fun_fact": "La NES salvó la industria del videojuego",
|
||||
"created_at": "2024-01-25T10:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### POST /admin/questions
|
||||
|
||||
Crea una nueva pregunta.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"category_id": 1,
|
||||
"question_text": "¿Cuál es el nombre del fontanero de Nintendo?",
|
||||
"correct_answer": "Mario",
|
||||
"alt_answers": ["Super Mario"],
|
||||
"difficulty": 1,
|
||||
"points": 100,
|
||||
"time_seconds": 15,
|
||||
"date_active": "2024-01-26",
|
||||
"status": "approved",
|
||||
"fun_fact": "Mario apareció por primera vez en Donkey Kong"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `201 Created`
|
||||
|
||||
#### PUT /admin/questions/{id}
|
||||
|
||||
Actualiza una pregunta existente.
|
||||
|
||||
#### DELETE /admin/questions/{id}
|
||||
|
||||
Elimina una pregunta.
|
||||
|
||||
---
|
||||
|
||||
### Admin - Categorías
|
||||
|
||||
#### GET /admin/categories
|
||||
|
||||
Lista todas las categorías.
|
||||
|
||||
#### POST /admin/categories
|
||||
|
||||
Crea una nueva categoría.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Ciencia",
|
||||
"icon": "🔬",
|
||||
"color": "#00BCD4"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Admin - Salas
|
||||
|
||||
#### GET /admin/rooms/active
|
||||
|
||||
Obtiene las salas activas en Redis.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"code": "ABC123",
|
||||
"status": "playing",
|
||||
"host": "Player1",
|
||||
"teams": {
|
||||
"A": [{"name": "Player1", "team": "A"}],
|
||||
"B": [{"name": "Player2", "team": "B"}]
|
||||
},
|
||||
"scores": {"A": 500, "B": 300},
|
||||
"current_team": "A"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebSocket Events
|
||||
|
||||
### Conexión
|
||||
|
||||
```javascript
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
const socket = io('http://localhost:8000', {
|
||||
transports: ['websocket', 'polling']
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Eventos Cliente → Servidor
|
||||
|
||||
#### create_room
|
||||
|
||||
Crea una nueva sala de juego.
|
||||
|
||||
```javascript
|
||||
socket.emit('create_room', {
|
||||
player_name: 'MiNombre'
|
||||
});
|
||||
```
|
||||
|
||||
#### join_room
|
||||
|
||||
Unirse a una sala existente.
|
||||
|
||||
```javascript
|
||||
socket.emit('join_room', {
|
||||
room_code: 'ABC123',
|
||||
player_name: 'MiNombre',
|
||||
team: 'A' // o 'B'
|
||||
});
|
||||
```
|
||||
|
||||
#### change_team
|
||||
|
||||
Cambiar de equipo en el lobby.
|
||||
|
||||
```javascript
|
||||
socket.emit('change_team', {
|
||||
team: 'B'
|
||||
});
|
||||
```
|
||||
|
||||
#### start_game
|
||||
|
||||
Iniciar el juego (solo host).
|
||||
|
||||
```javascript
|
||||
socket.emit('start_game', {});
|
||||
```
|
||||
|
||||
#### select_question
|
||||
|
||||
Seleccionar una pregunta del tablero.
|
||||
|
||||
```javascript
|
||||
socket.emit('select_question', {
|
||||
question_id: 1,
|
||||
category_id: 1
|
||||
});
|
||||
```
|
||||
|
||||
#### submit_answer
|
||||
|
||||
Enviar respuesta a la pregunta actual.
|
||||
|
||||
```javascript
|
||||
socket.emit('submit_answer', {
|
||||
answer: 'Mi respuesta',
|
||||
question: { /* objeto pregunta */ },
|
||||
is_steal: false
|
||||
});
|
||||
```
|
||||
|
||||
#### steal_decision
|
||||
|
||||
Decidir si intentar robar.
|
||||
|
||||
```javascript
|
||||
socket.emit('steal_decision', {
|
||||
attempt: true, // o false para pasar
|
||||
question_id: 1,
|
||||
answer: 'Mi respuesta' // solo si attempt=true
|
||||
});
|
||||
```
|
||||
|
||||
#### chat_message
|
||||
|
||||
Enviar mensaje al chat general.
|
||||
|
||||
```javascript
|
||||
socket.emit('chat_message', {
|
||||
message: 'Hola a todos!'
|
||||
});
|
||||
```
|
||||
|
||||
#### team_message
|
||||
|
||||
Enviar mensaje al chat de equipo.
|
||||
|
||||
```javascript
|
||||
socket.emit('team_message', {
|
||||
room_code: 'ABC123',
|
||||
team: 'A',
|
||||
player_name: 'MiNombre',
|
||||
message: 'Mensaje privado del equipo'
|
||||
});
|
||||
```
|
||||
|
||||
#### send_reaction
|
||||
|
||||
Enviar reacción emoji.
|
||||
|
||||
```javascript
|
||||
socket.emit('send_reaction', {
|
||||
emoji: '🎉',
|
||||
room_code: 'ABC123',
|
||||
player_name: 'MiNombre'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Eventos Servidor → Cliente
|
||||
|
||||
#### room_created
|
||||
|
||||
Confirmación de sala creada.
|
||||
|
||||
```javascript
|
||||
socket.on('room_created', (data) => {
|
||||
console.log(data.room.code); // 'ABC123'
|
||||
});
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"room": {
|
||||
"code": "ABC123",
|
||||
"status": "waiting",
|
||||
"host": "Player1",
|
||||
"teams": {"A": [...], "B": []},
|
||||
"scores": {"A": 0, "B": 0}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### player_joined
|
||||
|
||||
Un jugador se unió a la sala.
|
||||
|
||||
```javascript
|
||||
socket.on('player_joined', (data) => {
|
||||
console.log('Nuevo jugador:', data.room.teams);
|
||||
});
|
||||
```
|
||||
|
||||
#### player_left
|
||||
|
||||
Un jugador abandonó la sala.
|
||||
|
||||
```javascript
|
||||
socket.on('player_left', (data) => {
|
||||
console.log('Jugador salió');
|
||||
});
|
||||
```
|
||||
|
||||
#### game_started
|
||||
|
||||
El juego ha comenzado.
|
||||
|
||||
```javascript
|
||||
socket.on('game_started', (data) => {
|
||||
// data.room.board contiene el tablero con las preguntas
|
||||
console.log('Categorías:', Object.keys(data.room.board));
|
||||
});
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"room": {
|
||||
"status": "playing",
|
||||
"board": {
|
||||
"1": [
|
||||
{
|
||||
"id": 1,
|
||||
"question_text": "...",
|
||||
"difficulty": 1,
|
||||
"points": 100,
|
||||
"answered": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"current_team": "A",
|
||||
"current_player_index": {"A": 0, "B": 0}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### question_selected
|
||||
|
||||
Se seleccionó una pregunta.
|
||||
|
||||
```javascript
|
||||
socket.on('question_selected', (data) => {
|
||||
console.log('Pregunta:', data.question_id);
|
||||
});
|
||||
```
|
||||
|
||||
#### answer_result
|
||||
|
||||
Resultado de una respuesta.
|
||||
|
||||
```javascript
|
||||
socket.on('answer_result', (data) => {
|
||||
if (data.valid) {
|
||||
console.log('¡Correcto! +', data.points_earned);
|
||||
} else {
|
||||
console.log('Incorrecto:', data.reason);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"reason": "Respuesta correcta",
|
||||
"points_earned": 100,
|
||||
"was_steal": false,
|
||||
"room": { /* estado actualizado */ }
|
||||
}
|
||||
```
|
||||
|
||||
#### steal_prompt
|
||||
|
||||
Oportunidad de robo disponible.
|
||||
|
||||
```javascript
|
||||
socket.on('steal_prompt', (data) => {
|
||||
console.log('¡Puedes robar!');
|
||||
});
|
||||
```
|
||||
|
||||
#### game_finished
|
||||
|
||||
El juego ha terminado.
|
||||
|
||||
```javascript
|
||||
socket.on('game_finished', (data) => {
|
||||
console.log('Ganador:', data.winner);
|
||||
console.log('Puntuación final:', data.room.scores);
|
||||
});
|
||||
```
|
||||
|
||||
#### new_reaction
|
||||
|
||||
Nueva reacción de un jugador.
|
||||
|
||||
```javascript
|
||||
socket.on('new_reaction', (data) => {
|
||||
console.log(data.player_name, 'reaccionó con', data.emoji);
|
||||
});
|
||||
```
|
||||
|
||||
#### team_message
|
||||
|
||||
Mensaje de chat de equipo.
|
||||
|
||||
```javascript
|
||||
socket.on('team_message', (data) => {
|
||||
console.log(`[Equipo] ${data.player_name}: ${data.message}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### error
|
||||
|
||||
Error del servidor.
|
||||
|
||||
```javascript
|
||||
socket.on('error', (data) => {
|
||||
console.error('Error:', data.message);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Códigos de Error
|
||||
|
||||
| Código | Mensaje | Descripción |
|
||||
|--------|---------|-------------|
|
||||
| `room_not_found` | Room not found | La sala no existe |
|
||||
| `room_full` | Room is full | La sala está llena |
|
||||
| `not_host` | Only the host can start | Solo el host puede iniciar |
|
||||
| `need_players` | Both teams need players | Ambos equipos necesitan jugadores |
|
||||
| `not_your_turn` | Not your turn | No es tu turno |
|
||||
| `question_answered` | Question already answered | Pregunta ya contestada |
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- API REST: 100 requests/minuto por IP
|
||||
- WebSocket: Sin límite específico (controlado por lógica de juego)
|
||||
|
||||
## Timeouts
|
||||
|
||||
- Preguntas: 15-35 segundos según dificultad
|
||||
- Robo: 50% del tiempo original
|
||||
- Sala inactiva: 3 horas (configurable)
|
||||
Reference in New Issue
Block a user