From 1b721533fe1800f7daf90f5228eb59838bbb0d4d Mon Sep 17 00:00:00 2001 From: N4r35h Date: Sat, 20 Apr 2024 15:29:17 +0530 Subject: [PATCH 01/12] add basic guest play support --- apps/backend/package.json | 3 +- apps/backend/src/index.ts | 1 + apps/backend/src/router/auth.ts | 18 ++++- apps/frontend/src/components/MatchHeading.tsx | 33 ++++++++++ apps/frontend/src/components/PlayerTitle.tsx | 22 +++++++ apps/frontend/src/screens/Game.tsx | 18 +++-- apps/frontend/src/screens/Login.tsx | 16 ++++- apps/ws/src/Game.ts | 66 +++++++++++-------- apps/ws/src/GameManager.ts | 6 +- apps/ws/src/SocketManager.ts | 4 +- apps/ws/src/auth/index.ts | 16 ++++- apps/ws/src/index.ts | 9 ++- packages/store/package.json | 3 +- 13 files changed, 166 insertions(+), 49 deletions(-) create mode 100644 apps/frontend/src/components/MatchHeading.tsx create mode 100644 apps/frontend/src/components/PlayerTitle.tsx diff --git a/apps/backend/package.json b/apps/backend/package.json index 9d71feec..4c76d19d 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -22,7 +22,8 @@ "passport": "^0.7.0", "passport-facebook": "^3.0.0", "passport-github2": "^0.1.12", - "passport-google-oauth20": "^2.0.0" + "passport-google-oauth20": "^2.0.0", + "uuid": "^9.0.1" }, "devDependencies": { "@types/cookie-session": "^2.0.49", diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 7dccfa9b..a3b31bd1 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -10,6 +10,7 @@ import passport from "passport"; const app = express(); dotenv.config(); +app.use(express.json()); app.use(session({ secret: 'keyboard cat', resave: false, diff --git a/apps/backend/src/router/auth.ts b/apps/backend/src/router/auth.ts index 492b7cf4..3b9f4a52 100644 --- a/apps/backend/src/router/auth.ts +++ b/apps/backend/src/router/auth.ts @@ -2,6 +2,7 @@ import { Request, Response, Router } from 'express'; import passport from 'passport'; import jwt from 'jsonwebtoken'; import { db } from '../db'; +import {v4 as uuidv4} from "uuid" const router = Router(); const CLIENT_URL = 'http://localhost:5173/game/random'; @@ -9,8 +10,23 @@ const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key'; interface User { id: string; + token?: string + name: string } +// this route is to be hit when the user wants to login as a guest +router.post("/guest", (req: Request, res: Response) => { + const bodyData = req.body; + let guestUUID = "guest-" + uuidv4() + let User: User = { + id: guestUUID, + name: bodyData.name || guestUUID, + } + const token = jwt.sign({ userId: User.id, name: User.name }, JWT_SECRET); + User.token = token + res.json(User); +}) + router.get('/refresh', async (req: Request, res: Response) => { if (req.user) { const user = req.user as User; @@ -24,7 +40,7 @@ router.get('/refresh', async (req: Request, res: Response) => { } }); - const token = jwt.sign({ userId: user.id }, JWT_SECRET); + const token = jwt.sign({ userId: user.id, name: userDb?.name }, JWT_SECRET); res.json({ token, id: user.id, diff --git a/apps/frontend/src/components/MatchHeading.tsx b/apps/frontend/src/components/MatchHeading.tsx new file mode 100644 index 00000000..538370f0 --- /dev/null +++ b/apps/frontend/src/components/MatchHeading.tsx @@ -0,0 +1,33 @@ +import { useUser } from "@repo/store/useUser" +import { Metadata, Player } from "../screens/Game" +import { PlayerTitle } from "./PlayerTitle" + +interface Heading { + gameMetadata: Metadata | null +} + +export const MatchHeading = ({gameMetadata}: Heading) => { + const user = useUser() + let selfPlayer: Player | undefined = {id: user.id, name: user.name} + let opponent: Player | undefined + if (gameMetadata?.blackPlayer && gameMetadata?.whitePlayer) { + if (user.id === gameMetadata?.blackPlayer.id) { + selfPlayer = gameMetadata?.blackPlayer + opponent = gameMetadata?.whitePlayer + } else { + selfPlayer = gameMetadata?.whitePlayer + opponent = gameMetadata?.blackPlayer + } + } + return ( +
+ + {opponent && + <> +

vs

+ + + } +
+ ) +} \ No newline at end of file diff --git a/apps/frontend/src/components/PlayerTitle.tsx b/apps/frontend/src/components/PlayerTitle.tsx new file mode 100644 index 00000000..81784e82 --- /dev/null +++ b/apps/frontend/src/components/PlayerTitle.tsx @@ -0,0 +1,22 @@ +import { Player } from "../screens/Game" + +interface PlayerTitleProps { + player: Player | undefined + isSelf?: boolean +} + +export const PlayerTitle = ({player, isSelf}: PlayerTitleProps) => { + return ( +
+ {player && player.id.startsWith("guest") && +

+ [Guest] +

+ } +

{player && player.name}

+ {isSelf && +

(You)

+ } +
+ ) +} \ No newline at end of file diff --git a/apps/frontend/src/screens/Game.tsx b/apps/frontend/src/screens/Game.tsx index c2b39519..736e4a6e 100644 --- a/apps/frontend/src/screens/Game.tsx +++ b/apps/frontend/src/screens/Game.tsx @@ -8,6 +8,7 @@ import { Chess, Square } from 'chess.js' import { useNavigate, useParams } from "react-router-dom"; import MovesTable from "../components/MovesTable"; import { useUser } from "@repo/store/useUser"; +import { MatchHeading } from "../components/MatchHeading"; // TODO: Move together, there's code repetition here export const INIT_GAME = "init_game"; @@ -21,9 +22,14 @@ export interface IMove { from: Square; to: Square } -interface Metadata { - blackPlayer: { id: string, name: string }; - whitePlayer: {id: string, name: string }; +export interface Player { + id: string + name: string +} + +export interface Metadata { + blackPlayer: Player; + whitePlayer: Player; } export const Game = () => { @@ -95,7 +101,7 @@ export const Game = () => { }) setStarted(true) setMoves(message.payload.moves); - message.payload.moves.map(x => { + message.payload.moves.map((x: any) => { if (isPromoting(chess, x.from, x.to)) { chess.move({...x, promotion: 'q' }) } else { @@ -120,9 +126,7 @@ export const Game = () => { if (!socket) return
Connecting...
return
-
- {gameMetadata?.blackPlayer?.name} vs {gameMetadata?.whitePlayer?.name} -
+ {result &&
{result}
} diff --git a/apps/frontend/src/screens/Login.tsx b/apps/frontend/src/screens/Login.tsx index af81eb57..6b88fa78 100644 --- a/apps/frontend/src/screens/Login.tsx +++ b/apps/frontend/src/screens/Login.tsx @@ -1,11 +1,16 @@ import Google from "../assets/google.png"; import Github from "../assets/github.png"; import { useNavigate } from 'react-router-dom'; +import { useRef } from "react"; +import { useRecoilState } from "recoil"; +import { userAtom } from "@repo/store/userAtom"; const BACKEND_URL = "http://localhost:3000"; const Login = () => { const navigate = useNavigate(); + const guestName = useRef(null) + const [_, setUser] = useRecoilState(userAtom) const google = () => { window.open(`${BACKEND_URL}/auth/google`, "_self"); @@ -15,6 +20,14 @@ const Login = () => { window.open(`${BACKEND_URL}/auth/github`, "_self"); }; + const loginAsGuest = async () => { + const response = await fetch(`${BACKEND_URL}/auth/guest`, {method: "POST", headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify({name: guestName.current && guestName.current.value || ""})}) + const user = await response.json(); + setUser(user) + navigate("/game/random") + } return (
@@ -46,12 +59,13 @@ const Login = () => {
diff --git a/apps/ws/src/Game.ts b/apps/ws/src/Game.ts index 3ee9286a..1bbef9af 100644 --- a/apps/ws/src/Game.ts +++ b/apps/ws/src/Game.ts @@ -4,6 +4,7 @@ import { GAME_OVER, INIT_GAME, MOVE } from "./messages"; import { db } from "./db"; import { randomUUID } from "crypto"; import { SocketManager, User } from "./SocketManager"; +import { isGuest } from "./auth"; export function isPromoting(chess: Chess, from: Square, to: Square) { if (!from) { @@ -30,46 +31,56 @@ export function isPromoting(chess: Chess, from: Square, to: Square) { .includes(to); } +export interface Player { + id?: string + userId: string + name: string +} + export class Game { public gameId: string; - public player1UserId: string; - public player2UserId: string | null; + public player1: Player; + public player2: Player | null; public board: Chess private startTime: Date; private moveCount = 0; - constructor(player1UserId: string, player2UserId: string | null) { - this.player1UserId = player1UserId; - this.player2UserId = player2UserId; + constructor(player1: Player, player2: Player | null) { + this.player1 = player1; + this.player2 = player2; this.board = new Chess(); this.startTime = new Date(); this.gameId = randomUUID(); } - async updateSecondPlayer(player2UserId: string) { - this.player2UserId = player2UserId; - - const users = await db.user.findMany({ - where: { - id: { - in: [this.player1UserId, this.player2UserId ?? ""] - } - } - }); + isGuestGame() { + if (isGuest(this.player1) || isGuest(this.player2)) { + return true + } + return false + } - try { - await this.createGameInDb(); - } catch(e) { - console.error(e) - return; + async updateSecondPlayer(player2: Player) { + this.player2 = player2; + let player1Name = this.player1.name + let player2Name = this.player2?.name + + // only create the game in db if its not a guest game + if (!this.isGuestGame()) { + try { + await this.createGameInDb(); + } catch(e) { + console.error(e) + return; + } } SocketManager.getInstance().broadcast(this.gameId, JSON.stringify({ type: INIT_GAME, payload: { gameId: this.gameId, - whitePlayer: { name: users.find(user => user.id === this.player1UserId)?.name, id: this.player1UserId }, - blackPlayer: { name: users.find(user => user.id === this.player2UserId)?.name, id: this.player2UserId }, + whitePlayer: { name: player1Name, id: this.player1.userId }, + blackPlayer: { name: player2Name, id: this.player2.userId }, fen: this.board.fen(), moves: [] } @@ -87,12 +98,12 @@ export class Game { currentFen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", whitePlayer: { connect: { - id: this.player1UserId + id: this.player1.userId } }, blackPlayer: { connect: { - id: this.player2UserId ?? "" + id: this.player2?.userId ?? "" } }, }, @@ -108,6 +119,9 @@ export class Game { from: string; to: string; }) { + if (this.isGuestGame()) { + return + } await db.$transaction([ db.move.create({ data: { @@ -137,10 +151,10 @@ export class Game { to: Square; }) { // validate the type of move using zod - if (this.moveCount % 2 === 0 && user.userId !== this.player1UserId) { + if (this.moveCount % 2 === 0 && user.userId !== this.player1.userId) { return } - if (this.moveCount % 2 === 1 && user.userId !== this.player2UserId) { + if (this.moveCount % 2 === 1 && user.userId !== this.player2?.userId) { return; } diff --git a/apps/ws/src/GameManager.ts b/apps/ws/src/GameManager.ts index 96d92131..1174c2d0 100644 --- a/apps/ws/src/GameManager.ts +++ b/apps/ws/src/GameManager.ts @@ -42,10 +42,10 @@ export class GameManager { return; } SocketManager.getInstance().addUser(user, game.gameId) - await game?.updateSecondPlayer(user.userId); + await game?.updateSecondPlayer(user); this.pendingGameId = null; } else { - const game = new Game(user.userId, null) + const game = new Game(user, null) this.games.push(game); this.pendingGameId = game.gameId; SocketManager.getInstance().addUser(user, game.gameId) @@ -87,7 +87,7 @@ export class GameManager { } if (!availableGame) { - const game = new Game(gameFromDb?.whitePlayerId!, gameFromDb?.blackPlayerId!); + const game = new Game({name: gameFromDb?.whitePlayer?.name || "", userId: gameFromDb?.whitePlayerId!}, {name: gameFromDb?.blackPlayer?.name || "", userId: gameFromDb?.blackPlayerId!}); gameFromDb?.moves.forEach((move) => { if (isPromoting(game.board, move.from as Square, move.to as Square)) { game.board.move({ diff --git a/apps/ws/src/SocketManager.ts b/apps/ws/src/SocketManager.ts index 49754090..c9a29437 100644 --- a/apps/ws/src/SocketManager.ts +++ b/apps/ws/src/SocketManager.ts @@ -5,11 +5,13 @@ export class User { public socket: WebSocket; public id: string; public userId: string; + public name: string - constructor(socket: WebSocket, userId: string) { + constructor(socket: WebSocket, userId: string, name: string) { this.socket = socket; this.userId = userId; this.id = randomUUID(); + this.name = name } } diff --git a/apps/ws/src/auth/index.ts b/apps/ws/src/auth/index.ts index f9061cb2..8762a6bf 100644 --- a/apps/ws/src/auth/index.ts +++ b/apps/ws/src/auth/index.ts @@ -1,8 +1,18 @@ import jwt from 'jsonwebtoken'; +import { User } from '../SocketManager'; +import { Player } from '../Game'; +import { WebSocket } from "ws"; const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key'; -export const extractUserId = (token: string) => { - const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; - return decoded.userId; +export const extractAuthUser = (token: string, ws: WebSocket): User => { + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string, name: string }; + return new User(ws, decoded.userId, decoded.name) +} + +export const isGuest = (player: Player | null): boolean => { + if (player && player.userId.startsWith("guest-")) { + return true + } + return false } \ No newline at end of file diff --git a/apps/ws/src/index.ts b/apps/ws/src/index.ts index cfe26f6d..bf68fcc8 100644 --- a/apps/ws/src/index.ts +++ b/apps/ws/src/index.ts @@ -1,8 +1,7 @@ import { WebSocketServer } from 'ws'; import { GameManager } from './GameManager'; import url from "url"; -import { extractUserId } from './auth'; -import { User } from './SocketManager'; +import { extractAuthUser } from './auth'; const wss = new WebSocketServer({ port: 8080 }); @@ -12,11 +11,11 @@ wss.on('connection', function connection(ws, req) { //@ts-ignore const token: string = url.parse(req.url, true).query.token; - const userId = extractUserId(token); - gameManager.addUser(new User(ws, userId)); + const User = extractAuthUser(token, ws); + gameManager.addUser(User); ws.on("close", () => { - gameManager.removeUser(ws, userId) + gameManager.removeUser(ws) }) }); diff --git a/packages/store/package.json b/packages/store/package.json index 6df12d90..cf3a5c18 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "exports": { - "./useUser": "./src/hooks/useUser.ts" + "./useUser": "./src/hooks/useUser.ts", + "./userAtom": "./src/atoms/user.ts" }, "keywords": [], "author": "", From ed6e95e72eeb36c2981f5808e26ee40f537c6029 Mon Sep 17 00:00:00 2001 From: N4r35h Date: Sun, 21 Apr 2024 05:57:27 +0530 Subject: [PATCH 02/12] save users to database with auth provider set to guest so we can persist games with current datamodal --- apps/backend/src/index.ts | 34 +- apps/backend/src/router/auth.ts | 110 +++-- apps/frontend/src/components/MatchHeading.tsx | 33 -- apps/frontend/src/components/UserAvatar.tsx | 26 +- apps/frontend/src/screens/Game.tsx | 6 +- apps/ws/src/Game.ts | 382 +++++++++--------- apps/ws/src/GameManager.ts | 283 +++++++------ apps/ws/src/SocketManager.ts | 112 ++--- apps/ws/src/auth/index.ts | 19 +- .../migration.sql | 2 + packages/db/prisma/schema.prisma | 1 + 11 files changed, 539 insertions(+), 469 deletions(-) delete mode 100644 apps/frontend/src/components/MatchHeading.tsx create mode 100644 packages/db/prisma/migrations/20240420233109_add_guest_auth_provider/migration.sql diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 36587044..f8829191 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,39 +1,43 @@ -import express from "express"; -import v1Router from "./router/v1"; -import cors from "cors"; -import { initPassport } from "./passport"; -import authRoute from "./router/auth"; -import dotenv from "dotenv"; +import express from 'express'; +import v1Router from './router/v1'; +import cors from 'cors'; +import { initPassport } from './passport'; +import authRoute from './router/auth'; +import dotenv from 'dotenv'; import session from 'express-session'; -import passport from "passport"; +import passport from 'passport'; const app = express(); dotenv.config(); app.use(express.json()); -app.use(session({ +app.use( + session({ secret: process.env.COOKIE_SECRET || 'keyboard cat', resave: false, saveUninitialized: false, - cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 } -})); + cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 }, + }), +); initPassport(); app.use(passport.initialize()); app.use(passport.authenticate('session')); -const allowedHosts = process.env.ALLOWED_HOSTS ? process.env.ALLOWED_HOSTS.split(",") : []; +const allowedHosts = process.env.ALLOWED_HOSTS + ? process.env.ALLOWED_HOSTS.split(',') + : []; app.use( cors({ origin: allowedHosts, - methods: "GET,POST,PUT,DELETE", + methods: 'GET,POST,PUT,DELETE', credentials: true, - }) + }), ); -app.use("/auth", authRoute); -app.use("/v1", v1Router); +app.use('/auth', authRoute); +app.use('/v1', v1Router); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/apps/backend/src/router/auth.ts b/apps/backend/src/router/auth.ts index 0250c4c1..81e4cdea 100644 --- a/apps/backend/src/router/auth.ts +++ b/apps/backend/src/router/auth.ts @@ -2,30 +2,46 @@ import { Request, Response, Router } from 'express'; import passport from 'passport'; import jwt from 'jsonwebtoken'; import { db } from '../db'; -import {v4 as uuidv4} from "uuid" +import { v4 as uuidv4 } from 'uuid'; const router = Router(); -const CLIENT_URL = process.env.AUTH_REDIRECT_URL ?? 'http://localhost:5173/game/random'; +const CLIENT_URL = + process.env.AUTH_REDIRECT_URL ?? 'http://localhost:5173/game/random'; const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key'; interface User { id: string; - token?: string - name: string + token?: string; + name: string; + isGuest?: boolean; } // this route is to be hit when the user wants to login as a guest -router.post("/guest", (req: Request, res: Response) => { +router.post('/guest', async (req: Request, res: Response) => { const bodyData = req.body; - let guestUUID = "guest-" + uuidv4() + let guestUUID = 'guest-' + uuidv4(); + + const user = await db.user.create({ + data: { + username: guestUUID, + email: guestUUID + '@chess100x.com', + name: bodyData.name || guestUUID, + provider: 'GUEST', + }, + }); + + const token = jwt.sign( + { userId: user.id, name: user.name, isGuest: true }, + JWT_SECRET, + ); let User: User = { - id: guestUUID, - name: bodyData.name || guestUUID, - } - const token = jwt.sign({ userId: User.id, name: User.name }, JWT_SECRET); - User.token = token + id: user.id, + name: user.name!, + token: token, + isGuest: true, + }; res.json(User); -}) +}); router.get('/refresh', async (req: Request, res: Response) => { if (req.user) { @@ -34,17 +50,17 @@ router.get('/refresh', async (req: Request, res: Response) => { // Token is issued so it can be shared b/w HTTP and ws server // Todo: Make this temporary and add refresh logic here - const userDb =await db.user.findFirst({ + const userDb = await db.user.findFirst({ where: { - id: user.id - } + id: user.id, + }, }); const token = jwt.sign({ userId: user.id, name: userDb?.name }, JWT_SECRET); res.json({ token, id: user.id, - name: userDb?.name + name: userDb?.name, }); } else { res.status(401).json({ success: false, message: 'Unauthorized' }); @@ -67,25 +83,43 @@ router.get('/logout', (req: Request, res: Response) => { }); }); -router.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] })); - -router.get('/google/callback', passport.authenticate('google', { - successRedirect: CLIENT_URL, - failureRedirect: '/login/failed', -})); - -router.get('/github', passport.authenticate('github', { scope: ['profile', 'email'] })); - -router.get('/github/callback', passport.authenticate('github', { - successRedirect: CLIENT_URL, - failureRedirect: '/login/failed', -})); - -router.get('/facebook', passport.authenticate('facebook', { scope: ['profile'] })); - -router.get('/facebook/callback', passport.authenticate('facebook', { - successRedirect: CLIENT_URL, - failureRedirect: '/login/failed', -})); - -export default router; \ No newline at end of file +router.get( + '/google', + passport.authenticate('google', { scope: ['profile', 'email'] }), +); + +router.get( + '/google/callback', + passport.authenticate('google', { + successRedirect: CLIENT_URL, + failureRedirect: '/login/failed', + }), +); + +router.get( + '/github', + passport.authenticate('github', { scope: ['profile', 'email'] }), +); + +router.get( + '/github/callback', + passport.authenticate('github', { + successRedirect: CLIENT_URL, + failureRedirect: '/login/failed', + }), +); + +router.get( + '/facebook', + passport.authenticate('facebook', { scope: ['profile'] }), +); + +router.get( + '/facebook/callback', + passport.authenticate('facebook', { + successRedirect: CLIENT_URL, + failureRedirect: '/login/failed', + }), +); + +export default router; diff --git a/apps/frontend/src/components/MatchHeading.tsx b/apps/frontend/src/components/MatchHeading.tsx deleted file mode 100644 index 538370f0..00000000 --- a/apps/frontend/src/components/MatchHeading.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useUser } from "@repo/store/useUser" -import { Metadata, Player } from "../screens/Game" -import { PlayerTitle } from "./PlayerTitle" - -interface Heading { - gameMetadata: Metadata | null -} - -export const MatchHeading = ({gameMetadata}: Heading) => { - const user = useUser() - let selfPlayer: Player | undefined = {id: user.id, name: user.name} - let opponent: Player | undefined - if (gameMetadata?.blackPlayer && gameMetadata?.whitePlayer) { - if (user.id === gameMetadata?.blackPlayer.id) { - selfPlayer = gameMetadata?.blackPlayer - opponent = gameMetadata?.whitePlayer - } else { - selfPlayer = gameMetadata?.whitePlayer - opponent = gameMetadata?.blackPlayer - } - } - return ( -
- - {opponent && - <> -

vs

- - - } -
- ) -} \ No newline at end of file diff --git a/apps/frontend/src/components/UserAvatar.tsx b/apps/frontend/src/components/UserAvatar.tsx index 0631d55d..b0502010 100644 --- a/apps/frontend/src/components/UserAvatar.tsx +++ b/apps/frontend/src/components/UserAvatar.tsx @@ -1,6 +1,24 @@ +import { useUser } from '@repo/store/useUser'; +import { Metadata, Player } from '../screens/Game'; -export const UserAvatar = ({ name }: { name: string; }) => { - return
- {name} +interface UserAvatarProps { + gameMetadata: Metadata | null; + self?: boolean; +} + +export const UserAvatar = ({ gameMetadata, self }: UserAvatarProps) => { + const user = useUser(); + let player: Player; + if (gameMetadata?.blackPlayer.id === user.id) { + player = self ? gameMetadata.blackPlayer : gameMetadata.whitePlayer; + } else { + player = self ? gameMetadata?.whitePlayer! : gameMetadata?.blackPlayer!; + } + + return ( +
+

{player?.name}

+ {player?.isGuest &&

[Guest]

}
-} \ No newline at end of file + ); +}; diff --git a/apps/frontend/src/screens/Game.tsx b/apps/frontend/src/screens/Game.tsx index 0892ff91..6c5a42fb 100644 --- a/apps/frontend/src/screens/Game.tsx +++ b/apps/frontend/src/screens/Game.tsx @@ -8,7 +8,6 @@ import { Chess, Move, Square } from 'chess.js'; import { useNavigate, useParams } from 'react-router-dom'; import MovesTable from '../components/MovesTable'; import { useUser } from '@repo/store/useUser'; -import { MatchHeading } from '../components/MatchHeading'; import { UserAvatar } from '../components/UserAvatar'; // TODO: Move together, there's code repetition here @@ -29,6 +28,7 @@ export interface IMove { export interface Player { id: string; name: string; + isGuest: boolean; } export interface Metadata { @@ -150,7 +150,7 @@ export const Game = () => {
- +
{ />
- +
diff --git a/apps/ws/src/Game.ts b/apps/ws/src/Game.ts index 1bbef9af..bc2aa3ba 100644 --- a/apps/ws/src/Game.ts +++ b/apps/ws/src/Game.ts @@ -1,207 +1,213 @@ -import { WebSocket } from "ws"; -import { Chess, Square } from 'chess.js' -import { GAME_OVER, INIT_GAME, MOVE } from "./messages"; -import { db } from "./db"; -import { randomUUID } from "crypto"; -import { SocketManager, User } from "./SocketManager"; -import { isGuest } from "./auth"; +import { WebSocket } from 'ws'; +import { Chess, Square } from 'chess.js'; +import { GAME_OVER, INIT_GAME, MOVE } from './messages'; +import { db } from './db'; +import { randomUUID } from 'crypto'; +import { SocketManager, User } from './SocketManager'; export function isPromoting(chess: Chess, from: Square, to: Square) { - if (!from) { - return false; - } + if (!from) { + return false; + } - const piece = chess.get(from); - - if (piece?.type !== "p") { - return false; - } - - if (piece.color !== chess.turn()) { - return false; - } - - if (!["1", "8"].some((it) => to.endsWith(it))) { - return false; - } - - return chess - .moves({ square: from, verbose: true }) - .map((it) => it.to) - .includes(to); + const piece = chess.get(from); + + if (piece?.type !== 'p') { + return false; + } + + if (piece.color !== chess.turn()) { + return false; + } + + if (!['1', '8'].some((it) => to.endsWith(it))) { + return false; + } + + return chess + .moves({ square: from, verbose: true }) + .map((it) => it.to) + .includes(to); } export interface Player { - id?: string - userId: string - name: string + id?: string; + userId: string; + name: string; + isGuest?: boolean; } export class Game { - public gameId: string; - public player1: Player; - public player2: Player | null; - public board: Chess - private startTime: Date; - private moveCount = 0; - - constructor(player1: Player, player2: Player | null) { - this.player1 = player1; - this.player2 = player2; - this.board = new Chess(); - this.startTime = new Date(); - this.gameId = randomUUID(); + public gameId: string; + public player1: Player; + public player2: Player | null; + public board: Chess; + private startTime: Date; + private moveCount = 0; + + constructor(player1: Player, player2: Player | null) { + this.player1 = player1; + this.player2 = player2; + this.board = new Chess(); + this.startTime = new Date(); + this.gameId = randomUUID(); + } + + async updateSecondPlayer(player2: Player) { + this.player2 = player2; + let player1Name = this.player1.name; + let player2Name = this.player2?.name; + + try { + await this.createGameInDb(); + } catch (e) { + console.error(e); + return; } - isGuestGame() { - if (isGuest(this.player1) || isGuest(this.player2)) { - return true - } - return false + SocketManager.getInstance().broadcast( + this.gameId, + JSON.stringify({ + type: INIT_GAME, + payload: { + gameId: this.gameId, + whitePlayer: { + name: player1Name, + id: this.player1.userId, + isGuest: this.player1.isGuest, + }, + blackPlayer: { + name: player2Name, + id: this.player2.userId, + isGuest: this.player2.isGuest, + }, + fen: this.board.fen(), + moves: [], + }, + }), + ); + } + + async createGameInDb() { + const game = await db.game.create({ + data: { + id: this.gameId, + timeControl: 'CLASSICAL', + status: 'IN_PROGRESS', + currentFen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', + whitePlayer: { + connect: { + id: this.player1.userId, + }, + }, + blackPlayer: { + connect: { + id: this.player2?.userId ?? '', + }, + }, + }, + include: { + whitePlayer: true, + blackPlayer: true, + }, + }); + this.gameId = game.id; + } + + async addMoveToDb(move: { from: string; to: string }) { + await db.$transaction([ + db.move.create({ + data: { + gameId: this.gameId, + moveNumber: this.moveCount + 1, + from: move.from, + to: move.to, + // Todo: Fix start fen + startFen: this.board.fen(), + endFen: this.board.fen(), + createdAt: new Date(Date.now()), + }, + }), + db.game.update({ + data: { + currentFen: this.board.fen(), + }, + where: { + id: this.gameId, + }, + }), + ]); + } + + async makeMove( + user: User, + move: { + from: Square; + to: Square; + }, + ) { + // validate the type of move using zod + if (this.moveCount % 2 === 0 && user.userId !== this.player1.userId) { + return; } - - async updateSecondPlayer(player2: Player) { - this.player2 = player2; - let player1Name = this.player1.name - let player2Name = this.player2?.name - - // only create the game in db if its not a guest game - if (!this.isGuestGame()) { - try { - await this.createGameInDb(); - } catch(e) { - console.error(e) - return; - } - } - - SocketManager.getInstance().broadcast(this.gameId, JSON.stringify({ - type: INIT_GAME, - payload: { - gameId: this.gameId, - whitePlayer: { name: player1Name, id: this.player1.userId }, - blackPlayer: { name: player2Name, id: this.player2.userId }, - fen: this.board.fen(), - moves: [] - } - })); + if (this.moveCount % 2 === 1 && user.userId !== this.player2?.userId) { + return; } - - - async createGameInDb() { - const game = await db.game.create({ - data: { - id: this.gameId, - timeControl: "CLASSICAL", - status: "IN_PROGRESS", - currentFen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", - whitePlayer: { - connect: { - id: this.player1.userId - } - }, - blackPlayer: { - connect: { - id: this.player2?.userId ?? "" - } - }, - }, - include: { - whitePlayer: true, - blackPlayer: true, - } - }) - this.gameId = game.id; + try { + if (isPromoting(this.board, move.from, move.to)) { + this.board.move({ + from: move.from, + to: move.to, + promotion: 'q', + }); + } else { + this.board.move({ + from: move.from, + to: move.to, + }); + } + } catch (e) { + return; } - async addMoveToDb(move: { - from: string; - to: string; - }) { - if (this.isGuestGame()) { - return - } - await db.$transaction([ - db.move.create({ - data: { - gameId: this.gameId, - moveNumber: this.moveCount + 1, - from: move.from, - to: move.to, - // Todo: Fix start fen - startFen: this.board.fen(), - endFen: this.board.fen(), - createdAt: new Date(Date.now()), - }, - }), - db.game.update({ - data: { - currentFen: this.board.fen() - }, - where: { - id: this.gameId - } - }) - ]) + await this.addMoveToDb(move); + SocketManager.getInstance().broadcast( + this.gameId, + JSON.stringify({ + type: MOVE, + payload: move, + }), + ); + + if (this.board.isGameOver()) { + const result = this.board.isDraw() + ? 'DRAW' + : this.board.turn() === 'b' + ? 'WHITE_WINS' + : 'BLACK_WINS'; + + SocketManager.getInstance().broadcast( + this.gameId, + JSON.stringify({ + type: GAME_OVER, + payload: { + result, + }, + }), + ); + + await db.game.update({ + data: { + result, + status: 'COMPLETED', + }, + where: { + id: this.gameId, + }, + }); } - async makeMove(user: User, move: { - from: Square; - to: Square; - }) { - // validate the type of move using zod - if (this.moveCount % 2 === 0 && user.userId !== this.player1.userId) { - return - } - if (this.moveCount % 2 === 1 && user.userId !== this.player2?.userId) { - return; - } - - try { - if (isPromoting(this.board, move.from, move.to)) { - this.board.move({ - from: move.from, - to: move.to, - promotion: 'q' - }); - } else { - this.board.move({ - from: move.from, - to: move.to, - }); - } - } catch (e) { - return; - } - - await this.addMoveToDb(move); - SocketManager.getInstance().broadcast(this.gameId, JSON.stringify({ - type: MOVE, - payload: move - })) - - if (this.board.isGameOver()) { - const result = this.board.isDraw() ? "DRAW" : this.board.turn() === "b" ? "WHITE_WINS" : "BLACK_WINS"; - - SocketManager.getInstance().broadcast(this.gameId, JSON.stringify({ - type: GAME_OVER, - payload: { - result - } - })) - - await db.game.update({ - data: { - result, - status: "COMPLETED" - }, - where: { - id: this.gameId, - } - }) - } - - this.moveCount++; - } + this.moveCount++; + } } diff --git a/apps/ws/src/GameManager.ts b/apps/ws/src/GameManager.ts index 4c6ff39d..54aa9cae 100644 --- a/apps/ws/src/GameManager.ts +++ b/apps/ws/src/GameManager.ts @@ -1,140 +1,171 @@ -import { WebSocket } from "ws"; -import { INIT_GAME, JOIN_GAME, MOVE, OPPONENT_DISCONNECTED, JOIN_ROOM, GAME_JOINED, GAME_NOT_FOUND, GAME_ALERT, GAME_ADDED } from "./messages"; -import { Game, isPromoting } from "./Game"; -import { db } from "./db"; -import { SocketManager, User } from "./SocketManager"; -import { Square } from "chess.js"; +import { WebSocket } from 'ws'; +import { + INIT_GAME, + JOIN_GAME, + MOVE, + OPPONENT_DISCONNECTED, + JOIN_ROOM, + GAME_JOINED, + GAME_NOT_FOUND, + GAME_ALERT, + GAME_ADDED, +} from './messages'; +import { Game, isPromoting } from './Game'; +import { db } from './db'; +import { SocketManager, User } from './SocketManager'; +import { Square } from 'chess.js'; export class GameManager { - private games: Game[]; - private pendingGameId: string | null; - private users: User[]; + private games: Game[]; + private pendingGameId: string | null; + private users: User[]; - constructor() { - this.games = []; - this.pendingGameId = null; - this.users = []; - } + constructor() { + this.games = []; + this.pendingGameId = null; + this.users = []; + } + + addUser(user: User) { + this.users.push(user); + this.addHandler(user); + } - addUser(user: User) { - this.users.push(user); - this.addHandler(user) + removeUser(socket: WebSocket) { + const user = this.users.find((user) => user.socket !== socket); + if (!user) { + console.error('User not found?'); + return; } + this.users = this.users.filter((user) => user.socket !== socket); + SocketManager.getInstance().removeUser(user); + } - removeUser(socket: WebSocket) { - const user = this.users.find(user => user.socket !== socket); - if (!user) { - console.error("User not found?"); + private addHandler(user: User) { + user.socket.on('message', async (data) => { + const message = JSON.parse(data.toString()); + if (message.type === INIT_GAME) { + if (this.pendingGameId) { + const game = this.games.find((x) => x.gameId === this.pendingGameId); + if (!game) { + console.error('Pending game not found?'); return; + } + if (user.userId === game.player1.userId) { + SocketManager.getInstance().broadcast( + game.gameId, + JSON.stringify({ + type: GAME_ALERT, + payload: { + message: 'Trying to Connect with yourself?', + }, + }), + ); + return; + } + SocketManager.getInstance().addUser(user, game.gameId); + await game?.updateSecondPlayer(user); + this.pendingGameId = null; + } else { + const game = new Game(user, null); + this.games.push(game); + this.pendingGameId = game.gameId; + SocketManager.getInstance().addUser(user, game.gameId); + SocketManager.getInstance().broadcast( + game.gameId, + JSON.stringify({ + type: GAME_ADDED, + }), + ); } - this.users = this.users.filter(user => user.socket !== socket); - SocketManager.getInstance().removeUser(user) - } + } - private addHandler(user: User) { - user.socket.on("message", async (data) => { - const message = JSON.parse(data.toString()); - if (message.type === INIT_GAME) { - if (this.pendingGameId) { - const game = this.games.find(x => x.gameId === this.pendingGameId); - if (!game) { - console.error("Pending game not found?") - return; - } - if(user.userId === game.player1UserId) { - SocketManager.getInstance().broadcast(game.gameId, JSON.stringify({ - type: GAME_ALERT, - payload: { - message: "Trying to Connect with yourself?" - } - })); - return; - } - SocketManager.getInstance().addUser(user, game.gameId) - await game?.updateSecondPlayer(user); - this.pendingGameId = null; - } else { - const game = new Game(user, null) - this.games.push(game); - this.pendingGameId = game.gameId; - SocketManager.getInstance().addUser(user, game.gameId) - SocketManager.getInstance().broadcast(game.gameId, JSON.stringify({ - type: GAME_ADDED, - })); - } - } - - if (message.type === MOVE) { - const gameId = message.payload.gameId; - const game = this.games.find(game => game.gameId === gameId); - if (game) { - game.makeMove(user, message.payload.move); - } - } + if (message.type === MOVE) { + const gameId = message.payload.gameId; + const game = this.games.find((game) => game.gameId === gameId); + if (game) { + game.makeMove(user, message.payload.move); + } + } - if (message.type === JOIN_ROOM) { - const gameId = message.payload?.gameId; - if (!gameId) { - return; - } + if (message.type === JOIN_ROOM) { + const gameId = message.payload?.gameId; + if (!gameId) { + return; + } - const availableGame = this.games.find(game => game.gameId === gameId) - const gameFromDb = await db.game.findUnique({ - where: { id: gameId, }, include: { - moves: { - orderBy: { - moveNumber: "asc" - } - }, - blackPlayer: true, - whitePlayer: true, - } - }) - if (!gameFromDb) { + const availableGame = this.games.find((game) => game.gameId === gameId); + const gameFromDb = await db.game.findUnique({ + where: { id: gameId }, + include: { + moves: { + orderBy: { + moveNumber: 'asc', + }, + }, + blackPlayer: true, + whitePlayer: true, + }, + }); + if (!gameFromDb) { + user.socket.send( + JSON.stringify({ + type: GAME_NOT_FOUND, + }), + ); + return; + } - user.socket.send(JSON.stringify({ - type: GAME_NOT_FOUND - })); - return; - } + if (!availableGame) { + const game = new Game( + { + name: gameFromDb?.whitePlayer?.name || '', + userId: gameFromDb?.whitePlayerId!, + }, + { + name: gameFromDb?.blackPlayer?.name || '', + userId: gameFromDb?.blackPlayerId!, + }, + ); + gameFromDb?.moves.forEach((move) => { + if ( + isPromoting(game.board, move.from as Square, move.to as Square) + ) { + game.board.move({ + from: move.from, + to: move.to, + promotion: 'q', + }); + } else { + game.board.move({ + from: move.from, + to: move.to, + }); + } + }); + this.games.push(game); + } - if (!availableGame) { - const game = new Game({name: gameFromDb?.whitePlayer?.name || "", userId: gameFromDb?.whitePlayerId!}, {name: gameFromDb?.blackPlayer?.name || "", userId: gameFromDb?.blackPlayerId!}); - gameFromDb?.moves.forEach((move) => { - if (isPromoting(game.board, move.from as Square, move.to as Square)) { - game.board.move({ - from: move.from, - to: move.to, - promotion: 'q' - }); - } else { - game.board.move({ - from: move.from, - to: move.to, - }); - } - }); - this.games.push(game); - } - - user.socket.send(JSON.stringify({ - type: GAME_JOINED, - payload: { - gameId, - moves: gameFromDb.moves, - blackPlayer: { - id: gameFromDb.blackPlayer.id, - name: gameFromDb.blackPlayer.name, - }, - whitePlayer: { - id: gameFromDb.whitePlayer.id, - name: gameFromDb.whitePlayer.name, - } - } - })); + user.socket.send( + JSON.stringify({ + type: GAME_JOINED, + payload: { + gameId, + moves: gameFromDb.moves, + blackPlayer: { + id: gameFromDb.blackPlayer.id, + name: gameFromDb.blackPlayer.name, + }, + whitePlayer: { + id: gameFromDb.whitePlayer.id, + name: gameFromDb.whitePlayer.name, + }, + }, + }), + ); - SocketManager.getInstance().addUser(user, gameId) - } - }) - } -} \ No newline at end of file + SocketManager.getInstance().addUser(user, gameId); + } + }); + } +} diff --git a/apps/ws/src/SocketManager.ts b/apps/ws/src/SocketManager.ts index c9a29437..859334d0 100644 --- a/apps/ws/src/SocketManager.ts +++ b/apps/ws/src/SocketManager.ts @@ -1,68 +1,76 @@ -import { randomUUID } from "crypto"; -import { WebSocket } from "ws"; +import { randomUUID } from 'crypto'; +import { WebSocket } from 'ws'; +import { userJwtClaims } from './auth'; export class User { - public socket: WebSocket; - public id: string; - public userId: string; - public name: string + public socket: WebSocket; + public id: string; + public userId: string; + public name: string; + public isGuest?: boolean; - constructor(socket: WebSocket, userId: string, name: string) { - this.socket = socket; - this.userId = userId; - this.id = randomUUID(); - this.name = name - } + constructor(socket: WebSocket, userJwtClaims: userJwtClaims) { + this.socket = socket; + this.userId = userJwtClaims.userId; + this.id = randomUUID(); + this.name = userJwtClaims.name; + this.isGuest = userJwtClaims.isGuest; + } } export class SocketManager { - private static instance: SocketManager; - private interestedSockets: Map; - private userRoomMappping: Map; + private static instance: SocketManager; + private interestedSockets: Map; + private userRoomMappping: Map; - constructor() { - this.interestedSockets = new Map(); - this.userRoomMappping = new Map(); - } - - static getInstance() { - if (this.instance) { - return this.instance; - } + constructor() { + this.interestedSockets = new Map(); + this.userRoomMappping = new Map(); + } - this.instance = new SocketManager(); - return this.instance; + static getInstance() { + if (this.instance) { + return this.instance; } - addUser(user: User, roomId: string) { - this.interestedSockets.set(roomId, [...(this.interestedSockets.get(roomId) || []), user]); - this.userRoomMappping.set(user.id, roomId); - } + this.instance = new SocketManager(); + return this.instance; + } - broadcast(roomId: string, message: string) { - const users = this.interestedSockets.get(roomId); - if (!users) { - console.error("No users in room?"); - return; - } + addUser(user: User, roomId: string) { + this.interestedSockets.set(roomId, [ + ...(this.interestedSockets.get(roomId) || []), + user, + ]); + this.userRoomMappping.set(user.id, roomId); + } - users.forEach(user => { - user.socket.send(message); - }) + broadcast(roomId: string, message: string) { + const users = this.interestedSockets.get(roomId); + if (!users) { + console.error('No users in room?'); + return; } - removeUser(user: User) { - const roomId = this.userRoomMappping.get(user.id); - if (!roomId) { - console.error("User was not interested in any room?"); - return; - } - this.interestedSockets.set(roomId, (this.interestedSockets.get(roomId) || []).filter(u => u !== user)); - if (this.interestedSockets.get(roomId)?.length === 0) { - this.interestedSockets.delete(roomId); - } - - this.userRoomMappping.delete(user.id); + users.forEach((user) => { + user.socket.send(message); + }); + } + + removeUser(user: User) { + const roomId = this.userRoomMappping.get(user.id); + if (!roomId) { + console.error('User was not interested in any room?'); + return; + } + this.interestedSockets.set( + roomId, + (this.interestedSockets.get(roomId) || []).filter((u) => u !== user), + ); + if (this.interestedSockets.get(roomId)?.length === 0) { + this.interestedSockets.delete(roomId); } -} \ No newline at end of file + this.userRoomMappping.delete(user.id); + } +} diff --git a/apps/ws/src/auth/index.ts b/apps/ws/src/auth/index.ts index 8762a6bf..e80a3254 100644 --- a/apps/ws/src/auth/index.ts +++ b/apps/ws/src/auth/index.ts @@ -1,18 +1,17 @@ import jwt from 'jsonwebtoken'; import { User } from '../SocketManager'; import { Player } from '../Game'; -import { WebSocket } from "ws"; +import { WebSocket } from 'ws'; const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key'; -export const extractAuthUser = (token: string, ws: WebSocket): User => { - const decoded = jwt.verify(token, JWT_SECRET) as { userId: string, name: string }; - return new User(ws, decoded.userId, decoded.name) +export interface userJwtClaims { + userId: string; + name: string; + isGuest?: boolean; } -export const isGuest = (player: Player | null): boolean => { - if (player && player.userId.startsWith("guest-")) { - return true - } - return false -} \ No newline at end of file +export const extractAuthUser = (token: string, ws: WebSocket): User => { + const decoded = jwt.verify(token, JWT_SECRET) as userJwtClaims; + return new User(ws, decoded); +}; diff --git a/packages/db/prisma/migrations/20240420233109_add_guest_auth_provider/migration.sql b/packages/db/prisma/migrations/20240420233109_add_guest_auth_provider/migration.sql new file mode 100644 index 00000000..eac03f9d --- /dev/null +++ b/packages/db/prisma/migrations/20240420233109_add_guest_auth_provider/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "AuthProvider" ADD VALUE 'GUEST'; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 9f50b0af..cb6d89fb 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -83,4 +83,5 @@ enum AuthProvider { GOOGLE FACEBOOK GITHUB + GUEST } \ No newline at end of file From 6c062f496e21ec8a58302e84c5ee88fa30969c5a Mon Sep 17 00:00:00 2001 From: N4r35h Date: Sun, 21 Apr 2024 12:18:33 +0530 Subject: [PATCH 03/12] lint? --- apps/frontend/src/screens/Game.tsx | 233 ++++++++++++++++------------ apps/frontend/src/screens/Login.tsx | 2 - 2 files changed, 132 insertions(+), 103 deletions(-) diff --git a/apps/frontend/src/screens/Game.tsx b/apps/frontend/src/screens/Game.tsx index 26016499..f72140a8 100644 --- a/apps/frontend/src/screens/Game.tsx +++ b/apps/frontend/src/screens/Game.tsx @@ -22,8 +22,8 @@ export const GAME_ALERT = 'game_alert'; export const GAME_ADDED = 'game_added'; export interface IMove { - from: Square; - to: Square + from: Square; + to: Square; } export interface Player { @@ -56,47 +56,50 @@ export const Game = () => { >(null); const [moves, setMoves] = useState([]); - useEffect(() => { - if (!socket) { - return; - } - socket.onmessage = (event) => { - const message = JSON.parse(event.data); + useEffect(() => { + if (!socket) { + return; + } + socket.onmessage = (event) => { + const message = JSON.parse(event.data); - switch (message.type) { - case GAME_ADDED: - setAdded(true) - break; - case INIT_GAME: - setBoard(chess.board()); - setStarted(true) - navigate(`/game/${message.payload.gameId}`) - setGameMetadata({ - blackPlayer: message.payload.blackPlayer, - whitePlayer: message.payload.whitePlayer - }) - break; - case MOVE: - const move = message.payload; - const moves = chess.moves({verbose: true}); - //TODO: Fix later - if (moves.map(x => JSON.stringify(x)).includes(JSON.stringify(move))) return; - if (isPromoting(chess, move.from, move.to)) { - chess.move({ - from: move.from, - to: move.to, - promotion: 'q' - }); - } else { - chess.move({from:move.from, to: move.to}); - } - moveAudio.play() - setBoard(chess.board()); - setMoves(moves => [...moves, move]) - break; - case GAME_OVER: - setResult(message.payload.result); - break; + switch (message.type) { + case GAME_ADDED: + setAdded(true); + break; + case INIT_GAME: + setBoard(chess.board()); + setStarted(true); + navigate(`/game/${message.payload.gameId}`); + setGameMetadata({ + blackPlayer: message.payload.blackPlayer, + whitePlayer: message.payload.whitePlayer, + }); + break; + case MOVE: + const move = message.payload; + const moves = chess.moves({ verbose: true }); + //TODO: Fix later + if ( + moves.map((x) => JSON.stringify(x)).includes(JSON.stringify(move)) + ) + return; + if (isPromoting(chess, move.from, move.to)) { + chess.move({ + from: move.from, + to: move.to, + promotion: 'q', + }); + } else { + chess.move({ from: move.from, to: move.to }); + } + moveAudio.play(); + setBoard(chess.board()); + setMoves((moves) => [...moves, move]); + break; + case GAME_OVER: + setResult(message.payload.result); + break; case OPPONENT_DISCONNECTED: setResult(OPPONENT_DISCONNECTED); @@ -105,28 +108,28 @@ export const Game = () => { setResult(OPPONENT_DISCONNECTED); break; - case GAME_JOINED: - setGameMetadata({ - blackPlayer: message.payload.blackPlayer, - whitePlayer: message.payload.whitePlayer - }) - setStarted(true) - setMoves(message.payload.moves); - message.payload.moves.map((x: Move) => { - if (isPromoting(chess, x.from, x.to)) { - chess.move({...x, promotion: 'q' }) - } else { - chess.move(x) - } - }) - setBoard(chess.board()); - break; - - default: - alert(message.payload.message); - break; + case GAME_JOINED: + setGameMetadata({ + blackPlayer: message.payload.blackPlayer, + whitePlayer: message.payload.whitePlayer, + }); + setStarted(true); + setMoves(message.payload.moves); + message.payload.moves.map((x: Move) => { + if (isPromoting(chess, x.from, x.to)) { + chess.move({ ...x, promotion: 'q' }); + } else { + chess.move(x); } - } + }); + setBoard(chess.board()); + break; + + default: + alert(message.payload.message); + break; + } + }; if (gameId !== 'random') { socket.send( @@ -143,47 +146,75 @@ export const Game = () => { if (!socket) return
Connecting...
; if (!socket) return
Connecting...
; - return
- {result &&
- {result} -
} -
-
-
-
-
-
-
- -
-
- -
-
- -
-
-
-
-
- {!started && -
- {added ?
Waiting
: gameId === "random" && } -
} -
- {moves.length > 0 &&
} -
-
+ return ( +
+ {result && ( +
{result}
+ )} +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ {!started && ( +
+ {added ? ( +
Waiting
+ ) : ( + gameId === 'random' && ( + + ) + )}
+ )} +
+ {moves.length > 0 && ( +
+ +
+ )} +
- {/* */} +
+ {/* */} +
); }; diff --git a/apps/frontend/src/screens/Login.tsx b/apps/frontend/src/screens/Login.tsx index eb91c78a..2ec0f273 100644 --- a/apps/frontend/src/screens/Login.tsx +++ b/apps/frontend/src/screens/Login.tsx @@ -5,8 +5,6 @@ import { useRef } from 'react'; import { useRecoilState } from 'recoil'; import { userAtom } from '@repo/store/userAtom'; -const BACKEND_URL = - import.meta.env.VITE_APP_BACKEND_URL ?? 'http://localhost:3000'; const BACKEND_URL = import.meta.env.VITE_APP_BACKEND_URL ?? 'http://localhost:3000'; From 3ea1a8a6b2cc7f7ad7f32fb131f5f23c05600abb Mon Sep 17 00:00:00 2001 From: N4r35h Date: Sun, 21 Apr 2024 12:22:18 +0530 Subject: [PATCH 04/12] fix merge mistakes --- apps/frontend/src/screens/Login.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/frontend/src/screens/Login.tsx b/apps/frontend/src/screens/Login.tsx index 2ec0f273..0355b6d3 100644 --- a/apps/frontend/src/screens/Login.tsx +++ b/apps/frontend/src/screens/Login.tsx @@ -15,7 +15,6 @@ const Login = () => { const google = () => { window.open(`${BACKEND_URL}/auth/google`, '_self'); - window.open(`${BACKEND_URL}/auth/google`, '_self'); }; const github = () => { @@ -39,9 +38,6 @@ const Login = () => { return (
-

- Enter the Game World -

Enter the Game World

From e50dd34cb602b8be050f0b51b835c95aeca6da96 Mon Sep 17 00:00:00 2001 From: N4r35h Date: Sun, 21 Apr 2024 13:03:50 +0530 Subject: [PATCH 05/12] persist guest session based on a cookie --- apps/backend/package.json | 2 ++ apps/backend/src/index.ts | 6 +++++- apps/backend/src/router/auth.ts | 23 +++++++++++++++++++++++ apps/frontend/src/screens/Login.tsx | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 4c76d19d..d592ad00 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -13,7 +13,9 @@ "license": "ISC", "dependencies": { "@repo/db": "*", + "@types/cookie-parser": "^1.4.7", "@types/express-session": "^1.18.0", + "cookie-parser": "^1.4.6", "cookie-session": "^2.1.0", "cors": "^2.8.5", "express": "^4.19.2", diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index f8829191..cd2834e4 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -6,17 +6,21 @@ import authRoute from './router/auth'; import dotenv from 'dotenv'; import session from 'express-session'; import passport from 'passport'; +import cookieParser from 'cookie-parser'; + +export const COOKIE_MAX_AGE = 24 * 60 * 60 * 1000; const app = express(); dotenv.config(); app.use(express.json()); +app.use(cookieParser()); app.use( session({ secret: process.env.COOKIE_SECRET || 'keyboard cat', resave: false, saveUninitialized: false, - cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 }, + cookie: { secure: false, maxAge: COOKIE_MAX_AGE }, }), ); diff --git a/apps/backend/src/router/auth.ts b/apps/backend/src/router/auth.ts index 81e4cdea..9f20e45a 100644 --- a/apps/backend/src/router/auth.ts +++ b/apps/backend/src/router/auth.ts @@ -3,12 +3,19 @@ import passport from 'passport'; import jwt from 'jsonwebtoken'; import { db } from '../db'; import { v4 as uuidv4 } from 'uuid'; +import { COOKIE_MAX_AGE } from '..'; const router = Router(); const CLIENT_URL = process.env.AUTH_REDIRECT_URL ?? 'http://localhost:5173/game/random'; const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key'; +interface userJwtClaims { + userId: string; + name: string; + isGuest?: boolean; +} + interface User { id: string; token?: string; @@ -40,6 +47,7 @@ router.post('/guest', async (req: Request, res: Response) => { token: token, isGuest: true, }; + res.cookie('guest', token, { maxAge: COOKIE_MAX_AGE }); res.json(User); }); @@ -62,6 +70,20 @@ router.get('/refresh', async (req: Request, res: Response) => { id: user.id, name: userDb?.name, }); + } else if (req.cookies && req.cookies.guest) { + const decoded = jwt.verify(req.cookies.guest, JWT_SECRET) as userJwtClaims; + const token = jwt.sign( + { userId: decoded.userId, name: decoded.name, isGuest: true }, + JWT_SECRET, + ); + let User: User = { + id: decoded.userId, + name: decoded.name, + token: token, + isGuest: true, + }; + res.cookie('guest', token, { maxAge: COOKIE_MAX_AGE }); + res.json(User); } else { res.status(401).json({ success: false, message: 'Unauthorized' }); } @@ -72,6 +94,7 @@ router.get('/login/failed', (req: Request, res: Response) => { }); router.get('/logout', (req: Request, res: Response) => { + res.clearCookie('guest'); req.logout((err) => { if (err) { console.error('Error logging out:', err); diff --git a/apps/frontend/src/screens/Login.tsx b/apps/frontend/src/screens/Login.tsx index 0355b6d3..edd9c793 100644 --- a/apps/frontend/src/screens/Login.tsx +++ b/apps/frontend/src/screens/Login.tsx @@ -27,6 +27,7 @@ const Login = () => { headers: { 'Content-Type': 'application/json', }, + credentials: 'include', body: JSON.stringify({ name: (guestName.current && guestName.current.value) || '', }), From dbc387aa2037ba3bfb5106daf0b248701fb674c2 Mon Sep 17 00:00:00 2001 From: N4r35h Date: Mon, 22 Apr 2024 04:11:29 +0530 Subject: [PATCH 06/12] revert changes in Game.ts and GameManager.ts that changed userId expecting parts to expect user/player object --- apps/ws/src/Game.ts | 55 ++++++++++++++++++++------------------ apps/ws/src/GameManager.ts | 16 ++++------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/apps/ws/src/Game.ts b/apps/ws/src/Game.ts index d33a34a6..e56543eb 100644 --- a/apps/ws/src/Game.ts +++ b/apps/ws/src/Game.ts @@ -11,6 +11,7 @@ import { import { db } from './db'; import { randomUUID } from 'crypto'; import { SocketManager, User } from './SocketManager'; +import { AuthProvider } from '@prisma/client'; export function isPromoting(chess: Chess, from: Square, to: Square) { if (!from) { @@ -37,17 +38,10 @@ export function isPromoting(chess: Chess, from: Square, to: Square) { .includes(to); } -export interface Player { - id?: string; - userId: string; - name: string; - isGuest?: boolean; -} - export class Game { public gameId: string; - public player1: Player; - public player2: Player | null; + public player1UserId: string; + public player2UserId: string | null; public board: Chess; private startTime: Date; private moveCount = 0; @@ -57,18 +51,24 @@ export class Game { private gameStartTime: number = 0; private tempTime: number = 0; - constructor(player1: Player, player2: Player | null) { - this.player1 = player1; - this.player2 = player2; + constructor(player1UserId: string, player2UserId: string | null) { + this.player1UserId = player1UserId; + this.player2UserId = player2UserId; this.board = new Chess(); this.startTime = new Date(); this.gameId = randomUUID(); } - async updateSecondPlayer(player2: Player) { - this.player2 = player2; - let player1Name = this.player1.name; - let player2Name = this.player2?.name; + async updateSecondPlayer(player2UserId: string) { + this.player2UserId = player2UserId; + + const users = await db.user.findMany({ + where: { + id: { + in: [this.player1UserId, this.player2UserId ?? ''], + }, + }, + }); try { await this.createGameInDb(); @@ -77,6 +77,9 @@ export class Game { return; } + let WhitePlayer = users.find((user) => user.id === this.player1UserId); + let BlackPlayer = users.find((user) => user.id === this.player2UserId); + SocketManager.getInstance().broadcast( this.gameId, JSON.stringify({ @@ -84,14 +87,14 @@ export class Game { payload: { gameId: this.gameId, whitePlayer: { - name: player1Name, - id: this.player1.userId, - isGuest: this.player1.isGuest, + name: WhitePlayer?.name, + id: this.player1UserId, + isGuest: WhitePlayer?.provider === AuthProvider.GUEST, }, blackPlayer: { - name: player2Name, - id: this.player2.userId, - isGuest: this.player2.isGuest, + name: BlackPlayer?.name, + id: this.player2UserId, + isGuest: BlackPlayer?.provider === AuthProvider.GUEST, }, fen: this.board.fen(), moves: [], @@ -112,12 +115,12 @@ export class Game { currentFen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', whitePlayer: { connect: { - id: this.player1.userId, + id: this.player1UserId, }, }, blackPlayer: { connect: { - id: this.player2?.userId ?? '', + id: this.player2UserId ?? '', }, }, }, @@ -162,10 +165,10 @@ export class Game { }, ) { // validate the type of move using zod - if (this.moveCount % 2 === 0 && user.userId !== this.player1.userId) { + if (this.moveCount % 2 === 0 && user.userId !== this.player1UserId) { return; } - if (this.moveCount % 2 === 1 && user.userId !== this.player2?.userId) { + if (this.moveCount % 2 === 1 && user.userId !== this.player2UserId) { return; } diff --git a/apps/ws/src/GameManager.ts b/apps/ws/src/GameManager.ts index 32c2a7f2..6dc79048 100644 --- a/apps/ws/src/GameManager.ts +++ b/apps/ws/src/GameManager.ts @@ -56,7 +56,7 @@ export class GameManager { console.error('Pending game not found?'); return; } - if (user.userId === game.player1.userId) { + if (user.userId === game.player1UserId) { SocketManager.getInstance().broadcast( game.gameId, JSON.stringify({ @@ -69,10 +69,10 @@ export class GameManager { return; } SocketManager.getInstance().addUser(user, game.gameId); - await game?.updateSecondPlayer(user); + await game?.updateSecondPlayer(user.userId); this.pendingGameId = null; } else { - const game = new Game(user, null); + const game = new Game(user.userId, null); this.games.push(game); this.pendingGameId = game.gameId; SocketManager.getInstance().addUser(user, game.gameId); @@ -129,14 +129,8 @@ export class GameManager { if (!availableGame) { const game = new Game( - { - name: gameFromDb?.whitePlayer?.name || '', - userId: gameFromDb?.whitePlayerId!, - }, - { - name: gameFromDb?.blackPlayer?.name || '', - userId: gameFromDb?.blackPlayerId!, - }, + gameFromDb.whitePlayerId!, + gameFromDb.blackPlayerId!, ); gameFromDb?.moves.forEach((move) => { if ( From 8fc827ee9ec41c9f62a2899f5ca212aa2577fdca Mon Sep 17 00:00:00 2001 From: N4r35h Date: Wed, 1 May 2024 07:45:18 +0530 Subject: [PATCH 07/12] remove accidentaly added old IMove interface --- apps/frontend/src/screens/Game.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/frontend/src/screens/Game.tsx b/apps/frontend/src/screens/Game.tsx index 86a5c947..1d4d57f9 100644 --- a/apps/frontend/src/screens/Game.tsx +++ b/apps/frontend/src/screens/Game.tsx @@ -5,7 +5,7 @@ import MoveSound from '../../public/move.wav'; import { Button } from '../components/Button'; import { ChessBoard, isPromoting } from '../components/ChessBoard'; import { useSocket } from '../hooks/useSocket'; -import { Chess, Move, Square } from 'chess.js'; +import { Chess, Move } from 'chess.js'; import { useNavigate, useParams } from 'react-router-dom'; import MovesTable from '../components/MovesTable'; import { useUser } from '@repo/store/useUser'; @@ -25,12 +25,6 @@ export const GAME_TIME = 'game_time'; const GAME_TIME_MS = 10 * 60 * 1000; -export interface IMove { - from: Square; - to: Square; - piece: string; -} - export interface Player { id: string; name: string; From 9986dc5b206e1274a773fb4d2890601a83ce3eb5 Mon Sep 17 00:00:00 2001 From: N4r35h Date: Thu, 20 Jun 2024 18:56:18 +0530 Subject: [PATCH 08/12] change interfacename from User to UserDetails --- apps/backend/src/router/auth.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/router/auth.ts b/apps/backend/src/router/auth.ts index 83f4cdf0..4752badc 100644 --- a/apps/backend/src/router/auth.ts +++ b/apps/backend/src/router/auth.ts @@ -16,7 +16,7 @@ interface userJwtClaims { isGuest?: boolean; } -interface User { +interface UserDetails { id: string; token?: string; name: string; @@ -41,19 +41,19 @@ router.post('/guest', async (req: Request, res: Response) => { { userId: user.id, name: user.name, isGuest: true }, JWT_SECRET, ); - let User: User = { + const UserDetails: UserDetails = { id: user.id, name: user.name!, token: token, isGuest: true, }; res.cookie('guest', token, { maxAge: COOKIE_MAX_AGE }); - res.json(User); + res.json(UserDetails); }); router.get('/refresh', async (req: Request, res: Response) => { if (req.user) { - const user = req.user as User; + const user = req.user as UserDetails; // Token is issued so it can be shared b/w HTTP and ws server // Todo: Make this temporary and add refresh logic here @@ -76,7 +76,7 @@ router.get('/refresh', async (req: Request, res: Response) => { { userId: decoded.userId, name: decoded.name, isGuest: true }, JWT_SECRET, ); - let User: User = { + let User: UserDetails = { id: decoded.userId, name: decoded.name, token: token, From 1b7a018eb9019c95547e5b40c3081bf52a503abc Mon Sep 17 00:00:00 2001 From: N4r35h Date: Thu, 20 Jun 2024 19:01:28 +0530 Subject: [PATCH 09/12] move constant to a seperate file --- apps/backend/src/consts.ts | 1 + apps/backend/src/index.ts | 3 +-- apps/backend/src/router/auth.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 apps/backend/src/consts.ts diff --git a/apps/backend/src/consts.ts b/apps/backend/src/consts.ts new file mode 100644 index 00000000..71037200 --- /dev/null +++ b/apps/backend/src/consts.ts @@ -0,0 +1 @@ +export const COOKIE_MAX_AGE = 24 * 60 * 60 * 1000; \ No newline at end of file diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index cd2834e4..f0c00637 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -7,8 +7,7 @@ import dotenv from 'dotenv'; import session from 'express-session'; import passport from 'passport'; import cookieParser from 'cookie-parser'; - -export const COOKIE_MAX_AGE = 24 * 60 * 60 * 1000; +import { COOKIE_MAX_AGE } from './consts'; const app = express(); diff --git a/apps/backend/src/router/auth.ts b/apps/backend/src/router/auth.ts index 4752badc..e12a4fca 100644 --- a/apps/backend/src/router/auth.ts +++ b/apps/backend/src/router/auth.ts @@ -3,7 +3,7 @@ import passport from 'passport'; import jwt from 'jsonwebtoken'; import { db } from '../db'; import { v4 as uuidv4 } from 'uuid'; -import { COOKIE_MAX_AGE } from '..'; +import { COOKIE_MAX_AGE } from '../consts'; const router = Router(); const CLIENT_URL = From a795e07c4a825edfbbac85d1a823289f70babe0b Mon Sep 17 00:00:00 2001 From: N4r35h Date: Thu, 20 Jun 2024 19:04:04 +0530 Subject: [PATCH 10/12] change let WhitePlayer,BlackPlayer to consts --- apps/ws/src/Game.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ws/src/Game.ts b/apps/ws/src/Game.ts index 7e14bd4e..718d9a93 100644 --- a/apps/ws/src/Game.ts +++ b/apps/ws/src/Game.ts @@ -125,10 +125,10 @@ export class Game { return; } - let WhitePlayer = users.find((user) => user.id === this.player1UserId); - let BlackPlayer = users.find((user) => user.id === this.player2UserId); + const WhitePlayer = users.find((user) => user.id === this.player1UserId); + const BlackPlayer = users.find((user) => user.id === this.player2UserId); - SocketManager.getInstance().broadcast( + socketManager.broadcast( this.gameId, JSON.stringify({ type: INIT_GAME, From 5a14881b0ec3ca13e8d629ccb2956719493d3ce1 Mon Sep 17 00:00:00 2001 From: N4r35h Date: Thu, 20 Jun 2024 19:05:05 +0530 Subject: [PATCH 11/12] ensure camelCase --- apps/ws/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ws/src/index.ts b/apps/ws/src/index.ts index 456f8a87..288c42e1 100644 --- a/apps/ws/src/index.ts +++ b/apps/ws/src/index.ts @@ -10,8 +10,8 @@ const gameManager = new GameManager(); wss.on('connection', function connection(ws, req) { //@ts-ignore const token: string = url.parse(req.url, true).query.token; - const User = extractAuthUser(token, ws); - gameManager.addUser(User); + const user = extractAuthUser(token, ws); + gameManager.addUser(user); ws.on('close', () => { gameManager.removeUser(ws); From 20bf0309c4d941b49fb4bbf7fab01c4d1c967145 Mon Sep 17 00:00:00 2001 From: N4r35h Date: Mon, 1 Jul 2024 22:44:33 +0530 Subject: [PATCH 12/12] move @types/cookie-parser from dep to devDep --- apps/backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 08057759..fb6e1953 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -13,7 +13,6 @@ "license": "ISC", "dependencies": { "@repo/db": "*", - "@types/cookie-parser": "^1.4.7", "@types/express-session": "^1.18.0", "cookie-parser": "^1.4.6", "cookie-session": "^2.1.0", @@ -28,6 +27,7 @@ "uuid": "^9.0.1" }, "devDependencies": { + "@types/cookie-parser": "^1.4.7", "@types/cookie-session": "^2.0.49", "@types/cors": "^2.8.17", "@types/express": "^4.17.21",