718 lines
20 KiB
JavaScript
718 lines
20 KiB
JavaScript
import { Router } from "express";
|
|
import { pool } from './db.js';
|
|
import Joi from 'joi';
|
|
import j2s from "joi-to-swagger";
|
|
|
|
export class ApiError extends Error {
|
|
constructor(status, message, cause) {
|
|
super(message);
|
|
this.status = status;
|
|
this.cause = cause;
|
|
}
|
|
}
|
|
|
|
const router = Router();
|
|
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
|
|
|
const userSchema = Joi.object({
|
|
username: Joi.string().min(3).max(32).required()
|
|
});
|
|
|
|
const createGameSchema = Joi.object({
|
|
user: Joi.number().integer().positive().required(),
|
|
is_local: Joi.boolean().default(false)
|
|
});
|
|
|
|
const addPlayerSchema = Joi.object({
|
|
user: Joi.number().integer().positive().required()
|
|
});
|
|
|
|
const lockGameSchema = Joi.object({
|
|
user: Joi.number().integer().positive().required()
|
|
});
|
|
|
|
const turnSchema = Joi.object({
|
|
user: Joi.number().integer().positive().required(),
|
|
first_throw: Joi.number().integer().min(0).required(),
|
|
second_throw: Joi.number().integer().min(0).required(),
|
|
third_throw: Joi.number().integer().min(0).required(),
|
|
});
|
|
|
|
const endGameSchema = Joi.object({
|
|
winner: Joi.number().integer().positive()
|
|
});
|
|
|
|
const schemasByPath = {
|
|
"/api/users": userSchema,
|
|
"/api/games": createGameSchema,
|
|
"/api/games/:id/players": addPlayerSchema,
|
|
"/api/games/:id/lock": lockGameSchema,
|
|
"/api/games/:id/turns": turnSchema,
|
|
"/api/games/:id/end": endGameSchema,
|
|
};
|
|
|
|
export function generateRequestBodies() {
|
|
const requestBodies = {};
|
|
|
|
for (const path in schemasByPath) {
|
|
const { swagger } = j2s(schemasByPath[path]);
|
|
requestBodies[path] = {
|
|
content: {
|
|
"application/json": {
|
|
schema: swagger
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
return requestBodies;
|
|
}
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/healthcheck:
|
|
* get:
|
|
* summary: Check if the API is running and if the database is reachable
|
|
* responses:
|
|
* 200:
|
|
* description: API and DB are healthy
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* status: "ok"
|
|
* db: "ok"
|
|
* 500:
|
|
* description: Database unreachable
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* status: "error"
|
|
* db: "down"
|
|
*/
|
|
router.get("/healthcheck", async (req, res) => {
|
|
try {
|
|
await pool.query("SELECT 1"); // basic DB check
|
|
res.json({ status: "ok", db: "ok" });
|
|
} catch {
|
|
res.status(500).json({ status: "error", db: "down" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/users/{id}:
|
|
* get:
|
|
* summary: Get a user by ID
|
|
* parameters:
|
|
* - name: id
|
|
* in: path
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 200:
|
|
* description: User data
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* id: 1
|
|
* username: "john_doe"
|
|
* 400:
|
|
* description: Invalid user ID
|
|
* 404:
|
|
* description: User not found
|
|
*/
|
|
router.get("/users/:id", asyncHandler(async (req, res) => {
|
|
const userId = parseInt(req.params.id, 10);
|
|
if (isNaN(userId)) throw new ApiError(400, "Invalid user ID");
|
|
|
|
const [rows] = await pool.query("SELECT * FROM users WHERE id = ?", [userId]);
|
|
if (!rows.length) throw new ApiError(404, "User not found");
|
|
|
|
res.json(rows[0]);
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/users:
|
|
* post:
|
|
* summary: Create a new user or return existing user if username already exists
|
|
* responses:
|
|
* 200:
|
|
* description: User created or existing user returned
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* id: 1
|
|
* username: "john_doe"
|
|
* 400:
|
|
* description: Validation error
|
|
*/
|
|
router.post("/users", asyncHandler(async (req, res) => {
|
|
const { error, value } = userSchema.validate(req.body);
|
|
if (error) throw new ApiError(400, error.details[0].message);
|
|
|
|
const { username } = value;
|
|
const [result] = await pool.query(
|
|
`INSERT INTO users (username)
|
|
VALUES (?)
|
|
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)`,
|
|
[username]
|
|
);
|
|
|
|
const [userRow] = await pool.query("SELECT * FROM users WHERE id = ?", [result.insertId]);
|
|
res.status(200).json(userRow[0]);
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/games:
|
|
* get:
|
|
* summary: Get all current games
|
|
* responses:
|
|
* 200:
|
|
* description: List of current games
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* - id: 1
|
|
* is_open: true
|
|
* is_local: false
|
|
* current_playing_user: 1
|
|
* turn_order: null
|
|
*/
|
|
router.get("/games", async (req, res) => {
|
|
const [rows] = await pool.query("SELECT * FROM games");
|
|
res.json(rows);
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/games/{id}:
|
|
* get:
|
|
* summary: Get a specific game by ID
|
|
* parameters:
|
|
* - name: id
|
|
* in: path
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 200:
|
|
* description: Game data
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* id: 1
|
|
* is_open: true
|
|
* is_local: false
|
|
* current_playing_user: 1
|
|
* turn_order: null
|
|
* creator: 1
|
|
*/
|
|
router.get("/games/:id", asyncHandler(async (req, res) => {
|
|
const gameId = parseInt(req.params.id, 10);
|
|
if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
|
|
|
|
const [games] = await pool.query("SELECT * FROM games WHERE id = ?", [gameId]);
|
|
if (!games.length) throw new ApiError(404, "Game not found");
|
|
|
|
const game = games[0];
|
|
|
|
const [creatorRows] = await pool.query(
|
|
`SELECT u.id
|
|
FROM game_players gp
|
|
JOIN users u ON gp.user = u.id
|
|
WHERE gp.game = ? AND gp.is_creator = TRUE
|
|
LIMIT 1`,
|
|
[gameId]
|
|
);
|
|
const creator = creatorRows.length ? creatorRows[0].id : null;
|
|
|
|
const [players] = await pool.query(
|
|
`SELECT u.id, u.username
|
|
FROM game_players gp
|
|
JOIN users u ON gp.user = u.id
|
|
WHERE gp.game = ?`,
|
|
[gameId]
|
|
);
|
|
|
|
const pointsPromises = players.map(async p => {
|
|
const [lastTurn] = await pool.query(
|
|
"SELECT end_points FROM turns WHERE game = ? AND user = ? ORDER BY id DESC LIMIT 1",
|
|
[gameId, p.id]
|
|
);
|
|
return {
|
|
...p,
|
|
points: lastTurn.length ? lastTurn[0].end_points : 501
|
|
};
|
|
});
|
|
const playersWithPoints = await Promise.all(pointsPromises);
|
|
|
|
res.json({
|
|
...game,
|
|
creator,
|
|
players: playersWithPoints
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/games:
|
|
* post:
|
|
* summary: Create a new game
|
|
* responses:
|
|
* 201:
|
|
* description: Game created
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* game:
|
|
* id: 1
|
|
* is_open: true
|
|
* is_local: false
|
|
* current_playing_user: 1
|
|
* turn_order: null
|
|
* players:
|
|
* - id: 1
|
|
* username: "john_doe"
|
|
*/
|
|
router.post("/games", asyncHandler(async (req, res) => {
|
|
const { error, value } = createGameSchema.validate(req.body);
|
|
if (error) throw new ApiError(400, error.details[0].message);
|
|
|
|
const { user, is_local } = value;
|
|
const conn = await pool.getConnection();
|
|
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
const [gameResult] = await conn.query(
|
|
"INSERT INTO games (is_open, is_local) VALUES (?, ?)",
|
|
[true, is_local]
|
|
);
|
|
|
|
const gameId = gameResult.insertId;
|
|
|
|
await conn.query(
|
|
"INSERT INTO game_players (game, user, is_creator) VALUES (?, ?, ?)",
|
|
[gameId, user, true]
|
|
);
|
|
|
|
const [gameRow] = await conn.query("SELECT * FROM games WHERE id = ?", [gameId]);
|
|
const [players] = await conn.query(
|
|
`SELECT u.id, u.username FROM game_players gp
|
|
JOIN users u ON gp.user = u.id
|
|
WHERE gp.game = ?`,
|
|
[gameId]
|
|
);
|
|
|
|
await conn.commit();
|
|
res.status(201).json({ game: gameRow[0], players });
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw new ApiError(500, "Failed to create game", err);
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/games/{id}/players:
|
|
* post:
|
|
* summary: Add a player to a game
|
|
* parameters:
|
|
* - name: id
|
|
* in: path
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 201:
|
|
* description: Player added
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* game: 1
|
|
* players:
|
|
* - id: 1
|
|
* username: "john_doe"
|
|
* - id: 2
|
|
* username: "jane_doe"
|
|
*/
|
|
router.post("/games/:id/players", asyncHandler(async (req, res) => {
|
|
const gameId = parseInt(req.params.id, 10);
|
|
if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
|
|
|
|
const { error, value } = addPlayerSchema.validate(req.body);
|
|
if (error) throw new ApiError(400, error.details[0].message);
|
|
|
|
const { user } = value;
|
|
const conn = await pool.getConnection();
|
|
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
const [alreadyJoined] = await conn.query(
|
|
"SELECT 1 FROM game_players WHERE game = ? AND user = ?",
|
|
[gameId, user]
|
|
);
|
|
if (alreadyJoined.length) throw new ApiError(400, "User already joined this game");
|
|
|
|
const [gameRows] = await conn.query(
|
|
"SELECT * FROM games WHERE id = ? AND is_open = TRUE",// AND is_local = FALSE", TODO: wehn im body der richtige creator angegeben wurde wir user zu dem local game hinzugefügt
|
|
[gameId]
|
|
);
|
|
|
|
if (!gameRows.length) throw new ApiError(400, "Game is not open or is local");
|
|
|
|
await conn.query(
|
|
"INSERT IGNORE INTO game_players (game, user, is_creator) VALUES (?, ?, ?)",
|
|
[gameId, user, false]
|
|
);
|
|
|
|
const [players] = await conn.query(
|
|
`SELECT u.id, u.username FROM game_players gp
|
|
JOIN users u ON gp.user = u.id
|
|
WHERE gp.game = ?`,
|
|
[gameId]
|
|
);
|
|
|
|
await conn.commit();
|
|
res.status(201).json({ game: gameId, players });
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw new ApiError(500, "Failed to add player", err);
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/games/{id}/lock:
|
|
* patch:
|
|
* summary: Lock a game and generate turn order
|
|
* parameters:
|
|
* - name: id
|
|
* in: path
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 200:
|
|
* description: Game locked with turn order
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* message: "Game locked"
|
|
* turnOrder: [1,2,3]
|
|
* 400:
|
|
* description: Invalid game ID / Validation error
|
|
* 403:
|
|
* description: Only the creator can lock the game
|
|
* 500:
|
|
* description: Failed to lock game
|
|
*/
|
|
router.patch("/games/:id/lock", asyncHandler(async (req, res) => {
|
|
const gameId = parseInt(req.params.id, 10);
|
|
if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
|
|
|
|
const { error, value } = lockGameSchema.validate(req.body);
|
|
if (error) throw new ApiError(400, error.details[0].message);
|
|
|
|
const { user } = value;
|
|
const conn = await pool.getConnection();
|
|
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
const [creatorRows] = await conn.query(
|
|
"SELECT * FROM game_players WHERE game = ? AND user = ? AND is_creator = TRUE",
|
|
[gameId, user]
|
|
);
|
|
|
|
if (!creatorRows.length) throw new ApiError(403, "Only the creator can lock the game");
|
|
|
|
const [players] = await conn.query(
|
|
"SELECT user FROM game_players WHERE game = ?",
|
|
[gameId]
|
|
);
|
|
|
|
const turnOrder = players.map(p => p.user);
|
|
for (let i = turnOrder.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[turnOrder[i], turnOrder[j]] = [turnOrder[j], turnOrder[i]];
|
|
}
|
|
|
|
const currentPlayingUser = turnOrder[0];
|
|
await conn.query(
|
|
"UPDATE games SET is_open = FALSE, turn_order = ?, current_playing_user = ? WHERE id = ?",
|
|
[JSON.stringify(turnOrder), currentPlayingUser, gameId]
|
|
);
|
|
|
|
await conn.commit();
|
|
res.json({ message: "Game locked", turnOrder });
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw new ApiError(500, "Failed to lock game", err);
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/games/{id}/end:
|
|
* patch:
|
|
* summary: End a game
|
|
* parameters:
|
|
* - name: id
|
|
* in: path
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 200:
|
|
* description: Game marked as finished
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* message: "Game finished"
|
|
* gameId: 1
|
|
*/
|
|
router.patch("/games/:id/end", asyncHandler(async (req,res) => {
|
|
const gameId = parseInt(req.params.id, 10);
|
|
if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
|
|
|
|
const { error, value } = endGameSchema.validate(req.body);
|
|
if (error) throw new ApiError(400, error.details[0].message);
|
|
|
|
const { winner } = value;
|
|
|
|
const conn = await pool.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
const [gameRows] = await conn.query("SELECT * FROM games WHERE id = ?", [gameId]);
|
|
if (!gameRows.length) throw new ApiError(404, "Game not found");
|
|
if (gameRows[0].is_finished) throw new ApiError(400, "Game is already finished");
|
|
|
|
if (winner) {
|
|
const [playerRows] = await conn.query(
|
|
"SELECT 1 FROM game_players WHERE game = ? AND user = ?",
|
|
[gameId, winner]
|
|
);
|
|
if (!playerRows.length) throw new ApiError(400, "Winner must be a player in the game");
|
|
}
|
|
|
|
await conn.query(
|
|
"UPDATE games SET is_finished = TRUE, winner = ? WHERE id = ?",
|
|
[winner || null, gameId]
|
|
);
|
|
|
|
await conn.commit();
|
|
res.json({ message: "Game finished", gameId });
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw new ApiError(500, "Failed to finish game", err);
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/games/{id}/players:
|
|
* get:
|
|
* summary: Get all players in a game
|
|
* parameters:
|
|
* - name: id
|
|
* in: path
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 200:
|
|
* description: List of players
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* - id: 1
|
|
* username: "john_doe"
|
|
* - id: 2
|
|
* username: "jane_doe"
|
|
*/
|
|
router.get("/games/:id/players", asyncHandler(async (req, res) => {
|
|
const gameId = parseInt(req.params.id, 10);
|
|
if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
|
|
|
|
const [players] = await pool.query(
|
|
`SELECT u.id, u.username FROM game_players gp
|
|
JOIN users u ON gp.user = u.id
|
|
WHERE gp.game = ?`,
|
|
[gameId]
|
|
);
|
|
res.json(players);
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/games/{id}/turns:
|
|
* post:
|
|
* summary: Record a new turn for a player
|
|
* parameters:
|
|
* - name: id
|
|
* in: path
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 201:
|
|
* description: Turn recorded
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* turnId: 1
|
|
* nextPlayer: 2
|
|
* 400:
|
|
* description: Validation error / Invalid game ID
|
|
* 403:
|
|
* description: Not your turn
|
|
* 500:
|
|
* description: Failed to record turn
|
|
*/
|
|
router.post("/games/:id/turns", asyncHandler(async (req, res) => {
|
|
const gameId = parseInt(req.params.id, 10);
|
|
if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
|
|
|
|
const { error, value } = turnSchema.validate(req.body);
|
|
if (error) throw new ApiError(400, error.details[0].message);
|
|
|
|
const { user, first_throw, second_throw, third_throw } = value;
|
|
const conn = await pool.getConnection();
|
|
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
const [gameRows] = await conn.query(
|
|
"SELECT current_playing_user, turn_order FROM games WHERE id = ? FOR UPDATE",
|
|
[gameId]
|
|
);
|
|
if (!gameRows.length) throw new ApiError(404, "Game not found");
|
|
const game = gameRows[0];
|
|
if (game.current_playing_user !== user) throw new ApiError(403, "Not your turn");
|
|
|
|
const [lastTurn] = await conn.query(
|
|
"SELECT end_points FROM turns WHERE game = ? AND user = ? ORDER BY id DESC LIMIT 1",
|
|
[gameId, user]
|
|
);
|
|
|
|
const start_points = lastTurn.length ? lastTurn[0].end_points : 501;
|
|
const totalScore = first_throw + second_throw + third_throw;
|
|
let end_points = start_points - totalScore;
|
|
|
|
if (end_points < 0) {
|
|
end_points = start_points;
|
|
}
|
|
|
|
const round_number = lastTurn.length ? lastTurn[0].round_number + 1 : 1;
|
|
|
|
const [result] = await conn.query(
|
|
`INSERT INTO turns
|
|
(game, user, round_number, start_points, first_throw, second_throw, third_throw, end_points)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[gameId, user, round_number, start_points, first_throw, second_throw, third_throw, end_points]
|
|
);
|
|
|
|
const turnOrder = JSON.parse(game.turn_order || '[]');
|
|
const currentIndex = turnOrder.indexOf(user);
|
|
const nextIndex = (currentIndex + 1) % turnOrder.length;
|
|
const nextPlayer = turnOrder[nextIndex];
|
|
|
|
await conn.query(
|
|
"UPDATE games SET current_playing_user = ? WHERE id = ?",
|
|
[nextPlayer, gameId]
|
|
);
|
|
|
|
await conn.commit();
|
|
res.status(201).json({ turnId: result.insertId, start_points, end_points, nextPlayer });
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw new ApiError(500, "Failed to record turn", err);
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/games/{id}/turns:
|
|
* get:
|
|
* summary: Get turns of a game
|
|
* parameters:
|
|
* - name: id
|
|
* in: path
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 201:
|
|
* description: The turn
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* turnId: 1
|
|
* nextPlayer: 2
|
|
*/
|
|
router.get("/games/:id/turns", asyncHandler(async (req, res) => {
|
|
const gameId = parseInt(req.params.id, 10);
|
|
if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
|
|
|
|
const [rows] = await pool.query(
|
|
"SELECT * FROM turns WHERE game = ? ORDER BY round_number",
|
|
[gameId]
|
|
);
|
|
res.json(rows);
|
|
}));
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/turns/{id}:
|
|
* get:
|
|
* summary: Get a specific turn by ID
|
|
* parameters:
|
|
* - name: id
|
|
* in: path
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 200:
|
|
* description: Turn data
|
|
* content:
|
|
* application/json:
|
|
* example:
|
|
* id: 1
|
|
* game: 1
|
|
* user: 1
|
|
* round_number: 1
|
|
* start_points: 501
|
|
* first_throw: 60
|
|
* second_throw: 45
|
|
* third_throw: 36
|
|
* end_points: 360
|
|
*/
|
|
router.get("/turns/:id", asyncHandler(async (req, res) => {
|
|
const turnId = parseInt(req.params.id, 10);
|
|
if (isNaN(turnId)) throw new ApiError(400, "Invalid turn ID");
|
|
|
|
const [rows] = await pool.query("SELECT * FROM turns WHERE id = ?", [turnId]);
|
|
if (!rows.length) throw new ApiError(404, "Turn not found");
|
|
|
|
res.json(rows[0]);
|
|
}));
|
|
|
|
export default router;
|