refacto: Keeping entrypoints clean and making files by purpose
This commit is contained in:
83
server/routes/auth.js
Normal file
83
server/routes/auth.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import express from "express";
|
||||
import bcrypt from "bcryptjs";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { JWT_SECRET, authMiddleware } from "../middleware/auth.js";
|
||||
import { createUser, getUserByUsername, getUserById } from "../db/usersDb.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function issueToken(user) {
|
||||
return jwt.sign(
|
||||
{ userId: user.id, username: user.username, team: user.team, role: user.role },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
}
|
||||
|
||||
// POST /api/auth/register
|
||||
router.post("/register", async (req, res) => {
|
||||
const { username, email, password, team } = req.body ?? {};
|
||||
if (!username || !email || !password || !team) {
|
||||
return res.status(400).json({ error: "missing_fields" });
|
||||
}
|
||||
if (team !== "blue" && team !== "red") {
|
||||
return res.status(400).json({ error: "invalid_team" });
|
||||
}
|
||||
if (typeof username !== "string" || username.length < 2 || username.length > 32) {
|
||||
return res.status(400).json({ error: "invalid_username" });
|
||||
}
|
||||
if (typeof password !== "string" || password.length < 6) {
|
||||
return res.status(400).json({ error: "password_too_short" });
|
||||
}
|
||||
try {
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
const user = await createUser(username.trim(), email.trim().toLowerCase(), passwordHash, team);
|
||||
const token = issueToken(user);
|
||||
return res.status(201).json({
|
||||
token,
|
||||
user: { id: user.id, username: user.username, team: user.team, role: user.role },
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.code === "23505") {
|
||||
if (e.constraint?.includes("email")) return res.status(409).json({ error: "email_taken" });
|
||||
return res.status(409).json({ error: "username_taken" });
|
||||
}
|
||||
console.error(e);
|
||||
return res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/login
|
||||
router.post("/login", async (req, res) => {
|
||||
const { username, password } = req.body ?? {};
|
||||
if (!username || !password) return res.status(400).json({ error: "missing_fields" });
|
||||
try {
|
||||
const user = await getUserByUsername(username.trim());
|
||||
if (!user) return res.status(401).json({ error: "invalid_credentials" });
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!valid) return res.status(401).json({ error: "invalid_credentials" });
|
||||
const token = issueToken(user);
|
||||
return res.json({
|
||||
token,
|
||||
user: { id: user.id, username: user.username, team: user.team, role: user.role },
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/auth/me
|
||||
router.get("/me", authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const user = await getUserById(req.user.userId);
|
||||
if (!user) return res.status(404).json({ error: "user_not_found" });
|
||||
const token = issueToken(user);
|
||||
return res.json({ token, user });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
154
server/routes/game.js
Normal file
154
server/routes/game.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import express from "express";
|
||||
import { getConfig } from "../configLoader.js";
|
||||
import { computeWorldSeedState } from "../worldSeed.js";
|
||||
import {
|
||||
ensureSeedEpoch,
|
||||
getGridCells,
|
||||
insertCell,
|
||||
getExistingCell,
|
||||
getTeamCooldown,
|
||||
upsertTeamCooldown,
|
||||
getScores,
|
||||
} from "../db/gameDb.js";
|
||||
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET /api/config
|
||||
router.get("/config", async (req, res) => {
|
||||
try {
|
||||
const worldSeed = await ensureSeedEpoch();
|
||||
const cfg = getConfig();
|
||||
const rot = cfg.databaseWipeoutIntervalSeconds;
|
||||
const ws = computeWorldSeedState(rot);
|
||||
|
||||
let teamCooldownRemaining = 0;
|
||||
const team = typeof req.query.team === "string" ? req.query.team : undefined;
|
||||
if (team === "blue" || team === "red") {
|
||||
const row = await getTeamCooldown(worldSeed, team);
|
||||
if (row) {
|
||||
const secondsSince = (Date.now() - new Date(row.last_reveal).getTime()) / 1000;
|
||||
if (secondsSince < cfg.clickCooldownSeconds) {
|
||||
teamCooldownRemaining = Math.ceil(cfg.clickCooldownSeconds - secondsSince);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
clickCooldownSeconds: cfg.clickCooldownSeconds,
|
||||
databaseWipeoutIntervalSeconds: rot,
|
||||
debugModeForTeams: cfg.debugModeForTeams,
|
||||
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
|
||||
worldSeed: ws.worldSeed,
|
||||
seedPeriodEndsAtUtc: ws.seedPeriodEndsAtUtc,
|
||||
seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc,
|
||||
teamCooldownRemaining,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: "config_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/grid/:seed
|
||||
router.get("/grid/:seed", async (req, res) => {
|
||||
const seed = decodeURIComponent(req.params.seed || "");
|
||||
try {
|
||||
const worldSeed = await ensureSeedEpoch();
|
||||
if (seed !== worldSeed) {
|
||||
return res.status(410).json({
|
||||
error: "seed_expired",
|
||||
worldSeed,
|
||||
seedPeriodEndsAtUtc: computeWorldSeedState(
|
||||
getConfig().databaseWipeoutIntervalSeconds
|
||||
).seedPeriodEndsAtUtc,
|
||||
});
|
||||
}
|
||||
const rows = await getGridCells(seed);
|
||||
res.json({ seed, cells: rows });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/cell/reveal
|
||||
router.post("/cell/reveal", async (req, res) => {
|
||||
const seed = String(req.body?.seed ?? "");
|
||||
const team = String(req.body?.team ?? "");
|
||||
const x = Number(req.body?.x);
|
||||
const y = Number(req.body?.y);
|
||||
|
||||
if (!seed || !Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= 100 || y < 0 || y >= 100) {
|
||||
return res.status(400).json({ error: "invalid_body" });
|
||||
}
|
||||
if (team !== "blue" && team !== "red") {
|
||||
return res.status(400).json({ error: "invalid_team" });
|
||||
}
|
||||
|
||||
try {
|
||||
const worldSeed = await ensureSeedEpoch();
|
||||
if (seed !== worldSeed) {
|
||||
return res.status(410).json({ error: "seed_expired", worldSeed });
|
||||
}
|
||||
|
||||
const cfg = getConfig();
|
||||
if (cfg.clickCooldownSeconds > 0) {
|
||||
const cooldownRow = await getTeamCooldown(worldSeed, team);
|
||||
if (cooldownRow) {
|
||||
const secondsSince = (Date.now() - new Date(cooldownRow.last_reveal).getTime()) / 1000;
|
||||
if (secondsSince < cfg.clickCooldownSeconds) {
|
||||
return res.status(429).json({
|
||||
error: "cooldown_active",
|
||||
team,
|
||||
remainingSeconds: Math.ceil(cfg.clickCooldownSeconds - secondsSince),
|
||||
cooldownSeconds: cfg.clickCooldownSeconds,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cell = computeCell(seed, x, y);
|
||||
const planetJson = cell.planet ? JSON.stringify(cell.planet) : null;
|
||||
const inserted = await insertCell(seed, x, y, cell.exploitable, cell.hasPlanet, planetJson, team);
|
||||
|
||||
if (inserted) {
|
||||
await upsertTeamCooldown(worldSeed, team);
|
||||
return res.json(rowToCellPayload(inserted));
|
||||
}
|
||||
|
||||
const existing = await getExistingCell(seed, x, y);
|
||||
if (!existing) return res.status(500).json({ error: "insert_race" });
|
||||
|
||||
if (existing.discovered_by !== team) {
|
||||
return res.status(409).json({
|
||||
error: "taken_by_other_team",
|
||||
discoveredBy: existing.discovered_by,
|
||||
cell: rowToCellPayload(existing),
|
||||
});
|
||||
}
|
||||
return res.json(rowToCellPayload(existing));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/scores
|
||||
router.get("/scores", async (_req, res) => {
|
||||
try {
|
||||
const worldSeed = await ensureSeedEpoch();
|
||||
const rows = await getScores(worldSeed);
|
||||
const scores = { blue: 0, red: 0 };
|
||||
for (const row of rows) {
|
||||
if (row.discovered_by === "blue") scores.blue = Number(row.cnt);
|
||||
if (row.discovered_by === "red") scores.red = Number(row.cnt);
|
||||
}
|
||||
res.json(scores);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user