move to public repo

This commit is contained in:
Nathaniel Tampus
2023-01-03 15:47:30 +08:00
commit 17ce3cf4c4
58 changed files with 2430 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
import type { Request, Response } from "express";
import type { User } from "@types";
import xss from "xss";
export const getCurrentSession = async (req: Request, res: Response) => {
try {
if (req.session.user) {
res.status(200).json(req.session.user);
} else {
res.status(404).end();
}
} catch (err: unknown) {
console.log(err);
res.status(500).end();
}
};
export const guestSession = async (req: Request, res: Response) => {
try {
const name = xss(req.body.name);
if (!req.session.user || !req.session.user?.id) {
// create guest session
const user: User = {
id: req.session.id,
name
};
req.session.user = user;
} else if (typeof req.session.user.id === "string" && req.session.user.name !== name) {
// update guest name
req.session.user.name = name;
}
req.session.save(() => {
res.status(201).json(req.session.user);
});
} catch (err: unknown) {
console.log(err);
res.status(500).end();
}
};
export const logoutSession = async (req: Request, res: Response) => {
try {
req.session.destroy(() => {
res.status(204).end();
});
} catch (err: unknown) {
console.log(err);
res.status(500).end();
}
};

View File

@@ -0,0 +1,74 @@
import type { Request, Response } from "express";
import { activeGames } from "../db/models/game.model";
import type { Game, User } from "@types";
import { nanoid } from "nanoid";
export const getActiveGames = async (req: Request, res: Response) => {
try {
//if (!req.query || !req.query.code) {
res.status(200).json(activeGames);
//}
/*
// todo: if code query is URL, convert to code (or do it on client side?)
const code =
(req.query.code as string).startsWith("http") ||
(req.query.code as string).startsWith("ches.su")
? path.posix.basename(url.parse(req.query.code as string).pathname as string)
: req.query.code;
console.log(code);
const game = activeGames.find((g) => g.code === code);
if (!game) {
res.status(404).end();
} else {
res.status(200).json(game);
}*/
} catch (err: unknown) {
console.log(err);
res.status(500).end();
}
};
export const createGame = async (req: Request, res: Response) => {
try {
if (!req.session.user) {
console.log("unauthorized createGame:");
console.log(req.session);
res.status(401).end();
return;
}
const user: User = req.session.user;
const game: Game = {
code: nanoid(6),
open: true,
host: user
};
if (req.body.side === "white") {
game.white = user;
} else if (req.body.side === "black") {
game.black = user;
} else {
// random
if (Math.floor(Math.random() * 2) === 0) {
game.white = user;
} else {
game.black = user;
}
}
activeGames.push(game);
res.status(201).json({ code: game.code });
} catch (err: unknown) {
console.log(err);
res.status(500).end();
}
};
// use sockets for joining games
/*
export const joinGame = async (req: Request, res: Response) => {
console.log("joining game!");
};
*/

3
server/src/db/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import { Pool } from "pg";
export const db = new Pool();

16
server/src/db/init.sql Normal file
View File

@@ -0,0 +1,16 @@
-- users
CREATE TABLE "user" (
id SERIAL PRIMARY KEY,
name VARCHAR(128) UNIQUE NOT NULL,
email VARCHAR(128),
password TEXT
);
-- games
CREATE TABLE "game" (
id SERIAL PRIMARY KEY,
pgn TEXT,
white_id INT REFERENCES "user",
black_id INT REFERENCES "user",
winner CHAR(5)
);

View File

@@ -0,0 +1,89 @@
import { db } from "..";
import { Game } from "@types";
export const activeGames: Array<Game> = [];
// todo: join user and game relationship
const create = async (game: Game) => {
try {
const res = await db.query(
`INSERT INTO "game"(pgn, white_id, black_id, winner) VALUES($1, $2, $3, $4) RETURNING *`,
[game.pgn || null, game.white?.id || null, game.black?.id || null, game.winner || null]
);
return {
id: res.rows[0].id,
pgn: res.rows[0].pgn,
white: { id: res.rows[0].white_id },
black: { id: res.rows[0].black_id },
winner: res.rows[0].winner
} as Game;
} catch (err: unknown) {
console.log(err);
return null;
}
};
const find = async (where?: string, limit = 1) => {
const query = where
? `SELECT * FROM "game"`
: {
text: `SELECT * FROM "game" WHERE $1 LIMIT $2`,
values: [where, limit]
};
try {
const res = await db.query(query);
return res.rows.map((r) => {
return {
id: r.id,
pgn: r.pgn,
white: { id: r.white_id },
black: { id: r.black_id },
winner: r.winner
} as Game;
});
} catch (err: unknown) {
console.log(err);
return null;
}
};
const update = async (id: number, data: string) => {
try {
const res = await db.query(`UPDATE "game" SET $1 WHERE id = $2 RETURNING *`, [data, id]);
return {
id: res.rows[0].id,
pgn: res.rows[0].pgn,
white: { id: res.rows[0].white_id },
black: { id: res.rows[0].black_id },
winner: res.rows[0].winner
} as Game;
} catch (err: unknown) {
console.log(err);
return null;
}
};
const remove = async (id: number) => {
try {
const res = await db.query(`DELETE FROM "game" WHERE id = $1 RETURNING *`, [id]);
return {
id: res.rows[0].id,
pgn: res.rows[0].pgn,
white: { id: res.rows[0].white_id },
black: { id: res.rows[0].black_id },
winner: res.rows[0].winner
} as Game;
} catch (err: unknown) {
console.log(err);
return null;
}
};
export const GameModel = {
create,
find,
update,
remove
};

View File

@@ -0,0 +1,87 @@
import { db } from "..";
import type { User } from "@types";
const create = async (user: User, password: string) => {
if (user.name === "Guest" || user.email === undefined) {
return null;
}
try {
const res = await db.query(
`INSERT INTO "user"(name, email, password) VALUES($1, $2, $3) RETURNING id, name, email`,
[user.name, user.email ?? null, password]
);
return res.rows[0] as User;
} catch (err: unknown) {
console.log(err);
return null;
}
};
const find = async (where?: string, limit = 1) => {
// if user is not specified, get all users
if (!where) {
try {
const res = await db.query(`SELECT id, name, email FROM "user"`);
return res.rows as Array<User>;
} catch (err: unknown) {
console.log(err);
return null;
}
}
try {
/* const res = await db.query(
`SELECT id, name, email FROM "user" WHERE ${typeof user.id === "number" ? "id" : typeof user.name === "string" ? "name" : "email" } = $1 LIMIT $2`,
[typeof user.id === "number" ? user.id : typeof user.name === "string" ? user.name : user.email, limit]
); */
const res = await db.query(`SELECT id, name, email FROM "user" WHERE $1 LIMIT $2`, [
where,
limit
]);
return res.rows as Array<User>;
} catch (err: unknown) {
console.log(err);
return null;
}
};
const update = async (id: number, data: string) => {
if (typeof id === "string" || id === 0) {
return null;
}
try {
const res = await db.query(`UPDATE "user" SET $1 WHERE id = $2 RETURNING id, name, email`, [
data,
id
]);
return res.rows[0] as User;
} catch (err: unknown) {
console.log(err);
return null;
}
};
const remove = async (id: number) => {
if (typeof id === "string" || id === 0) {
return null;
}
try {
const res = await db.query(`DELETE FROM "user" WHERE id = $1 RETURNING id, name, email`, [
id
]);
return res.rows[0] as User;
} catch (err: unknown) {
console.log(err);
return null;
}
};
export const UserModel = {
create,
find,
update,
remove
};

View File

View File

@@ -0,0 +1,42 @@
import { nanoid } from "nanoid";
import session, { Session } from "express-session";
import PGSimple from "connect-pg-simple";
import { db } from "../db";
const PGSession = PGSimple(session);
import type { User } from "@types";
declare module "express-session" {
interface SessionData {
user: User;
}
}
declare module "http" {
interface IncomingMessage {
session: Session & {
user: User;
};
}
}
const sessionMiddleware = session({
store: new PGSession({
pool: db,
createTableIfMissing: true
}),
secret: process.env.SESSION_SECRET || "whatever this is",
resave: false,
saveUninitialized: false,
name: "chessu",
proxy: true,
cookie: {
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
secure: true,
httpOnly: true,
sameSite: "none"
},
genid: function () {
return nanoid(21);
}
});
export default sessionMiddleware;

View File

@@ -0,0 +1,16 @@
import { Router } from "express";
const router = Router();
import * as controller from "../controllers/auth.controller";
router.route("/").get(controller.getCurrentSession);
// create or update guest sessions
router.route("/guest").post(controller.guestSession);
router.route("/logout").post(controller.logoutSession);
//router.route("/register").post(controller.registerUser);
//router.route("/login").post(controller.loginUser);
export default router;

View File

@@ -0,0 +1,12 @@
import { Router } from "express";
const router = Router();
import * as controller from "../controllers/games.controller";
router.route("/").get(controller.getActiveGames).post(controller.createGame);
//router.route("/:id").put(controller.joinGame);
// todo: api for updating games/moves requiring authentication
export default router;

View File

@@ -0,0 +1,10 @@
import { Router } from "express";
const router = Router();
import games from "./games.route";
import auth from "./auth.route";
router.use("/games", games);
router.use("/auth", auth);
export default router;

46
server/src/server.ts Normal file
View File

@@ -0,0 +1,46 @@
import "dotenv/config";
import cors from "cors";
const corsConfig = { origin: "https://ches.su", credentials: true };
import express, { Request, Response, NextFunction } from "express";
import { createServer } from "http";
import session from "./middleware/session";
import { Server } from "socket.io";
import { init as initSocket } from "./socket";
import { db } from "./db";
import routes from "./routes";
const app = express();
const server = createServer(app);
// database
db.connect();
// middleware
app.use(cors(corsConfig));
app.use(express.json());
app.set("trust proxy", 1);
app.use(session);
app.use("/v1", routes);
// socket.io
export const io = new Server(server, { cors: corsConfig });
io.use((socket, next) => {
session(socket.request as Request, {} as Response, next as NextFunction);
});
io.use((socket, next) => {
const session = socket.request.session;
if (session && session.user) {
next();
} else {
console.log("io.use: no session");
socket.disconnect();
}
});
initSocket();
const port = process.env.PORT || 5432;
server.listen(port, () => {
console.log(`listening on :${port}`);
});

View File

@@ -0,0 +1,164 @@
import { activeGames } from "../db/models/game.model";
import type { Socket } from "socket.io";
import { Chess } from "chess.js";
import { io } from "../server";
export async function joinLobby(this: Socket, gameCode: string) {
const game = activeGames.find((g) => g.code === gameCode);
if (!game) {
console.log(`joinLobby: Game code ${gameCode} not found.`);
return;
}
if (
!(game.white?.id === this.request.session.id || game.black?.id === this.request.session.id)
) {
if (game.observers === undefined) game.observers = [];
game.observers?.push(this.request.session.user);
}
if (this.rooms.size >= 2) {
await leaveLobby.call(this);
}
await this.join(gameCode);
this.emit("receivedLatestGame", game);
io.to(game.code as string).emit("receivedLatestLobby", game);
io.to(game.code as string).emit("userJoined", this.request.session.user.name);
}
export async function leaveLobby(this: Socket, code?: string) {
if (this.rooms.size === 2) {
const game = activeGames.find((g) => g.code === (code || Array.from(this.rooms)[1]));
if (game) {
const user = game.observers?.find((o) => o.id === this.request.session.id);
let name = "";
if (user) {
name = user.name as string;
game.observers?.splice(game.observers?.indexOf(user), 1);
}
if (game.black?.id === this.request.session.id) {
name = game.black?.name as string;
game.black = undefined;
}
if (game.white?.id === this.request.session.id) {
name = game.white?.name as string;
game.white = undefined;
}
if (!game.white && !game.black && !game.observers) {
// TODO
//activeGames.splice(activeGames.indexOf(game), 1); // remove game if empty
} else {
this.to(game.code as string).emit("userLeft", name);
this.to(game.code as string).emit("receivedLatestLobby", game);
}
}
await this.leave(code || Array.from(this.rooms)[1]);
} else if (this.rooms.size >= 3) {
console.log(`[WARNING] leaveLobby: room size is ${this.rooms.size}, aborting...`);
} else {
// try to find a game with this user
const game = activeGames.find(
(g) =>
g.code === code ||
g.black?.id === this.request.session.id ||
g.white?.id === this.request.session.id ||
g.observers?.find((o) => this.request.session.id === o.id)
);
if (game) {
const user = game.observers?.find((o) => this.request.session.id === o.id);
let name = "";
if (user) {
name = user.name as string;
game.observers?.splice(game.observers?.indexOf(user), 1);
}
if (game.black?.id === this.request.session.id) {
name = game.black?.name as string;
game.black = undefined;
}
if (game.white?.id === this.request.session.id) {
name = game.white?.name as string;
game.white = undefined;
}
this.to(game.code as string).emit("userLeft", name);
this.to(game.code as string).emit("receivedLatestLobby", game);
}
}
}
export async function getLatestGame(this: Socket) {
const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]);
if (game) this.emit("receivedLatestGame", game);
}
export async function sendMove(this: Socket, m: { from: string; to: string; promotion?: string }) {
const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]);
if (!game) return;
const chess = new Chess();
if (game.pgn) {
chess.loadPgn(game.pgn);
}
const prevTurn = chess.turn();
const newMove = chess.move(m);
if (chess.isGameOver()) {
let reason = "";
if (chess.isCheckmate()) reason = "checkmate";
else if (chess.isStalemate()) reason = "stalemate";
else if (chess.isThreefoldRepetition()) reason = "repetition";
else if (chess.isInsufficientMaterial()) reason = "insufficient";
else if (chess.isDraw()) reason = "draw";
const winnerSide =
reason === "checkmate" ? (prevTurn === "w" ? "white" : "black") : undefined;
const winnerName =
reason === "checkmate"
? winnerSide === "white"
? game.white?.name
: game.black?.name
: undefined;
if (reason === "checkmate") {
game.winner = winnerSide;
} else {
game.winner = "draw";
}
io.to(game.code as string).emit("gameOver", { reason, winnerName, winnerSide });
}
if (newMove === null) {
this.emit("receivedLatestGame", game);
} else {
game.pgn = chess.pgn();
this.to(game.code as string).emit("receivedMove", { from: m.from, to: m.to });
}
}
export async function joinAsPlayer(this: Socket) {
const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]);
if (!game) return;
const user = game.observers?.find((o) => o.id === this.request.session.id);
if (!game.white) {
game.white = this.request.session.user;
if (user) game.observers?.splice(game.observers?.indexOf(user), 1);
io.to(game.code as string).emit("userJoinedAsPlayer", {
name: this.request.session.user.name,
side: "white"
});
} else if (!game.black) {
game.black = this.request.session.user;
if (user) game.observers?.splice(game.observers?.indexOf(user), 1);
io.to(game.code as string).emit("userJoinedAsPlayer", {
name: this.request.session.user.name,
side: "black"
});
} else {
console.log("[WARNING] attempted to join a game with already 2 players");
}
io.to(game.code as string).emit("receivedLatestGame", game);
io.to(game.code as string).emit("receivedLatestLobby", game);
}
export async function chat(this: Socket, message: string) {
this.to(Array.from(this.rooms)[1]).emit("chat", {
author: this.request.session.user,
message
});
}

View File

@@ -0,0 +1,34 @@
import type { Socket } from "socket.io";
import { io } from "../server";
import { joinLobby, leaveLobby, getLatestGame, sendMove, joinAsPlayer, chat } from "./game.socket";
const socketConnect = (socket: Socket) => {
const req = socket.request;
// re-analyze if this is necessary, or if io.use will handle logout
socket.use((__, next) => {
req.session.reload((err) => {
if (err) {
console.log("reload: disconnecting socket.");
console.log(err);
socket.disconnect();
} else {
next();
}
});
});
socket.on("disconnect", leaveLobby);
socket.on("joinLobby", joinLobby);
socket.on("leaveLobby", leaveLobby);
socket.on("getLatestGame", getLatestGame);
socket.on("sendMove", sendMove);
socket.on("joinAsPlayer", joinAsPlayer);
socket.on("chat", chat);
};
export const init = () => {
io.on("connection", socketConnect);
};