This commit is contained in:
2025-09-11 17:05:56 +02:00
parent 5b1a528748
commit 7b604df87c
3 changed files with 235 additions and 101 deletions

View File

@@ -3,115 +3,234 @@ import { pool } from './db.js';
const router = Router(); const router = Router();
/**
* @swagger
* /healthcheck:
* get:
* summary: Check if the API is running
* responses:
* 200:
* description: API is healthy
*/
router.get("/healthcheck", (req, res) => { router.get("/healthcheck", (req, res) => {
res.json({ status: "ok" }) 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) => { router.post("/users", async (req, res) => {
const { username } = req.body; const { username, input_method = 0 } = req.body;
const [result] = await pool.query(`INSERT INTO users (username) VALUES (?)`, [username]);
res.status(201).json({ id: result.insertId, username });
});
router.put("/users/:id", async (req, res) => { const [result] = await pool.query(
const { username } = req.body; "INSERT INTO users (username, input_method) VALUES (?, ?)",
await pool.query(`UPDATE users SET username = ? WHERE id = ?`, [username, req.params.id]); [username, input_method]
res.json({ id: req.params.id, username }); );
});
router.delete("/users/:id", async (req, res) => { const [userRow] = await pool.query("SELECT * FROM users WHERE id = ?", [result.insertId]);
await pool.query(`DELETE FROM users WHERE id = ?`, [req.params.id]); res.status(201).json(userRow[0]);
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 });
}); });
router.get("/games", async (req, res) => { 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); res.json(rows);
}); });
router.get("/games/:id", async (req, res) => { 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); res.json(rows[0] || null);
}); });
router.post("/games", async (req, res) => { router.post("/games", async (req, res) => {
const [result] = await pool.query(`INSERT INTO games (created_at) VALUES (NOW())`); const { user, is_local = false } = req.body;
res.status(201).json({ id: result.insertId, created_at: new Date() }); const conn = await pool.getConnection();
});
router.put("/games/:id", async (req, res) => { try {
const { winner } = req.body; await conn.beginTransaction();
await pool.query(`UPDATE games SET winner = ? WHERE id = ?`, [winner, req.params.id]);
res.json({ id: req.params.id, winner });
});
router.delete("/games/:id", async (req, res) => { const [gameResult] = await conn.query(
await pool.query(`DELETE FROM games WHERE id = ?`, [req.params.id]); "INSERT INTO current_games (is_open, is_local, current_playing_user) VALUES (?, ?, ?)",
res.status(204).send(); [true, is_local, user]
}); );
router.get("/games/:id/players", async (req, res) => { const gameId = gameResult.insertId;
const [rows] = await pool.query(`
SELECT u.id, u.username await conn.query("INSERT INTO game_players (game, user, is_creator) VALUES (?, ?)", [gameId, user, true]);
FROM game_players gp
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 JOIN users u ON gp.user = u.id
WHERE gp.game = ? WHERE gp.game = ?`,
`, [req.params.id]); [gameId]
res.json(rows); );
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) => { router.post("/games/:id/players", async (req, res) => {
const { user } = req.body; const { user } = req.body;
await pool.query(`INSERT IGNORE INTO game_players (game, user) VALUES (?, ?)`, [req.params.id, user]); const gameId = req.params.id;
res.status(201).json({ game: req.params.id, user });
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) => { router.patch("/games/:id/lock", async (req, res) => {
await pool.query(`DELETE FROM game_players WHERE game = ? AND user = ?`, [req.params.id, req.params.user]); const { user } = req.body;
res.status(204).send(); 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) => { 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); res.json(rows);
}); });
router.post("/games/:id/turns", async (req, res) => { 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 { user, round_number, start_points, first_throw, second_throw, third_throw, end_points } = req.body;
const [result] = await pool.query(` const gameId = req.params.id;
INSERT INTO turns (game, user, round_number, start_points, first_throw, second_throw, third_throw, end_points) const conn = await pool.getConnection();
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, [req.params.id, user, round_number, start_points, first_throw, second_throw, third_throw, end_points]); try {
res.status(201).json({ id: result.insertId }); 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) => { 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); res.json(rows[0] || null);
}); });

View File

@@ -21,56 +21,60 @@ const migrations = [
name: "init", name: "init",
up: async (conn) => { up: async (conn) => {
await conn.query(` await conn.query(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT, id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255) NOT NULL username VARCHAR(32),
input_method INT
) )
`); `);
await conn.query(` await conn.query(`
CREATE TABLE IF NOT EXISTS user_settings ( CREATE TABLE games (
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 (
id INT PRIMARY KEY AUTO_INCREMENT, id INT PRIMARY KEY AUTO_INCREMENT,
winner INT, winner INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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(` await conn.query(`
CREATE TABLE IF NOT EXISTS game_players ( CREATE TABLE current_games (
id INT PRIMARY KEY AUTO_INCREMENT, id INT PRIMARY KEY AUTO_INCREMENT,
game INT NOT NULL, is_open BOOL,
user INT NOT NULL, is_local BOOL,
FOREIGN KEY (game) REFERENCES games(id) ON DELETE CASCADE, current_playing_user INT,
FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE, turn_order JSON DEFAULT NULL,
UNIQUE (game, user) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (current_playing_user) REFERENCES users(id)
) )
`); `);
await conn.query(` await conn.query(`
CREATE TABLE IF NOT EXISTS turns ( CREATE TABLE game_players (
id INT PRIMARY KEY AUTO_INCREMENT, id INT PRIMARY KEY AUTO_INCREMENT,
game INT NOT NULL, game INT,
user INT NOT NULL, user INT,
round_number INT NOT NULL, 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, start_points INT,
first_throw INT, first_throw INT,
second_throw INT, second_throw INT,
third_throw INT, third_throw INT,
end_points INT, end_points INT,
FOREIGN KEY (game) REFERENCES games(id) ON DELETE CASCADE, FOREIGN KEY (game) REFERENCES current_games(id),
FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY (user) REFERENCES users(id)
) )
`); `)
} }
} }
]; ];

View File

@@ -1,4 +1,6 @@
import fs from 'fs'; import fs from 'fs';
import swaggerJsdoc from "swagger-jsdoc";
import swaggerUi from "swagger-ui-express";
import https from 'https'; import https from 'https';
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
@@ -10,6 +12,16 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.join(path.dirname(__filename), ".."); // going back a dir cuz code is in src/ const __dirname = path.join(path.dirname(__filename), ".."); // going back a dir cuz code is in src/
const app = express(); 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(); initDB();
@@ -19,13 +31,12 @@ app.use(express.urlencoded({ extended: true }))
app.use(express.static(path.join(__dirname, "public"), { app.use(express.static(path.join(__dirname, "public"), {
dotfiles: "ignore" dotfiles: "ignore"
})); }));
app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(specs));
app.use("/api", apiRouter); app.use("/api", apiRouter);
const options = { const server = https.createServer({
key: fs.readFileSync('server.key'), key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert') cert: fs.readFileSync('server.cert')
}; }, app).listen(5555, () => {
const server = https.createServer(options, app).listen(5555, () => {
console.log(`Server running on http://localhost:${server.address().port}`); console.log(`Server running on http://localhost:${server.address().port}`);
}); });