From 7b604df87caeeab26a2d0765aee444c91e21d752 Mon Sep 17 00:00:00 2001 From: RHM Date: Thu, 11 Sep 2025 17:05:56 +0200 Subject: [PATCH] miauw --- backend/src/api.js | 261 +++++++++++++++++++++++++++++++------------ backend/src/db.js | 56 +++++----- backend/src/index.js | 19 +++- 3 files changed, 235 insertions(+), 101 deletions(-) diff --git a/backend/src/api.js b/backend/src/api.js index a5355fd..f634a82 100644 --- a/backend/src/api.js +++ b/backend/src/api.js @@ -3,115 +3,234 @@ import { pool } from './db.js'; const router = Router(); +/** + * @swagger + * /healthcheck: + * get: + * summary: Check if the API is running + * responses: + * 200: + * description: API is healthy + */ router.get("/healthcheck", (req, res) => { res.json({ status: "ok" }) }); -router.get("/users", async (req, res) => { - const [rows] = await pool.query(`SELECT * FROM users`); - res.json(rows); -}); - -router.get("/users/:id", async (req, res) => { - const [rows] = await pool.query(`SELECT * FROM users WHERE id = ?`, [req.params.id]); - res.json(rows[0] || null); -}); - router.post("/users", async (req, res) => { - const { username } = req.body; - const [result] = await pool.query(`INSERT INTO users (username) VALUES (?)`, [username]); - res.status(201).json({ id: result.insertId, username }); -}); + const { username, input_method = 0 } = req.body; -router.put("/users/:id", async (req, res) => { - const { username } = req.body; - await pool.query(`UPDATE users SET username = ? WHERE id = ?`, [username, req.params.id]); - res.json({ id: req.params.id, username }); -}); + const [result] = await pool.query( + "INSERT INTO users (username, input_method) VALUES (?, ?)", + [username, input_method] + ); -router.delete("/users/:id", async (req, res) => { - await pool.query(`DELETE FROM users WHERE id = ?`, [req.params.id]); - res.status(204).send(); -}); - -router.get("/users/:id/settings", async (req, res) => { - const [rows] = await pool.query(`SELECT * FROM user_settings WHERE user = ?`, [req.params.id]); - res.json(rows[0] || null); -}); - -router.put("/users/:id/settings", async (req, res) => { - const { input_method } = req.body; - await pool.query(` - INSERT INTO user_settings (user, input_method) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE input_method = ? - `, [req.params.id, input_method, input_method]); - res.json({ user: req.params.id, input_method }); + const [userRow] = await pool.query("SELECT * FROM users WHERE id = ?", [result.insertId]); + res.status(201).json(userRow[0]); }); router.get("/games", async (req, res) => { - const [rows] = await pool.query(`SELECT * FROM games`); + const [rows] = await pool.query("SELECT * FROM current_games"); res.json(rows); }); router.get("/games/:id", async (req, res) => { - const [rows] = await pool.query(`SELECT * FROM games WHERE id = ?`, [req.params.id]); + const [rows] = await pool.query("SELECT * FROM current_games WHERE id = ?", [req.params.id]); res.json(rows[0] || null); }); router.post("/games", async (req, res) => { - const [result] = await pool.query(`INSERT INTO games (created_at) VALUES (NOW())`); - res.status(201).json({ id: result.insertId, created_at: new Date() }); -}); + const { user, is_local = false } = req.body; + const conn = await pool.getConnection(); -router.put("/games/:id", async (req, res) => { - const { winner } = req.body; - await pool.query(`UPDATE games SET winner = ? WHERE id = ?`, [winner, req.params.id]); - res.json({ id: req.params.id, winner }); -}); + try { + await conn.beginTransaction(); -router.delete("/games/:id", async (req, res) => { - await pool.query(`DELETE FROM games WHERE id = ?`, [req.params.id]); - res.status(204).send(); -}); + const [gameResult] = await conn.query( + "INSERT INTO current_games (is_open, is_local, current_playing_user) VALUES (?, ?, ?)", + [true, is_local, user] + ); -router.get("/games/:id/players", async (req, res) => { - const [rows] = await pool.query(` - SELECT u.id, u.username - FROM game_players gp - JOIN users u ON gp.user = u.id - WHERE gp.game = ? - `, [req.params.id]); - res.json(rows); + 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 current_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 err; + } finally { + conn.release(); + } }); router.post("/games/:id/players", async (req, res) => { const { user } = req.body; - await pool.query(`INSERT IGNORE INTO game_players (game, user) VALUES (?, ?)`, [req.params.id, user]); - res.status(201).json({ game: req.params.id, user }); + const gameId = req.params.id; + + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + + const [gameRows] = await conn.query( + "SELECT * FROM current_games WHERE id = ? AND is_open = TRUE AND is_local = FALSE", + [gameId] + ); + + if (gameRows.length === 0) { + return res.status(400).json({ error: "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 err; + } finally { + conn.release(); + } }); -router.delete("/games/:id/players/:user", async (req, res) => { - await pool.query(`DELETE FROM game_players WHERE game = ? AND user = ?`, [req.params.id, req.params.user]); - res.status(204).send(); +router.patch("/games/:id/lock", async (req, res) => { + const { user } = req.body; + const gameId = req.params.id; + + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + + const [creatorRows] = await pool.query( + "SELECT * FROM game_players WHERE game = ? AND user = ? AND is_creator = TRUE", + [gameId, user] + ); + + if (creatorRows.length === 0) { + return res.status(403).json({ error: "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]]; + } + + await conn.query( + `UPDATE current_games SET is_open = FALSE, turn_order = ? WHERE id = ?`, + [JSON.stringify(turnOrder), gameId] + ); + + await conn.commit(); + res.json({ message: "Game locked", turnOrder }); + } catch (err) { + await conn.rollback(); + throw err; + } finally { + conn.release(); + } + + await pool.query("UPDATE current_games SET is_open = FALSE WHERE id = ?", [gameId]); + res.json({ message: "Game locked" }); +}); + +router.get("/games/:id/players", async (req, res) => { + 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 = ?`, + [req.params.id] + ); + res.json(players); }); router.get("/games/:id/turns", async (req, res) => { - const [rows] = await pool.query(`SELECT * FROM turns WHERE game = ? ORDER BY round_number`, [req.params.id]); + const [rows] = await pool.query( + "SELECT * FROM turns WHERE game = ? ORDER BY round_number", + [req.params.id] + ); res.json(rows); }); router.post("/games/:id/turns", async (req, res) => { const { user, round_number, start_points, first_throw, second_throw, third_throw, end_points } = req.body; - const [result] = await pool.query(` - INSERT INTO turns (game, user, round_number, start_points, first_throw, second_throw, third_throw, end_points) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, [req.params.id, user, round_number, start_points, first_throw, second_throw, third_throw, end_points]); - res.status(201).json({ id: result.insertId }); + const gameId = req.params.id; + const conn = await pool.getConnection(); + + try { + await conn.beginTransaction(); + + const [gameRows] = await conn.query( + "SELECT current_playing_user FROM current_games WHERE id = ? FOR UPDATE", + [gameId] + ); + + if (gameRows.length === 0) { + return res.status(404).json({ error: "Game not found" }); + } + + const game = gameRows[0]; + const turnOrder = JSON.parse(game.turn_order || '[]'); + + if (game.current_playing_user !== user) { + return res.status(403).json({ error: "Not your turn" }); + } + + 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 currentIndex = turnOrder.indexOf(user); + const nextIndex = (currentIndex + 1) % turnOrder.length; + const nextPlayer = turnOrder[nextIndex]; + + await conn.query( + "UPDATE current_games SET current_playing_user = ? WHERE id = ?", + [nextPlayer, gameId] + ); + + await conn.commit(); + + res.status(201).json({ + turnId: result.insertId, + nextPlayer + }); + } catch (err) { + await conn.rollback(); + throw err; + } finally { + conn.release(); + } }); router.get("/turns/:id", async (req, res) => { - const [rows] = await pool.query(`SELECT * FROM turns WHERE id = ?`, [req.params.id]); + const [rows] = await pool.query("SELECT * FROM turns WHERE id = ?", [req.params.id]); res.json(rows[0] || null); }); diff --git a/backend/src/db.js b/backend/src/db.js index e646673..f54a33f 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -21,56 +21,60 @@ const migrations = [ name: "init", up: async (conn) => { await conn.query(` - CREATE TABLE IF NOT EXISTS users ( + CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, - username VARCHAR(255) NOT NULL + username VARCHAR(32), + input_method INT ) `); await conn.query(` - CREATE TABLE IF NOT EXISTS user_settings ( - user INT NOT NULL, - input_method INT, - PRIMARY KEY (user), - FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE - ) - `); - - await conn.query(` - CREATE TABLE IF NOT EXISTS games ( + CREATE TABLE games ( id INT PRIMARY KEY AUTO_INCREMENT, winner INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (winner) REFERENCES users(id) ON DELETE SET NULL + FOREGIN KEY (winner) REFERENCES users(id) ) `); await conn.query(` - CREATE TABLE IF NOT EXISTS game_players ( + CREATE TABLE current_games ( id INT PRIMARY KEY AUTO_INCREMENT, - game INT NOT NULL, - user INT NOT NULL, - FOREIGN KEY (game) REFERENCES games(id) ON DELETE CASCADE, - FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE, - UNIQUE (game, user) + is_open BOOL, + is_local BOOL, + current_playing_user INT, + turn_order JSON DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (current_playing_user) REFERENCES users(id) ) `); await conn.query(` - CREATE TABLE IF NOT EXISTS turns ( + CREATE TABLE game_players ( id INT PRIMARY KEY AUTO_INCREMENT, - game INT NOT NULL, - user INT NOT NULL, - round_number INT NOT NULL, + game INT, + user INT, + is_creator BOOL, + FOREIGN KEY (game) REFERENCES current_games(id), + FOREIGN KEY (user) REFERENCES users(id) + ) + `) + + await conn.query(` + CREATE TABLE turns ( + id INT PRIMARY KEY AUTO_INCREMENT, + game INT, + user INT, + round_number INT, start_points INT, first_throw INT, second_throw INT, third_throw INT, end_points INT, - FOREIGN KEY (game) REFERENCES games(id) ON DELETE CASCADE, - FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE + FOREIGN KEY (game) REFERENCES current_games(id), + FOREIGN KEY (user) REFERENCES users(id) ) - `); + `) } } ]; diff --git a/backend/src/index.js b/backend/src/index.js index e44aebe..86af407 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,4 +1,6 @@ import fs from 'fs'; +import swaggerJsdoc from "swagger-jsdoc"; +import swaggerUi from "swagger-ui-express"; import https from 'https'; import express from 'express'; import cors from 'cors'; @@ -10,6 +12,16 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.join(path.dirname(__filename), ".."); // going back a dir cuz code is in src/ const app = express(); +const specs = swaggerJsdoc({ + definition: { + openapi: "3.0.0", + info: { + title: "Analogue Game Assistent API", + version: "1.0.0", + }, + }, + apis: ["./src/api.js"], +}); initDB(); @@ -19,13 +31,12 @@ app.use(express.urlencoded({ extended: true })) app.use(express.static(path.join(__dirname, "public"), { dotfiles: "ignore" })); +app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(specs)); app.use("/api", apiRouter); -const options = { +const server = https.createServer({ key: fs.readFileSync('server.key'), cert: fs.readFileSync('server.cert') -}; - -const server = https.createServer(options, app).listen(5555, () => { +}, app).listen(5555, () => { console.log(`Server running on http://localhost:${server.address().port}`); }); \ No newline at end of file