From 96a262ec29edfda691c1b80d2d0f539d8d88787d Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 19 Mar 2023 20:43:00 +0800 Subject: [PATCH 01/68] minor cleanup --- server/src/db/models/game.model.ts | 15 ++++----------- server/src/socket/index.ts | 2 +- types/index.d.ts | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/server/src/db/models/game.model.ts b/server/src/db/models/game.model.ts index 1aabe77..d31d90b 100644 --- a/server/src/db/models/game.model.ts +++ b/server/src/db/models/game.model.ts @@ -5,7 +5,7 @@ export const activeGames: Array = []; // todo: join user and game relationship -const create = async (game: Game) => { +export 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 *`, @@ -24,7 +24,7 @@ const create = async (game: Game) => { } }; -const find = async (where?: string, limit = 1) => { +export const find = async (where?: string, limit = 1) => { const query = where ? `SELECT * FROM "game"` : { @@ -49,7 +49,7 @@ const find = async (where?: string, limit = 1) => { } }; -const update = async (id: number, data: string) => { +export const update = async (id: number, data: string) => { try { const res = await db.query(`UPDATE "game" SET $1 WHERE id = $2 RETURNING *`, [data, id]); return { @@ -65,7 +65,7 @@ const update = async (id: number, data: string) => { } }; -const remove = async (id: number) => { +export const remove = async (id: number) => { try { const res = await db.query(`DELETE FROM "game" WHERE id = $1 RETURNING *`, [id]); return { @@ -80,10 +80,3 @@ const remove = async (id: number) => { return null; } }; - -export const GameModel = { - create, - find, - update, - remove -}; diff --git a/server/src/socket/index.ts b/server/src/socket/index.ts index 81ef55e..7c0c7ca 100644 --- a/server/src/socket/index.ts +++ b/server/src/socket/index.ts @@ -12,7 +12,7 @@ import { const socketConnect = (socket: Socket) => { const req = socket.request; - // re-analyze if this is necessary, or if io.use will handle logout + // review if this is necessary, or if io.use will handle logout socket.use((__, next) => { req.session.reload((err) => { if (err) { diff --git a/types/index.d.ts b/types/index.d.ts index e2452ec..25dd30c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -13,7 +13,7 @@ export interface Game { export interface User { id?: number | string; // string for guest IDs - name?: string; + name?: string | null; email?: string; connected?: boolean; // mainly for players, not spectators } From 24d3c7fcbd87e2aaf15c1c5b3c3582ace0263629 Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 19 Mar 2023 20:43:16 +0800 Subject: [PATCH 02/68] update readme --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5761a2e..6a46a80 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# chessu [![Deployment status](https://img.shields.io/github/deployments/nizewn/chessu/Production?label=deployment)](https://ches.su) [![MIT License](https://img.shields.io/github/license/nizewn/chessu?color=blue)](https://github.com/nizewn/chessu/blob/main/LICENSE) +# chessu [![Deployment status](https://img.shields.io/github/deployments/nizewn/chessu/Production?label=deployment)](https://ches.su) -> ❗ This project is still in the early stages of development and should be considered unstable. Expect bugs and weird behavior. +> ❗ This project is still in the early stages of development. Expect bugs and incomplete features. Yet another Chess web app. Live demo at [ches.su](https://ches.su). @@ -19,7 +19,9 @@ This project is structured as a monorepo using npm workspaces, separated into th - `server` - Node/Express.js application for the back-end, deployed to [server.ches.su](https://server.ches.su). - `types` - Shared type definitions for the client and server. -### Scripts +For separate deployments, you may exclude the `client` or `server` directory. However, you should include the `types` folder as it contains shared type definitions that are required by both packages. + +
Scripts

```sh # install all dependencies, including eslint and prettier for development @@ -27,8 +29,9 @@ npm install # concurrently run frontend and backend development servers npm run dev # -w client/server to run only one +``` - +```sh # for separate production deployments npm install -w client npm install -w server @@ -40,9 +43,9 @@ npm start -w client npm start -w server ``` -For separate deployments, you may exclude the `client` or `server` directory. However, you should include the `types` folder as it contains shared type definitions that are required by both packages. +

-### Environment variables +
Environment variables

You may create a `.env` file in each package directory to set their environment variables. @@ -65,3 +68,13 @@ PGUSER=exampleuser PGPASSWORD=examplepassword PGDATABASE=chessu ``` + +

+ +## Contributing + +Pull requests are welcome. For feature changes or suggestions, please open an issue first for discussion. + +## License + +[MIT](https://github.com/nizewn/chessu/blob/main/LICENSE) From a9630a4cf160b7bcfe95726ad13ab6aab5dd1d2a Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 19 Mar 2023 20:43:31 +0800 Subject: [PATCH 03/68] update deps, add argon2 --- client/package.json | 6 +++--- package.json | 2 +- server/package.json | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/package.json b/client/package.json index 65d1b62..4fc8a09 100644 --- a/client/package.json +++ b/client/package.json @@ -10,10 +10,10 @@ }, "dependencies": { "@tabler/icons-react": "^2.10.0", - "chess.js": "^1.0.0-beta.3", + "chess.js": "^1.0.0-beta.4", "next": "13.2.4", "react": "18.2.0", - "react-chessboard": "^2.1.1", + "react-chessboard": "^2.1.2", "react-dom": "18.2.0", "socket.io-client": "^4.6.1" }, @@ -26,7 +26,7 @@ "daisyui": "^2.51.4", "postcss": "^8.4.21", "tailwindcss": "^3.2.7", - "typescript": "4.9.5" + "typescript": "5.0.2" }, "optionalDependencies": { "bufferutil": "^4.0.7", diff --git a/package.json b/package.json index 1c0b9cc..7cd7109 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "eslint-config-next": "13.2.4", "eslint-config-prettier": "^8.7.0", "prettier": "^2.8.4", - "prettier-plugin-tailwindcss": "^0.2.4" + "prettier-plugin-tailwindcss": "^0.2.5" }, "engines": { "node": ">=18" diff --git a/server/package.json b/server/package.json index e762dc5..e6e3204 100644 --- a/server/package.json +++ b/server/package.json @@ -9,7 +9,8 @@ "dev": "node --loader ts-node/esm --watch src/server.ts" }, "dependencies": { - "chess.js": "^1.0.0-beta.3", + "argon2": "^0.30.3", + "chess.js": "^1.0.0-beta.4", "connect-pg-simple": "^8.0.0", "cors": "^2.8.5", "dotenv": "^16.0.3", @@ -28,7 +29,7 @@ "@types/node": "^18.15.3", "@types/pg": "^8.6.6", "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "typescript": "^5.0.2" }, "engines": { "node": ">=18" From 42dc24a9bc65bba76b201ff6d58f4ea6ecaf656f Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 19 Mar 2023 20:44:01 +0800 Subject: [PATCH 04/68] init db on server startup --- server/src/db/index.ts | 16 ++++++++++++++++ server/src/db/init.sql | 15 --------------- server/src/server.ts | 11 +++++++++-- 3 files changed, 25 insertions(+), 17 deletions(-) delete mode 100644 server/src/db/init.sql diff --git a/server/src/db/index.ts b/server/src/db/index.ts index 40f6898..c8262ef 100644 --- a/server/src/db/index.ts +++ b/server/src/db/index.ts @@ -1,3 +1,19 @@ import pg from "pg"; export const db = new pg.Pool(); + +export const INIT_TABLES = ` + CREATE TABLE IF NOT EXISTS "user" ( + id SERIAL PRIMARY KEY, + name VARCHAR(128) UNIQUE NOT NULL, + email VARCHAR(128), + password TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS "game" ( + id SERIAL PRIMARY KEY, + pgn TEXT, + white_id INT REFERENCES "user", + black_id INT REFERENCES "user" + ); +`; diff --git a/server/src/db/init.sql b/server/src/db/init.sql deleted file mode 100644 index a4c2c80..0000000 --- a/server/src/db/init.sql +++ /dev/null @@ -1,15 +0,0 @@ --- 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", -); \ No newline at end of file diff --git a/server/src/server.ts b/server/src/server.ts index bda46c0..2bc1380 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -6,7 +6,7 @@ import { createServer } from "http"; import session from "./middleware/session.js"; import { Server } from "socket.io"; import { init as initSocket } from "./socket/index.js"; -import { db } from "./db/index.js"; +import { db, INIT_TABLES } from "./db/index.js"; import routes from "./routes/index.js"; const corsConfig = { @@ -18,7 +18,14 @@ const app = express(); const server = createServer(app); // database -db.connect(); +await db.connect(); +db.query(INIT_TABLES, (err) => { + if (err) { + console.error(err); + } else { + console.log("Tables initialized"); + } +}); // middleware app.use(cors(corsConfig)); From 17ef05b31c6898a30e0089b7779dceb0cfe728ba Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 19 Mar 2023 20:51:29 +0800 Subject: [PATCH 05/68] sync user info in games, adjust game timeout --- server/src/controllers/games.controller.ts | 5 +-- server/src/socket/game.socket.ts | 41 ++++++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/server/src/controllers/games.controller.ts b/server/src/controllers/games.controller.ts index 8e10a7f..ed41a83 100644 --- a/server/src/controllers/games.controller.ts +++ b/server/src/controllers/games.controller.ts @@ -34,13 +34,14 @@ export const getActiveGame = async (req: Request, res: Response) => { export const createGame = async (req: Request, res: Response) => { try { - if (!req.session.user) { + if (!req.session.user?.id) { console.log("unauthorized createGame"); res.status(401).end(); return; } const user: User = { - ...req.session.user, + id: req.session.user.id, + name: req.session.user.name, connected: false }; const unlisted: boolean = req.body.unlisted ?? false; diff --git a/server/src/socket/game.socket.ts b/server/src/socket/game.socket.ts index 6ad6643..b3eed82 100644 --- a/server/src/socket/game.socket.ts +++ b/server/src/socket/game.socket.ts @@ -10,13 +10,29 @@ export async function joinLobby(this: Socket, gameCode: string) { return; } + if (game.host && game.host?.id === this.request.session.user.id) { + game.host.connected = true; + if (game.host.name !== this.request.session.user.name) { + game.host.name = this.request.session.user.name; + } + } if (game.white && game.white?.id === this.request.session.user.id) { game.white.connected = true; + if (game.white.name !== this.request.session.user.name) { + game.white.name = this.request.session.user.name; + } } else if (game.black && game.black?.id === this.request.session.user.id) { game.black.connected = true; + if (game.black.name !== this.request.session.user.name) { + game.black.name = this.request.session.user.name; + } } else { if (game.observers === undefined) game.observers = []; - game.observers?.push(this.request.session.user); + const user = { + id: this.request.session.user.id, + name: this.request.session.user.name + }; + game.observers?.push(user); } if (this.rooms.size >= 2) { @@ -61,10 +77,15 @@ export async function leaveLobby(this: Socket, reason?: DisconnectReason, code?: if (sockets.length <= 0 || (reason === undefined && sockets.length <= 1)) { if (game.timeout) clearTimeout(game.timeout); + + let timeout = 1000 * 60; // 1 minute + if (game.pgn) { + timeout *= 20; // 20 minutes if game has started + } game.timeout = Number( setTimeout(() => { activeGames.splice(activeGames.indexOf(game), 1); - }, 1000 * 60 * 10) // 10 minutes + }, timeout) ); } else { this.to(game.code as string).emit("receivedLatestGame", game); @@ -137,16 +158,24 @@ export async function joinAsPlayer(this: Socket) { if (!game) return; const user = game.observers?.find((o) => o.id === this.request.session.user.id); if (!game.white) { - game.white = this.request.session.user; - game.white.connected = true; + const sessionUser = { + id: this.request.session.user.id, + name: this.request.session.user.name, + connected: true + }; + game.white = sessionUser; 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; - game.black.connected = true; + const sessionUser = { + id: this.request.session.user.id, + name: this.request.session.user.name, + connected: true + }; + game.black = sessionUser; if (user) game.observers?.splice(game.observers?.indexOf(user), 1); io.to(game.code as string).emit("userJoinedAsPlayer", { name: this.request.session.user.name, From 76b0d142cb32a8858524a1e7014a3a6eebfaeb39 Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 19 Mar 2023 20:52:04 +0800 Subject: [PATCH 06/68] update user db queries --- server/src/db/models/user.model.ts | 51 ++++++++++++++---------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/server/src/db/models/user.model.ts b/server/src/db/models/user.model.ts index a7ec173..0245eee 100644 --- a/server/src/db/models/user.model.ts +++ b/server/src/db/models/user.model.ts @@ -1,7 +1,7 @@ import { db } from "../index.js"; import type { User } from "@chessu/types"; -const create = async (user: User, password: string) => { +export const create = async (user: User, password: string) => { if (user.name === "Guest" || user.email === undefined) { return null; } @@ -9,7 +9,7 @@ const create = async (user: User, password: string) => { 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] + [user.name, user.email || null, password] ); return res.rows[0] as User; } catch (err: unknown) { @@ -18,12 +18,14 @@ const create = async (user: User, password: string) => { } }; -const find = async (where?: string, limit = 1) => { +export const find = async (user: User, limit?: number) => { // if user is not specified, get all users - if (!where) { + if (!user) { try { - const res = await db.query(`SELECT id, name, email FROM "user"`); - return res.rows as Array; + const res = await db.query(`SELECT id, name, email FROM "user" LIMIT $1`, [ + limit ?? 10 + ]); + return res.rows as Array; } catch (err: unknown) { console.log(err); return null; @@ -31,31 +33,31 @@ const find = async (where?: string, limit = 1) => { } 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; + const res = await db.query( + `SELECT id, name, email, password FROM "user" WHERE name=$1 OR email=$2 LIMIT $3`, + [user.name, user.email, limit ?? 1] + ); + return res.rows as Array; } catch (err: unknown) { console.log(err); return null; } }; -const update = async (id: number, data: string) => { +export const update = async (id: number, updatedUser: User & { password?: 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 - ]); + let query = `UPDATE "user" SET name=$1, email=$2 WHERE id=$3 RETURNING id, name, email`; + let values = [updatedUser.name, updatedUser.email, id]; + + if (updatedUser.password) { + query = `UPDATE "user" SET name=$1, email=$2, password=$3 WHERE id=$4 RETURNING id, name, email`; + values = [updatedUser.name, updatedUser.email, updatedUser.password, id]; + } + const res = await db.query(query, values); return res.rows[0] as User; } catch (err: unknown) { console.log(err); @@ -63,7 +65,7 @@ const update = async (id: number, data: string) => { } }; -const remove = async (id: number) => { +export const remove = async (id: number) => { if (typeof id === "string" || id === 0) { return null; } @@ -78,10 +80,3 @@ const remove = async (id: number) => { return null; } }; - -export const UserModel = { - create, - find, - update, - remove -}; From 1fab992f534e50fbcfee461e98740e9bae8edbab Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 19 Mar 2023 20:52:58 +0800 Subject: [PATCH 07/68] add auth controllers & validation improvements --- server/src/controllers/auth.controller.ts | 215 +++++++++++++++++++++- 1 file changed, 214 insertions(+), 1 deletion(-) diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 2e2b067..4029a4b 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -1,6 +1,11 @@ import type { Request, Response } from "express"; import type { User } from "@chessu/types"; import xss from "xss"; +import { hash, verify } from "argon2"; + +import { activeGames } from "../db/models/game.model.js"; +import { io } from "../server.js"; +import * as UserModel from "../db/models/user.model.js"; export const getCurrentSession = async (req: Request, res: Response) => { try { @@ -17,9 +22,13 @@ export const getCurrentSession = async (req: Request, res: Response) => { export const guestSession = async (req: Request, res: Response) => { try { + if (req.session.user?.id && typeof req.session.user.id === "number") { + res.status(403).end(); + return; + } const name = xss(req.body.name); - const pattern = /^[A-Za-z0-9_]+$/; + const pattern = /^[A-Za-z0-9]+$/; if (!pattern.test(name)) { res.status(400).end(); @@ -36,6 +45,29 @@ export const guestSession = async (req: Request, res: Response) => { } else if (typeof req.session.user.id === "string" && req.session.user.name !== name) { // update guest name req.session.user.name = name; + + const game = activeGames.find( + (g) => + g.white?.id === req.session.user.id || + g.black?.id === req.session.user.id || + g.observers?.find((o) => o.id === req.session.user.id) + ); + if (game) { + if (game.host?.id === req.session.user.id) { + game.host.name = name; + } + if (game.white?.id === req.session.user.id) { + game.white.name = name; + } else if (game.black?.id === req.session.user.id) { + game.black.name = name; + } else { + const observer = game.observers?.find((o) => o.id === req.session.user.id); + if (observer) { + observer.name = name; + } + } + io.to(game.code as string).emit("receivedLatestGame", game); + } } req.session.save(() => { res.status(201).json(req.session.user); @@ -56,3 +88,184 @@ export const logoutSession = async (req: Request, res: Response) => { res.status(500).end(); } }; + +export const registerUser = async (req: Request, res: Response) => { + try { + if (req.session.user?.id && typeof req.session.user.id === "number") { + res.status(403).end(); + return; + } + + const name = xss(req.body.name); + const email = xss(req.body.email); + const password = await hash(req.body.password); + + const pattern = /^[A-Za-z0-9]+$/; + + if (!pattern.test(name)) { + res.status(400).end(); + return; + } + + const duplicateUsers = await UserModel.find({ name, email }); + if (duplicateUsers && duplicateUsers.length) { + const dupl = duplicateUsers[0].name === name ? "Username" : "Email"; + res.status(409).json({ message: `${dupl} is already in use.` }); + return; + } + + const newUser = await UserModel.create({ name, email }, password); + if (!newUser) { + throw new Error("Failed to create user"); + } + + if (req.session.user?.id && typeof req.session.user.id === "string") { + const game = activeGames.find( + (g) => + g.white?.id === req.session.user.id || + g.black?.id === req.session.user.id || + g.observers?.find((o) => o.id === req.session.user.id) + ); + if (game) { + if (game.host?.id === req.session.user.id) { + game.host = newUser; + } + if (game.white && game.white?.id === req.session.user.id) { + game.white = newUser; + } else if (game.black && game.black?.id === req.session.user.id) { + game.black = newUser; + } else { + const observer = game.observers?.find((o) => o.id === req.session.user.id); + if (observer) { + observer.id = newUser.id; + observer.name = newUser.name; + } + } + io.to(game.code as string).emit("receivedLatestGame", game); + } + } + + req.session.user = newUser; + req.session.save(() => { + res.status(201).json(req.session.user); + }); + } catch (err: unknown) { + console.log(err); + res.status(500).end(); + } +}; + +export const loginUser = async (req: Request, res: Response) => { + try { + if (req.session.user?.id && typeof req.session.user.id === "number") { + res.status(403).end(); + return; + } + + const nameOrEmail = xss(req.body.name); + const password = req.body.password; + + const users = await UserModel.find({ name: nameOrEmail, email: nameOrEmail }); + if (!users || !users.length) { + res.status(404).json({ message: "Invalid username/email." }); + return; + } + + const validPassword = await verify(users[0].password as string, password); + if (!validPassword) { + res.status(401).json({ message: "Invalid password." }); + return; + } + + const newUser = { + id: users[0].id, + name: users[0].name, + email: users[0].email + }; + + if (req.session.user?.id && typeof req.session.user.id === "string") { + const game = activeGames.find( + (g) => + g.white?.id === req.session.user.id || + g.black?.id === req.session.user.id || + g.observers?.find((o) => o.id === req.session.user.id) + ); + if (game) { + if (game.host?.id === req.session.user.id) { + game.host = newUser; + } + if (game.white && game.white?.id === req.session.user.id) { + game.white = newUser; + } else if (game.black && game.black?.id === req.session.user.id) { + game.black = newUser; + } else { + const observer = game.observers?.find((o) => o.id === req.session.user.id); + if (observer) { + observer.id = newUser.id; + observer.name = newUser.name; + } + } + io.to(game.code as string).emit("receivedLatestGame", game); + } + } + + req.session.user = { + id: users[0].id, + name: users[0].name + }; + req.session.save(() => { + res.status(200).json(req.session.user); + }); + } catch (err: unknown) { + console.log(err); + res.status(500).end(); + } +}; + +export const updateUser = async (req: Request, res: Response) => { + try { + if (!req.session.user?.id || typeof req.session.user.id === "string") { + res.status(403).end(); + return; + } + + const name = xss(req.body.name || req.session.user.name); + const pattern = /^[A-Za-z0-9]+$/; + if (!pattern.test(name)) { + res.status(400).end(); + return; + } + + const email = xss(req.body.email || req.session.user.email); + + const duplicateUsers = await UserModel.find({ name, email }); + if ( + duplicateUsers && + duplicateUsers.length && + duplicateUsers[0].id !== req.session.user.id + ) { + const dupl = duplicateUsers[0].name === name ? "Username" : "Email"; + res.status(409).json({ message: `${dupl} is already in use.` }); + return; + } + + let password: string | undefined = undefined; + if (req.body.password) { + password = await hash(req.body.password); + } + + const updatedUser = await UserModel.update(req.session.user.id, { name, email, password }); + + if (!updatedUser) { + throw new Error("Failed to update user"); + } + + req.session.user = updatedUser; + req.session.save(() => { + res.status(200).json(req.session.user); + }); + } catch (err: unknown) { + console.log(err); + res.status(500).end(); + } +}; From 20b26448ae67af3aa1e5dc6fa62c632c5d159854 Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 19 Mar 2023 20:53:43 +0800 Subject: [PATCH 08/68] add auth api routes --- client/src/lib/auth.ts | 85 +++++++++++++++++++++++++++++++-- server/src/routes/auth.route.ts | 6 +-- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 0c99c9c..b20fc1b 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -11,9 +11,7 @@ export const fetchSession = async () => { const user: User = await res.json(); return user; } - } catch (err) { - console.error(err); - } + } catch (err) {} }; export const setGuestSession = async (name: string) => { @@ -34,3 +32,84 @@ export const setGuestSession = async (name: string) => { console.error(err); } }; + +export const register = async (name: string, password: string, email?: string) => { + try { + const res = await fetch(`${API_URL}/v1/auth/register`, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ name, password, email }) + }); + if (res.status === 201) { + const user: User = await res.json(); + return user; + } else if (res.status === 409) { + const { message } = await res.json(); + return message as string; + } + } catch (err) { + console.error(err); + } +}; + +export const login = async (name: string, password: string) => { + try { + const res = await fetch(`${API_URL}/v1/auth/login`, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ name, password }) + }); + if (res.status === 200) { + const user: User = await res.json(); + return user; + } else if (res.status === 404 || res.status === 401) { + const { message } = await res.json(); + return message as string; + } + } catch (err) { + console.error(err); + } +}; + +export const logout = async () => { + try { + const res = await fetch(`${API_URL}/v1/auth/logout`, { + method: "POST", + credentials: "include" + }); + if (res.status === 204) { + return true; + } + } catch (err) { + console.error(err); + } +}; + +export const updateUser = async (email?: string, password?: string) => { + try { + if (!email && !password) return; + const res = await fetch(`${API_URL}/v1/auth/`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ email, password }) + }); + if (res.status === 200) { + const user: User = await res.json(); + return user; + } else if (res.status === 409) { + const { message } = await res.json(); + return message as string; + } + } catch (err) { + console.error(err); + } +}; diff --git a/server/src/routes/auth.route.ts b/server/src/routes/auth.route.ts index 11aa1dc..efe6641 100644 --- a/server/src/routes/auth.route.ts +++ b/server/src/routes/auth.route.ts @@ -3,14 +3,14 @@ import * as controller from "../controllers/auth.controller.js"; const router = Router(); -router.route("/").get(controller.getCurrentSession); +router.route("/").get(controller.getCurrentSession).patch(controller.updateUser); // 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); +router.route("/register").post(controller.registerUser); +router.route("/login").post(controller.loginUser); export default router; From f564298c939f361cc9ab21268c2351a6b6d9096a Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 19 Mar 2023 20:53:59 +0800 Subject: [PATCH 09/68] login and register tabs, initial user dashboard --- client/src/components/auth/AuthModal.tsx | 261 ++++++++++++++++++----- client/src/components/auth/Guest.tsx | 26 +++ client/src/components/auth/Login.tsx | 35 +++ client/src/components/auth/Register.tsx | 46 ++++ client/src/components/game/GamePage.tsx | 4 +- 5 files changed, 320 insertions(+), 52 deletions(-) create mode 100644 client/src/components/auth/Guest.tsx create mode 100644 client/src/components/auth/Login.tsx create mode 100644 client/src/components/auth/Register.tsx diff --git a/client/src/components/auth/AuthModal.tsx b/client/src/components/auth/AuthModal.tsx index a164caa..f92c3a6 100644 --- a/client/src/components/auth/AuthModal.tsx +++ b/client/src/components/auth/AuthModal.tsx @@ -1,36 +1,128 @@ "use client"; -import type { FormEvent } from "react"; -import { useRef, useState, useContext } from "react"; import { SessionContext } from "@/context/session"; -import { setGuestSession } from "@/lib/auth"; +import { login, logout, register, setGuestSession, updateUser } from "@/lib/auth"; +import type { FormEvent } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; -// TODO: add login and register views +import Guest from "./Guest"; +import Login from "./Login"; +import Register from "./Register"; export default function AuthModal() { const session = useContext(SessionContext); + const [activeTab, setActiveTab] = useState<"guest" | "login" | "register">("guest"); + const [serverMessage, setServerMessage] = useState(null); const [buttonLoading, setButtonLoading] = useState(false); const modalToggleRef = useRef(null); - async function updateGuestName(e: FormEvent) { + async function clickLogout() { + if (serverMessage) { + setServerMessage(null); + } + await logout(); + session?.setUser(null); + } + + async function updateAccount(e: FormEvent) { + e.preventDefault(); + const target = e.target as HTMLFormElement; + const updateEmail = target.elements.namedItem("updateEmail") as HTMLInputElement; + const updatePassword = target.elements.namedItem("updatePassword") as HTMLInputElement; + if ( + !updateEmail || + !updatePassword || + ((!updateEmail.value || updateEmail.value === session?.user?.email) && !updatePassword.value) + ) + return; + + setButtonLoading(true); + const user = await updateUser( + updateEmail.value || undefined, + updatePassword.value || undefined + ); + if (typeof user === "string") { + setServerMessage(user); + } else if (user?.id) { + session?.setUser(user); + setServerMessage("Account updated successfully"); + setTimeout(() => { + setServerMessage(null); + }, 5000); + } + updatePassword.value = ""; + setButtonLoading(false); + } + + async function submitAuth(e: FormEvent) { e.preventDefault(); const target = e.target as HTMLFormElement; - const guestName = target.elements.namedItem("guestName") as HTMLInputElement; - if (!guestName || !guestName.value) return; + if (activeTab === "guest") { + const guestName = target.elements.namedItem("guestName") as HTMLInputElement; + if (!guestName || !guestName.value) return; - setButtonLoading(true); - const user = await setGuestSession(guestName.value); - if (user) { - session?.setUser(user); - if (modalToggleRef.current?.checked) { - modalToggleRef.current.checked = false; + setButtonLoading(true); + const user = await setGuestSession(guestName.value); + if (user) { + session?.setUser(user); + if (modalToggleRef.current?.checked) { + modalToggleRef.current.checked = false; + } + } + guestName.value = ""; + } else if (activeTab === "login") { + const loginName = target.elements.namedItem("loginName") as HTMLInputElement; + const loginPassword = target.elements.namedItem("loginPassword") as HTMLInputElement; + if (!loginName || !loginName.value || !loginPassword || !loginPassword.value) return; + + setButtonLoading(true); + const user = await login(loginName.value, loginPassword.value); + if (typeof user === "string") { + setServerMessage(user); + } else if (user?.id) { + session?.setUser(user); + if (serverMessage) { + setServerMessage(null); + } + if (modalToggleRef.current?.checked) { + modalToggleRef.current.checked = false; + } + } + } else if (activeTab === "register") { + const registerName = target.elements.namedItem("registerName") as HTMLInputElement; + const registerEmail = target.elements.namedItem("registerEmail") as HTMLInputElement; + const registerPassword = target.elements.namedItem("registerPassword") as HTMLInputElement; + if (!registerName || !registerName.value || !registerPassword || !registerPassword.value) { + return; + } + + setButtonLoading(true); + const user = await register( + registerName.value, + registerPassword.value, + registerEmail.value || undefined + ); + + if (typeof user === "string") { + setServerMessage(user); + } else if (user?.id) { + session?.setUser(user); + if (serverMessage) { + setServerMessage(null); + } + if (modalToggleRef.current?.checked) { + modalToggleRef.current.checked = false; + } } } - guestName.value = ""; setButtonLoading(false); } + useEffect(() => { + setServerMessage(null); + }, [activeTab]); + return ( <> @@ -39,47 +131,116 @@ export default function AuthModal() { htmlFor="auth-modal" className={"modal" + (session?.user === null ? " modal-open" : "")} > -