From 9987c5f9843e90164fb98f1b8f4646d210a79269 Mon Sep 17 00:00:00 2001 From: adi790uu Date: Thu, 9 May 2024 16:44:15 +0530 Subject: [PATCH] Implemented settings page and better error handling --- apps/backend/src/controllers/auth.ts | 131 +++++++++++++ apps/backend/src/index.ts | 1 + apps/backend/src/middleware/errorHandling.ts | 21 ++ apps/backend/src/router/auth.ts | 62 ++---- apps/backend/src/utils/error.ts | 16 ++ apps/backend/src/utils/handlers.ts | 12 ++ apps/backend/src/utils/statusCodes.ts | 10 + apps/frontend/package.json | 1 + apps/frontend/src/App.tsx | 12 +- apps/frontend/src/components/Card.tsx | 6 +- apps/frontend/src/components/GameSettings.tsx | 149 ++++++++++++++ .../src/components/ProfileSettings.tsx | 182 ++++++++++++++++++ .../src/components/constants/side-nav.tsx | 2 +- apps/frontend/src/components/side-nav.tsx | 8 +- apps/frontend/src/components/sidebar.tsx | 8 +- apps/frontend/src/screens/Game.tsx | 23 +-- apps/frontend/src/screens/Landing.tsx | 6 +- apps/frontend/src/screens/Settings.tsx | 51 +++++ apps/frontend/src/utils/formatResult.ts | 12 ++ apps/frontend/src/utils/formatTime.ts | 21 ++ .../20240509092438_init/migration.sql | 17 ++ packages/db/prisma/schema.prisma | 6 +- packages/store/src/atoms/user.ts | 32 +++ packages/store/src/hooks/useUser.ts | 7 +- 24 files changed, 710 insertions(+), 86 deletions(-) create mode 100644 apps/backend/src/controllers/auth.ts create mode 100644 apps/backend/src/middleware/errorHandling.ts create mode 100644 apps/backend/src/utils/error.ts create mode 100644 apps/backend/src/utils/handlers.ts create mode 100644 apps/backend/src/utils/statusCodes.ts create mode 100644 apps/frontend/src/components/GameSettings.tsx create mode 100644 apps/frontend/src/components/ProfileSettings.tsx create mode 100644 apps/frontend/src/screens/Settings.tsx create mode 100644 apps/frontend/src/utils/formatResult.ts create mode 100644 apps/frontend/src/utils/formatTime.ts create mode 100644 packages/db/prisma/migrations/20240509092438_init/migration.sql diff --git a/apps/backend/src/controllers/auth.ts b/apps/backend/src/controllers/auth.ts new file mode 100644 index 00000000..62e03249 --- /dev/null +++ b/apps/backend/src/controllers/auth.ts @@ -0,0 +1,131 @@ +import { Request, Response, Router } from 'express'; +import jwt from 'jsonwebtoken'; +import { db } from '../db'; +import { AppError } from '../utils/error'; + +interface User { + id: string; +} + +const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key'; + +export const getToken = async (req: Request, res: Response) => { + if (req.user) { + const user = req.user as User; + + // 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({ + where: { + id: user.id, + }, + }); + + const token = jwt.sign({ userId: user.id }, JWT_SECRET); + return res.json({ + token, + id: user.id, + name: userDb?.name, + }); + } else { + throw new AppError({ name: 'UNAUTHORIZED', message: 'Unauthorized!' }); + } +}; + +export const getUserInfo = async (req: Request, res: Response) => { + if (req.user) { + const user = req.user as User; + const userDb = await db.user.findFirst({ + where: { + id: user.id, + }, + include: { + gamesAsBlack: { + include: { + whitePlayer: true, + blackPlayer: true, + }, + }, + gamesAsWhite: { + include: { + whitePlayer: true, + blackPlayer: true, + }, + }, + }, + }); + + return res.status(200).json({ + email: userDb?.email, + rating: userDb?.rating, + username: userDb?.username, + name: userDb?.name, + gamesAsBlack: userDb?.gamesAsBlack, + gamesAsWhite: userDb?.gamesAsWhite, + }); + } else { + throw new AppError({ name: 'UNAUTHORIZED', message: 'Unauthorized!' }); + } +}; + +export const deleteAccount = async (req: Request, res: Response) => { + if (req.user) { + const user = req.user as User; + const deletedUser = await db.user.delete({ + where: { + id: user.id, + }, + }); + + if (deletedUser) { + return res.status(200).json({ success: true }); + } + throw new AppError({ + name: 'INTERNAL_SERVER_ERROR', + message: 'Some error occurred!', + }); + } +}; + +export const updateUserInfo = async (req: Request, res: Response) => { + if (req.user) { + const user = req.user as User; + const { username } = req.body; + const updatedUser = await db.user.update({ + where: { + id: user.id, + }, + data: { + username: username, + }, + }); + + if (updatedUser) { + return res.status(200).json({ success: true }); + } + + throw new AppError({ + name: 'INTERNAL_SERVER_ERROR', + message: 'Some error occurred!', + }); + } else { + throw new AppError({ name: 'UNAUTHORIZED', message: 'Unauthorized!' }); + } +}; + +export const logout = async (req: Request, res: Response) => { + req.logout((err) => { + if (err) { + console.error('Error logging out:', err); + res.status(500).json({ error: 'Failed to log out' }); + } else { + res.clearCookie('jwt'); + res.redirect('http://localhost:5173/'); + } + }); +}; + +export const fail = async (req: Request, res: Response) => { + throw new AppError({ name: 'UNAUTHORIZED', message: 'Failed!' }); +}; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 474ac707..68d1ad75 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -20,6 +20,7 @@ app.use( ); initPassport(); +app.use(express.json()); app.use(passport.initialize()); app.use(passport.authenticate('session')); diff --git a/apps/backend/src/middleware/errorHandling.ts b/apps/backend/src/middleware/errorHandling.ts new file mode 100644 index 00000000..61bbb69e --- /dev/null +++ b/apps/backend/src/middleware/errorHandling.ts @@ -0,0 +1,21 @@ +import { NextFunction, Request, Response } from 'express'; +import { AppError } from '../utils/error'; +import { HttpStatus } from '../utils/statusCodes'; + +export const errorHandler = ( + err: Error, + req: Request, + res: Response, + next: NextFunction, +) => { + if (err instanceof AppError) { + console.log(err); + return res + .status(err.statusCode) + .json({ success: false, error: err.message }); + } + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + success: false, + error: err, + }); +}; diff --git a/apps/backend/src/router/auth.ts b/apps/backend/src/router/auth.ts index ad328713..20adcc09 100644 --- a/apps/backend/src/router/auth.ts +++ b/apps/backend/src/router/auth.ts @@ -1,56 +1,26 @@ import { Request, Response, Router } from 'express'; import passport from 'passport'; -import jwt from 'jsonwebtoken'; -import { db } from '../db'; +import { asyncHandler } from '../utils/handlers'; +import { + deleteAccount, + fail, + getToken, + getUserInfo, + logout, + updateUserInfo, +} from '../controllers/auth'; + 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 User { - id: string; -} - -router.get('/refresh', async (req: Request, res: Response) => { - if (req.user) { - const user = req.user as User; - - // 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({ - where: { - id: user.id, - }, - }); - - const token = jwt.sign({ userId: user.id }, JWT_SECRET); - res.json({ - token, - id: user.id, - name: userDb?.name, - }); - } else { - res.status(401).json({ success: false, message: 'Unauthorized' }); - } -}); - -router.get('/login/failed', (req: Request, res: Response) => { - res.status(401).json({ success: false, message: 'failure' }); -}); -router.get('/logout', (req: Request, res: Response) => { - req.logout((err) => { - if (err) { - console.error('Error logging out:', err); - res.status(500).json({ error: 'Failed to log out' }); - } else { - res.clearCookie('jwt'); - res.redirect('http://localhost:5173/'); - } - }); -}); +router.get('/refresh', asyncHandler(getToken)); +router.get('/userInfo', asyncHandler(getUserInfo)); +router.get('/login/failed', asyncHandler(fail)); +router.get('/logout', asyncHandler(logout)); +router.post('/updateUser', asyncHandler(updateUserInfo)); +router.delete('/deleteAccount', asyncHandler(deleteAccount)); router.get( '/google', diff --git a/apps/backend/src/utils/error.ts b/apps/backend/src/utils/error.ts new file mode 100644 index 00000000..ca53aac2 --- /dev/null +++ b/apps/backend/src/utils/error.ts @@ -0,0 +1,16 @@ +import { HttpStatus } from './statusCodes'; +type AppErrorParameters = { + name: keyof typeof HttpStatus; + message: string; +}; +export class AppError extends Error { + message: string; + statusCode: number; + + constructor({ name, message }: AppErrorParameters) { + const statusCode = HttpStatus[name]; + super(message); + this.statusCode = statusCode; + this.message = message; + } +} diff --git a/apps/backend/src/utils/handlers.ts b/apps/backend/src/utils/handlers.ts new file mode 100644 index 00000000..59638b47 --- /dev/null +++ b/apps/backend/src/utils/handlers.ts @@ -0,0 +1,12 @@ +import { Request, Response, NextFunction } from 'express'; + +type AsyncHandlerCallback = ( + req: Request, + res: Response, + next: NextFunction, +) => any; +export function asyncHandler(cb: AsyncHandlerCallback) { + return (req: Request, res: Response, next: NextFunction) => { + return cb(req, res, next)?.catch(next); + }; +} diff --git a/apps/backend/src/utils/statusCodes.ts b/apps/backend/src/utils/statusCodes.ts new file mode 100644 index 00000000..10fbf0a2 --- /dev/null +++ b/apps/backend/src/utils/statusCodes.ts @@ -0,0 +1,10 @@ +export const HttpStatus = { + OK: 200, + INTERNAL_SERVER_ERROR: 500, + UNAUTHORIZED: 401, + UNPROCESSABLE_ENTITY: 422, + NOT_FOUND: 404, + TO_MANY_REQUEST: 429, + BAD_REQUEST: 400, + CONFLICT: 409, +} as const; diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 51d2eef1..54b7c735 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-slot": "^1.0.2", "@repo/store": "*", "@repo/ui": "*", + "axios": "^1.6.8", "chess.js": "^1.0.0-beta.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index e0a1abfa..b4b538d2 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { RecoilRoot } from 'recoil'; import { useUser } from '@repo/store/useUser'; import { Loader } from './components/Loader'; import { Layout } from './layout'; +import Settings from './screens/Settings'; function App() { return ( @@ -27,14 +28,9 @@ function AuthApp() { } />} /> - } - /> - } />} - /> + } /> + } />} /> + } />} /> ); diff --git a/apps/frontend/src/components/Card.tsx b/apps/frontend/src/components/Card.tsx index 7967bd4c..b14ddede 100644 --- a/apps/frontend/src/components/Card.tsx +++ b/apps/frontend/src/components/Card.tsx @@ -5,7 +5,7 @@ import { CardDescription, CardHeader, CardTitle, -} from '@/components/ui/card'; +} from '../components/ui/card'; import chessIcon from '../../public/chess.png'; import computerIcon from '../../public/computer.png'; import lightningIcon from '../../public/lightning-bolt.png'; @@ -84,13 +84,13 @@ export function PlayCard() { const navigate = useNavigate(); return ( - +

Play Chess

- chess + chess
diff --git a/apps/frontend/src/components/GameSettings.tsx b/apps/frontend/src/components/GameSettings.tsx new file mode 100644 index 00000000..185f3efe --- /dev/null +++ b/apps/frontend/src/components/GameSettings.tsx @@ -0,0 +1,149 @@ +import { formatResult } from '../utils/formatResult'; +import { formatDate } from '../utils/formatTime'; +import { GroupIcon, LightningBoltIcon } from '@radix-ui/react-icons'; +import { useState } from 'react'; + +//@ts-ignore +const GameSettings = ({ userInfo }) => { + const gamesPerPage = 5; + + const [currentPage, setCurrentPage] = useState(1); + const games = [...userInfo.gamesAsWhite, ...userInfo.gamesAsBlack]; + + const totalGames = games.length; + + let w: number = 0, + b: number = 0; + + //@ts-ignore + userInfo.gamesAsBlack.map((data) => { + if (data.result === 'BLACK_WINS') { + b++; + } + }); + + //@ts-ignore + userInfo.gamesAsWhite.map((data) => { + if (data.result === 'WHITE_WINS') { + w++; + } + }); + + const winPercentage = totalGames ? ((w / totalGames) * 100).toFixed(2) : 0; + //@ts-ignore + games.sort((a, b) => new Date(b.startAt) - new Date(a.startAt)); + + const indexOfLastGame = currentPage * gamesPerPage; + const indexOfFirstGame = indexOfLastGame - gamesPerPage; + const currentGames = games.slice(indexOfFirstGame, indexOfLastGame); + + const paginate = (pageNumber: number) => { + setCurrentPage(pageNumber); + }; + + return ( +
+
+
+
+
+ +
+
+

Game Statistics

+
+
+
+
+
+

Total Games Played

+

{totalGames}

+
+
+
+
+

Games won as White

+

{w}

+
+
+
+
+

Games won as Black

+

{b}

+
+
+
+
+

Win Percentage

+

{winPercentage}%

+
+
+
+
+ + + + + + + + + + + {currentGames.map((game, index) => ( + + + + + + + ))} + +
PlayersResultTime ControlDate
+
+ +
+
+
+
+

{game.blackPlayer.name}

+
+
+
+

{game.whitePlayer.name}

+
+
+
+

{formatResult(game.result)}

+
+

{game.timeControl}

+
+

{formatDate(game.startAt)}

+
+
+
+ {Array.from( + { length: Math.ceil(games.length / gamesPerPage) }, + (_, i) => ( + + ), + )} +
+
+ ); +}; + +export default GameSettings; diff --git a/apps/frontend/src/components/ProfileSettings.tsx b/apps/frontend/src/components/ProfileSettings.tsx new file mode 100644 index 00000000..ed353714 --- /dev/null +++ b/apps/frontend/src/components/ProfileSettings.tsx @@ -0,0 +1,182 @@ +import { useEffect, useState } from 'react'; +import { StarIcon } from '@radix-ui/react-icons'; +import { UserIcon } from 'lucide-react'; +import { TrashIcon } from '@radix-ui/react-icons'; +import axios from 'axios'; + +const BACKEND_URL = + import.meta.env.VITE_APP_BACKEND_URL ?? 'http://localhost:3000'; + +//@ts-ignore +const ProfileSettings = ({ userInfo }) => { + const [username, setUsername] = useState(userInfo?.username || ''); + const [changesSaved, setChangesSaved] = useState(false); + const [showModal, setShowModal] = useState(false); + const [confirmation, setConfirmation] = useState(false); + + const handleInputChange = ( + e: React.ChangeEvent, + setter: React.Dispatch>, + ) => { + setter(e.target.value); + setChangesSaved(false); + }; + + const deleteAccount = () => { + setShowModal(true); + }; + + useEffect(() => { + if (confirmation) { + axios + .delete(`${BACKEND_URL}/auth/deleteAccount`, { withCredentials: true }) + .then((response) => { + if (response.data.success) { + window.location.href = `${BACKEND_URL}/auth/logout`; + } + }) + .catch((error) => { + console.error('Error deleting account:', error); + }) + .finally(() => { + setShowModal(false); + }); + } + }, [confirmation]); + + const handleSaveChanges = () => { + const data = { username: username }; + axios + .post(`${BACKEND_URL}/auth/updateUser`, data, { withCredentials: true }) + .then((response) => { + if (response.data.success) { + alert('Changes saved successfully!'); + setChangesSaved(true); + } else { + alert('Failed to save changes. Please try again later.'); + } + }) + .catch((error) => { + console.error('Error saving changes:', error); + }); + }; + + return ( +
+
+ +

Profile Settings

+
+
+

+ Rating +

+ + + {userInfo?.rating} + +
+

+

+ Email + + {userInfo?.email} + +

+

+ Name + + {userInfo?.name} + +

+

+ Username + handleInputChange(e, setUsername)} + /> +

+ +
+ + +
+ {showModal && ( +
+
+ + +
+
+
+
+ +
+
+ +
+

+ Are you sure you want to delete your account? +

+
+
+
+
+
+ + +
+
+
+
+ )} +
+
+ ); +}; + +export default ProfileSettings; diff --git a/apps/frontend/src/components/constants/side-nav.tsx b/apps/frontend/src/components/constants/side-nav.tsx index 31c94f31..39c09ccd 100644 --- a/apps/frontend/src/components/constants/side-nav.tsx +++ b/apps/frontend/src/components/constants/side-nav.tsx @@ -39,7 +39,7 @@ export const LowerNavItems = [ { title: 'Settings', icon: SettingsIcon, - href: '/', + href: '/settings', color: 'text-green-500', }, ]; diff --git a/apps/frontend/src/components/side-nav.tsx b/apps/frontend/src/components/side-nav.tsx index 846b1c4d..7758c921 100644 --- a/apps/frontend/src/components/side-nav.tsx +++ b/apps/frontend/src/components/side-nav.tsx @@ -1,13 +1,13 @@ -import { cn } from '@/lib/utils'; -import { useSidebar } from '@/hooks/useSidebar'; -import { buttonVariants } from '@/components/ui/button'; +import { cn } from '../lib/utils'; +import { useSidebar } from '../hooks/useSidebar'; +import { buttonVariants } from '../components/ui/button'; import { useLocation } from 'react-router-dom'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, -} from '@/components/subnav-accordian'; +} from '../components/subnav-accordian'; import { useEffect, useState } from 'react'; import { ChevronDownIcon } from '@radix-ui/react-icons'; import { type LucideIcon } from 'lucide-react'; diff --git a/apps/frontend/src/components/sidebar.tsx b/apps/frontend/src/components/sidebar.tsx index 99eaa072..5a50ae3f 100644 --- a/apps/frontend/src/components/sidebar.tsx +++ b/apps/frontend/src/components/sidebar.tsx @@ -1,9 +1,9 @@ import { useEffect } from 'react'; -import { SideNav } from '@/components/side-nav'; -import { UpperNavItems, LowerNavItems } from '@/components/constants/side-nav'; +import { SideNav } from '../components/side-nav'; +import { UpperNavItems, LowerNavItems } from '../components/constants/side-nav'; -import { cn } from '@/lib/utils'; -import { useSidebar } from '@/hooks/useSidebar'; +import { cn } from '../lib/utils'; +import { useSidebar } from '../hooks/useSidebar'; interface SidebarProps { className?: string; } diff --git a/apps/frontend/src/screens/Game.tsx b/apps/frontend/src/screens/Game.tsx index b6f8c0e4..d0d9ea63 100644 --- a/apps/frontend/src/screens/Game.tsx +++ b/apps/frontend/src/screens/Game.tsx @@ -33,13 +33,12 @@ export interface GameResult { by: string; } - const GAME_TIME_MS = 10 * 60 * 1000; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { movesAtom, userSelectedMoveIndexAtom } from '@repo/store/chessBoard'; -import GameEndModal from '@/components/GameEndModal'; +import GameEndModal from '../components/GameEndModal'; const moveAudio = new Audio(MoveSound); @@ -60,10 +59,7 @@ export const Game = () => { const [added, setAdded] = useState(false); const [started, setStarted] = useState(false); const [gameMetadata, setGameMetadata] = useState(null); - const [result, setResult] = useState< - GameResult - | null - >(null); + const [result, setResult] = useState(null); const [player1TimeConsumed, setPlayer1TimeConsumed] = useState(0); const [player2TimeConsumed, setPlayer2TimeConsumed] = useState(0); @@ -130,8 +126,12 @@ export const Game = () => { break; case GAME_ENDED: - const wonBy = message.payload.status === 'COMPLETED' ? - message.payload.result !== 'DRAW' ? 'CheckMate' : 'Draw' : 'Timeout'; + const wonBy = + message.payload.status === 'COMPLETED' + ? message.payload.result !== 'DRAW' + ? 'CheckMate' + : 'Draw' + : 'Timeout'; setResult({ result: message.payload.result, by: wonBy, @@ -147,8 +147,7 @@ export const Game = () => { blackPlayer: message.payload.blackPlayer, whitePlayer: message.payload.whitePlayer, }); - - + break; case USER_TIMEOUT: @@ -269,9 +268,7 @@ export const Game = () => { )}
-
+
{ return (
-
+
chess-board diff --git a/apps/frontend/src/screens/Settings.tsx b/apps/frontend/src/screens/Settings.tsx new file mode 100644 index 00000000..0b18c98c --- /dev/null +++ b/apps/frontend/src/screens/Settings.tsx @@ -0,0 +1,51 @@ +import { useUser, useUserInfo } from '@repo/store/useUser'; +import { useEffect, useState } from 'react'; +import ProfileSettings from '../components/ProfileSettings'; +import GameSettings from '../components/GameSettings'; + +const Settings = () => { + const [activeTab, setActiveTab] = useState('profile'); + const user = useUser(); + const userInfo = useUserInfo(); + + useEffect(() => { + if (!user) { + window.location.href = '/login'; + } + }, [user]); + + return ( +
+
+
+ + +
+
+ {activeTab === 'profile' && } + {activeTab === 'games' && } +
+
+
+ ); +}; + +export default Settings; diff --git a/apps/frontend/src/utils/formatResult.ts b/apps/frontend/src/utils/formatResult.ts new file mode 100644 index 00000000..56e2f2cb --- /dev/null +++ b/apps/frontend/src/utils/formatResult.ts @@ -0,0 +1,12 @@ +export const formatResult = (result: string) => { + switch (result) { + case 'WHITE_WINS': + return 'White Wins'; + case 'BLACK_WINS': + return 'Black Wins'; + case 'DRAW': + return 'Draw'; + default: + return 'Unknown'; + } +}; diff --git a/apps/frontend/src/utils/formatTime.ts b/apps/frontend/src/utils/formatTime.ts new file mode 100644 index 00000000..38bd2d14 --- /dev/null +++ b/apps/frontend/src/utils/formatTime.ts @@ -0,0 +1,21 @@ +interface Options { + year: 'numeric' | '2-digit'; + month: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long'; + day: 'numeric' | '2-digit'; + hour: 'numeric' | '2-digit'; + minute: 'numeric' | '2-digit'; + second: 'numeric' | '2-digit'; +} + +export const formatDate = (dateString: Date) => { + const date = new Date(dateString); + const options: Options = { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }; + return date.toLocaleDateString('en-US', options); +}; diff --git a/packages/db/prisma/migrations/20240509092438_init/migration.sql b/packages/db/prisma/migrations/20240509092438_init/migration.sql new file mode 100644 index 00000000..53806da8 --- /dev/null +++ b/packages/db/prisma/migrations/20240509092438_init/migration.sql @@ -0,0 +1,17 @@ +-- DropForeignKey +ALTER TABLE "Game" DROP CONSTRAINT "Game_blackPlayerId_fkey"; + +-- DropForeignKey +ALTER TABLE "Game" DROP CONSTRAINT "Game_whitePlayerId_fkey"; + +-- DropForeignKey +ALTER TABLE "Move" DROP CONSTRAINT "Move_gameId_fkey"; + +-- AddForeignKey +ALTER TABLE "Game" ADD CONSTRAINT "Game_whitePlayerId_fkey" FOREIGN KEY ("whitePlayerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Game" ADD CONSTRAINT "Game_blackPlayerId_fkey" FOREIGN KEY ("blackPlayerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Move" ADD CONSTRAINT "Move_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 8faaf75c..ed0806aa 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -27,8 +27,8 @@ model Game { id String @id @default(uuid()) whitePlayerId String blackPlayerId String - whitePlayer User @relation("GamesAsWhite", fields: [whitePlayerId], references: [id]) - blackPlayer User @relation("GamesAsBlack", fields: [blackPlayerId], references: [id]) + whitePlayer User @relation("GamesAsWhite", fields: [whitePlayerId], references: [id], onDelete: Cascade) + blackPlayer User @relation("GamesAsBlack", fields: [blackPlayerId], references: [id], onDelete: Cascade) status GameStatus result GameResult? timeControl TimeControl @@ -46,7 +46,7 @@ model Game { model Move { id String @id @default(uuid()) gameId String - game Game @relation(fields: [gameId], references: [id]) + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) moveNumber Int from String to String diff --git a/packages/store/src/atoms/user.ts b/packages/store/src/atoms/user.ts index c31bec0b..0ceed177 100644 --- a/packages/store/src/atoms/user.ts +++ b/packages/store/src/atoms/user.ts @@ -8,6 +8,12 @@ export interface User { name: string; } +export interface UserInfo { + email: string; + username: string; + rating: number; +} + export const userAtom = atom({ key: 'user', default: selector({ @@ -33,3 +39,29 @@ export const userAtom = atom({ }, }), }); + +export const userInfoAtom = atom({ + key: 'userInfo', + default: selector({ + key: 'userInfo/default', + get: async () => { + try { + const response = await fetch(`${BACKEND_URL}/auth/userInfo`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + if (response.ok) { + const data = await response.json(); + return data; + } + } catch (e) { + console.error(e); + } + + return null; + }, + }), +}); diff --git a/packages/store/src/hooks/useUser.ts b/packages/store/src/hooks/useUser.ts index a300382e..e0818553 100644 --- a/packages/store/src/hooks/useUser.ts +++ b/packages/store/src/hooks/useUser.ts @@ -1,7 +1,12 @@ import { useRecoilValue } from 'recoil'; -import { userAtom } from '../atoms/user'; +import { userAtom, userInfoAtom } from '../atoms/user'; export const useUser = () => { const value = useRecoilValue(userAtom); return value; }; + +export const useUserInfo = () => { + const value = useRecoilValue(userInfoAtom); + return value; +};