Skip to content

Commit e718e13

Browse files
authored
Merge pull request #115 from N4r35h/main
Add basic Guest play support
2 parents 394065b + 20bf030 commit e718e13

File tree

16 files changed

+182
-40
lines changed

16 files changed

+182
-40
lines changed

apps/backend/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"dependencies": {
1717
"@repo/db": "*",
1818
"@types/express-session": "^1.18.0",
19+
"cookie-parser": "^1.4.6",
1920
"cookie-session": "^2.1.0",
2021
"cors": "^2.8.5",
2122
"dotenv": "^16.4.5",
@@ -24,9 +25,11 @@
2425
"jsonwebtoken": "^9.0.2",
2526
"passport": "^0.7.0",
2627
"passport-github2": "^0.1.12",
27-
"passport-google-oauth20": "^2.0.0"
28+
"passport-google-oauth20": "^2.0.0",
29+
"uuid": "^9.0.1"
2830
},
2931
"devDependencies": {
32+
"@types/cookie-parser": "^1.4.7",
3033
"@types/cookie-session": "^2.0.49",
3134
"@types/cors": "^2.8.17",
3235
"@types/express": "^4.17.21",

apps/backend/src/consts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const COOKIE_MAX_AGE = 24 * 60 * 60 * 1000;

apps/backend/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ import authRoute from './router/auth';
66
import dotenv from 'dotenv';
77
import session from 'express-session';
88
import passport from 'passport';
9+
import cookieParser from 'cookie-parser';
10+
import { COOKIE_MAX_AGE } from './consts';
911

1012
const app = express();
1113

1214
dotenv.config();
15+
app.use(express.json());
16+
app.use(cookieParser());
1317
app.use(
1418
session({
1519
secret: process.env.COOKIE_SECRET || 'keyboard cat',
1620
resave: false,
1721
saveUninitialized: false,
18-
cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 },
22+
cookie: { secure: false, maxAge: COOKIE_MAX_AGE },
1923
}),
2024
);
2125

apps/backend/src/router/auth.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,58 @@ import { Request, Response, Router } from 'express';
22
import passport from 'passport';
33
import jwt from 'jsonwebtoken';
44
import { db } from '../db';
5+
import { v4 as uuidv4 } from 'uuid';
6+
import { COOKIE_MAX_AGE } from '../consts';
57
const router = Router();
68

79
const CLIENT_URL =
810
process.env.AUTH_REDIRECT_URL ?? 'http://localhost:5173/game/random';
911
const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key';
1012

11-
interface User {
13+
interface userJwtClaims {
14+
userId: string;
15+
name: string;
16+
isGuest?: boolean;
17+
}
18+
19+
interface UserDetails {
1220
id: string;
21+
token?: string;
22+
name: string;
23+
isGuest?: boolean;
1324
}
1425

26+
// this route is to be hit when the user wants to login as a guest
27+
router.post('/guest', async (req: Request, res: Response) => {
28+
const bodyData = req.body;
29+
let guestUUID = 'guest-' + uuidv4();
30+
31+
const user = await db.user.create({
32+
data: {
33+
username: guestUUID,
34+
email: guestUUID + '@chess100x.com',
35+
name: bodyData.name || guestUUID,
36+
provider: 'GUEST',
37+
},
38+
});
39+
40+
const token = jwt.sign(
41+
{ userId: user.id, name: user.name, isGuest: true },
42+
JWT_SECRET,
43+
);
44+
const UserDetails: UserDetails = {
45+
id: user.id,
46+
name: user.name!,
47+
token: token,
48+
isGuest: true,
49+
};
50+
res.cookie('guest', token, { maxAge: COOKIE_MAX_AGE });
51+
res.json(UserDetails);
52+
});
53+
1554
router.get('/refresh', async (req: Request, res: Response) => {
1655
if (req.user) {
17-
const user = req.user as User;
56+
const user = req.user as UserDetails;
1857

1958
// Token is issued so it can be shared b/w HTTP and ws server
2059
// Todo: Make this temporary and add refresh logic here
@@ -25,12 +64,26 @@ router.get('/refresh', async (req: Request, res: Response) => {
2564
},
2665
});
2766

28-
const token = jwt.sign({ userId: user.id }, JWT_SECRET);
67+
const token = jwt.sign({ userId: user.id, name: userDb?.name }, JWT_SECRET);
2968
res.json({
3069
token,
3170
id: user.id,
3271
name: userDb?.name,
3372
});
73+
} else if (req.cookies && req.cookies.guest) {
74+
const decoded = jwt.verify(req.cookies.guest, JWT_SECRET) as userJwtClaims;
75+
const token = jwt.sign(
76+
{ userId: decoded.userId, name: decoded.name, isGuest: true },
77+
JWT_SECRET,
78+
);
79+
let User: UserDetails = {
80+
id: decoded.userId,
81+
name: decoded.name,
82+
token: token,
83+
isGuest: true,
84+
};
85+
res.cookie('guest', token, { maxAge: COOKIE_MAX_AGE });
86+
res.json(User);
3487
} else {
3588
res.status(401).json({ success: false, message: 'Unauthorized' });
3689
}
@@ -41,6 +94,7 @@ router.get('/login/failed', (req: Request, res: Response) => {
4194
});
4295

4396
router.get('/logout', (req: Request, res: Response) => {
97+
res.clearCookie('guest');
4498
req.logout((err) => {
4599
if (err) {
46100
console.error('Error logging out:', err);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Player } from "../screens/Game"
2+
3+
interface PlayerTitleProps {
4+
player: Player | undefined
5+
isSelf?: boolean
6+
}
7+
8+
export const PlayerTitle = ({player, isSelf}: PlayerTitleProps) => {
9+
return (
10+
<div className="flex gap-1">
11+
{player && player.id.startsWith("guest") &&
12+
<p className="text-gray-500">
13+
[Guest]
14+
</p>
15+
}
16+
<p>{player && player.name}</p>
17+
{isSelf &&
18+
<p className="text-gray-500">(You)</p>
19+
}
20+
</div>
21+
)
22+
}
Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1-
export const UserAvatar = ({ name }: { name: string }) => {
2-
return <div className="text-white">{name}</div>;
1+
import { useUser } from '@repo/store/useUser';
2+
import { Metadata, Player } from '../screens/Game';
3+
4+
interface UserAvatarProps {
5+
gameMetadata: Metadata | null;
6+
self?: boolean;
7+
}
8+
9+
export const UserAvatar = ({ gameMetadata, self }: UserAvatarProps) => {
10+
const user = useUser();
11+
let player: Player;
12+
if (gameMetadata?.blackPlayer.id === user.id) {
13+
player = self ? gameMetadata.blackPlayer : gameMetadata.whitePlayer;
14+
} else {
15+
player = self ? gameMetadata?.whitePlayer! : gameMetadata?.blackPlayer!;
16+
}
17+
18+
return (
19+
<div className="text-white flex gap-2 ">
20+
<p>{player?.name}</p>
21+
{player?.isGuest && <p className="text-gray-500">[Guest]</p>}
22+
</div>
23+
);
324
};

apps/frontend/src/screens/Game.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export interface GameResult {
3636

3737
const GAME_TIME_MS = 10 * 60 * 1000;
3838

39+
export interface Player {
40+
id: string;
41+
name: string;
42+
isGuest: boolean;
43+
}
3944
import { useRecoilValue, useSetRecoilState } from 'recoil';
4045

4146
import { movesAtom, userSelectedMoveIndexAtom } from '@repo/store/chessBoard';
@@ -46,9 +51,9 @@ import ExitGameModel from '@/components/ExitGameModel';
4651

4752
const moveAudio = new Audio(MoveSound);
4853

49-
interface Metadata {
50-
blackPlayer: { id: string; name: string };
51-
whitePlayer: { id: string; name: string };
54+
export interface Metadata {
55+
blackPlayer: Player;
56+
whitePlayer: Player;
5257
}
5358

5459
export const Game = () => {
@@ -267,13 +272,7 @@ export const Game = () => {
267272
{started && (
268273
<div className="mb-4">
269274
<div className="flex justify-between">
270-
<UserAvatar
271-
name={
272-
user.id === gameMetadata?.whitePlayer?.id
273-
? gameMetadata?.blackPlayer?.name
274-
: gameMetadata?.whitePlayer?.name ?? ''
275-
}
276-
/>
275+
<UserAvatar gameMetadata={gameMetadata} />
277276
{getTimer(
278277
user.id === gameMetadata?.whitePlayer?.id
279278
? player2TimeConsumed
@@ -299,13 +298,7 @@ export const Game = () => {
299298
</div>
300299
{started && (
301300
<div className="mt-4 flex justify-between">
302-
<UserAvatar
303-
name={
304-
user.id === gameMetadata?.blackPlayer?.id
305-
? gameMetadata?.blackPlayer?.name
306-
: gameMetadata?.whitePlayer?.name ?? ''
307-
}
308-
/>
301+
<UserAvatar gameMetadata={gameMetadata} self />
309302
{getTimer(
310303
user.id === gameMetadata?.blackPlayer?.id
311304
? player2TimeConsumed

apps/frontend/src/screens/Login.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { useNavigate } from 'react-router-dom';
2+
import { useRef } from 'react';
3+
import { useRecoilState } from 'recoil';
4+
import { userAtom } from '@repo/store/userAtom';
25

36
const BACKEND_URL =
47
import.meta.env.VITE_APP_BACKEND_URL ?? 'http://localhost:3000';
58

69
const Login = () => {
710
const navigate = useNavigate();
11+
const guestName = useRef<HTMLInputElement>(null);
12+
const [_, setUser] = useRecoilState(userAtom);
813

914
const google = () => {
1015
window.open(`${BACKEND_URL}/auth/google`, '_self');
@@ -14,6 +19,22 @@ const Login = () => {
1419
window.open(`${BACKEND_URL}/auth/github`, '_self');
1520
};
1621

22+
const loginAsGuest = async () => {
23+
const response = await fetch(`${BACKEND_URL}/auth/guest`, {
24+
method: 'POST',
25+
headers: {
26+
'Content-Type': 'application/json',
27+
},
28+
credentials: 'include',
29+
body: JSON.stringify({
30+
name: (guestName.current && guestName.current.value) || '',
31+
}),
32+
});
33+
const user = await response.json();
34+
setUser(user);
35+
navigate('/game/random');
36+
};
37+
1738
return (
1839
<div className="flex flex-col items-center justify-center h-screen text-textMain">
1940
<h1 className="text-4xl font-bold mb-8 text-center text-green-500 drop-shadow-lg">
@@ -44,12 +65,13 @@ const Login = () => {
4465
</div>
4566
<input
4667
type="text"
68+
ref={guestName}
4769
placeholder="Username"
4870
className="border px-4 py-2 rounded-md mb-4 w-full md:w-64"
4971
/>
5072
<button
5173
className="bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600 transition-colors duration-300"
52-
onClick={() => navigate('/game/random')}
74+
onClick={() => loginAsGuest()}
5375
>
5476
Enter as guest
5577
</button>

apps/ws/src/Game.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,19 +125,24 @@ export class Game {
125125
return;
126126
}
127127

128+
const WhitePlayer = users.find((user) => user.id === this.player1UserId);
129+
const BlackPlayer = users.find((user) => user.id === this.player2UserId);
130+
128131
socketManager.broadcast(
129132
this.gameId,
130133
JSON.stringify({
131134
type: INIT_GAME,
132135
payload: {
133136
gameId: this.gameId,
134137
whitePlayer: {
135-
name: users.find((user) => user.id === this.player1UserId)?.name,
138+
name: WhitePlayer?.name,
136139
id: this.player1UserId,
140+
isGuest: WhitePlayer?.provider === AuthProvider.GUEST,
137141
},
138142
blackPlayer: {
139-
name: users.find((user) => user.id === this.player2UserId)?.name,
143+
name: BlackPlayer?.name,
140144
id: this.player2UserId,
145+
isGuest: BlackPlayer?.provider === AuthProvider.GUEST,
141146
},
142147
fen: this.board.fen(),
143148
moves: [],

apps/ws/src/GameManager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class GameManager {
9494
const game = this.games.find((game) => game.gameId === gameId);
9595
if (game) {
9696
game.makeMove(user, message.payload.move);
97-
if (game.result) {
97+
if (game.result) {
9898
this.removeGame(game.gameId);
9999
}
100100
}
@@ -172,9 +172,9 @@ export class GameManager {
172172
gameFromDb?.whitePlayerId!,
173173
gameFromDb?.blackPlayerId!,
174174
gameFromDb.id,
175-
gameFromDb.startAt
175+
gameFromDb.startAt,
176176
);
177-
game.seedMoves(gameFromDb?.moves || [])
177+
game.seedMoves(gameFromDb?.moves || []);
178178
this.games.push(game);
179179
availableGame = game;
180180
}

0 commit comments

Comments
 (0)