diff --git a/backend/src/controllers/userController.ts b/backend/src/controllers/userController.ts index 2fefd29..0318ddb 100644 --- a/backend/src/controllers/userController.ts +++ b/backend/src/controllers/userController.ts @@ -3,6 +3,7 @@ import { catchError, ControllerFunction } from '../utils'; import jwt from 'jsonwebtoken'; import { IUser, User } from '../models/userModel'; import { Types } from 'mongoose'; +import { AuthRequest } from '../middlewares/authMiddleware'; const getUserToken = (_id: string | Types.ObjectId): string => { const token = jwt.sign({ _id }, process.env.JWT_SECRET as string, { @@ -11,6 +12,11 @@ const getUserToken = (_id: string | Types.ObjectId): string => { return token; }; +const cookieOptions = { + httpOnly: true, + secure: true, +}; + const registerUser: ControllerFunction = catchError( async (req: Request, res: Response): Promise => { const { username, password }: IUser = req.body; @@ -34,9 +40,11 @@ const registerUser: ControllerFunction = catchError( throw new Error('Failed to create user'); } - res.status(201).send({ - message: 'User created successfully', - createdUser, + const token = getUserToken(user.id); + res.status(201).cookie('accessToken', token, cookieOptions).json({ + message: 'Sign up successful', + user: createdUser, + token, }); } ); @@ -59,20 +67,23 @@ const loginUser: ControllerFunction = catchError( throw new Error('Invalid credentials'); } - const token: string = getUserToken((user as IUser)._id as string); + const token: string = getUserToken(user.id); const loggedInUser = await User.findById(user._id).select('-password'); - const options = { - httpOnly: true, - secure: true, - }; - - res.status(200).cookie('accessToken', token, options).json({ - token, + res.status(200).cookie('accessToken', token, cookieOptions).json({ + message: 'Login successful', user: loggedInUser, + token, }); } ); -export { registerUser, loginUser }; +const verifyUser: ControllerFunction = catchError( + async (req: AuthRequest, res: Response): Promise => { + const user = req.user as IUser; + res.status(200).json({ user }); + } +); + +export { registerUser, loginUser, verifyUser }; diff --git a/backend/src/middlewares/authMiddleware.ts b/backend/src/middlewares/authMiddleware.ts index 94db664..5db773e 100644 --- a/backend/src/middlewares/authMiddleware.ts +++ b/backend/src/middlewares/authMiddleware.ts @@ -12,7 +12,7 @@ export const verifyToken = async ( next: NextFunction ) => { try { - const accessToken: string = req.cookies.accessToken; + const accessToken: string = req.body.token; if (!accessToken) { return res.status(401).json({ error: 'Access token is required' }); diff --git a/backend/src/routes/userRoutes.js b/backend/src/routes/userRoutes.js index 62ca073..709def7 100644 --- a/backend/src/routes/userRoutes.js +++ b/backend/src/routes/userRoutes.js @@ -1,9 +1,15 @@ import express from 'express'; -import { loginUser, registerUser } from '../controllers/userController'; +import { + loginUser, + registerUser, + verifyUser, +} from '../controllers/userController'; +import { verifyToken } from '../middlewares/authMiddleware'; const router = express.Router(); router.route('/register').post(registerUser); router.route('/login').post(loginUser); +router.route('/verify').post(verifyToken, verifyUser); export default router; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 676650a..ad0e618 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1,9 +1,13 @@ -import { Request, Response } from 'express'; +import { Response } from 'express'; +import { AuthRequest } from './middlewares/authMiddleware'; -export type ControllerFunction = (req: Request, res: Response) => Promise; +export type ControllerFunction = ( + req: AuthRequest, + res: Response +) => Promise; export function catchError(fn: ControllerFunction): ControllerFunction { - return async function (req: Request, res: Response) { + return async function (req: AuthRequest, res: Response) { try { return await fn(req, res); } catch (error) { diff --git a/frontend/src/Navbar.tsx b/frontend/src/Navbar.tsx index 821d932..54c75a6 100644 --- a/frontend/src/Navbar.tsx +++ b/frontend/src/Navbar.tsx @@ -1,36 +1,21 @@ import React, { useState } from 'react'; import Button from './library/button'; import './index.css'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import RulesModal from './library/rulesModal'; import { useAuth } from './contexts/AuthContext'; -type NavbarProps = { - isLoggedIn?: boolean; - username?: string; - onLogin?: () => void; - onLogout?: () => void; - onOpenRulesModal?: () => void; -}; - -const Navbar: React.FC = ({ - isLoggedIn, - username = 'unknown', - onLogin, - onLogout, - onOpenRulesModal, -}) => { - const [isOpen, setIsOpen] = useState(false); +const Navbar: React.FC = () => { + const [sidebarOpen, setSidebarOpen] = useState(false); const [showRulesModal, setShowRulesModal] = useState(false); - const auth = useAuth(); - - const user = auth.getUser(); - const navigate = useNavigate(); + const location = useLocation(); + const showLoginBtn = + location.pathname !== '/login' && location.pathname !== '/signup'; const toggleMenu = () => { - setIsOpen(!isOpen); + setSidebarOpen(!sidebarOpen); }; const goToLogin = () => { @@ -53,28 +38,31 @@ const Navbar: React.FC = ({ <>
) : ( <> -
-
+ {showLoginBtn && ( +
+
+ )} )} - {isOpen && ( + {sidebarOpen && (
- {isLoggedIn ? ( + {auth.isLoggedIn() ? ( <>
- {username} + {auth.getUser()?.name}
@@ -133,8 +121,8 @@ const Navbar: React.FC = ({ buttonSize="w-[170px] h-12" px="px-0" onClick={() => { - toggleMenu(); - onOpenRulesModal?.(); + setShowRulesModal(true); + setSidebarOpen(false); }} />
diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 68bc566..67ea979 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -3,23 +3,28 @@ import { createContext, useCallback, useContext, + useEffect, useState, } from 'react'; +import { useToast } from '../library/toast/toast-context'; export type User = { name: string; - pass: string; }; export type AuthContextProps = { getUser: () => User | null; - login: (arg0: string, arg1: string) => void; + authenticate: ( + name: string, + pass: string, + newuser?: boolean + ) => Promise; logout: () => void; isLoggedIn: () => boolean; }; export const AuthContext = createContext({ getUser: () => null, - login: () => {}, + authenticate: () => new Promise(() => {}), logout: () => {}, isLoggedIn: () => false, }); @@ -31,19 +36,78 @@ export function useAuth() { export function AuthProvider({ children }: { children: ReactElement }) { const [user, setUser] = useState(null); + const [jwt, setJwt] = useState(null); + const toast = useToast(); - const login = useCallback( - (name: string, pass: string) => { + useEffect(() => { + async function checkExistingJWT() { + try { + const localToken = localStorage.getItem('jwt'); + const res = await fetch( + process.env.REACT_APP_BACKEND_URL + '/auth/verify', + { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + token: localToken, + }), + } + ); + if (!res.ok) { + throw new Error('Invalid token'); + } else { + const data = await res.json(); + setUser({ + name: data.user.username, + }); + setJwt(localToken); + } + } catch (e) { + console.info('deleting existing jwt'); + localStorage.removeItem('jwt'); + } + } + checkExistingJWT(); + }, []); + + const authenticate = useCallback( + async ( + username: string, + password: string, + newuser: boolean = false + ) => { + const authType = newuser ? 'register' : 'login'; + const response = await fetch( + process.env.REACT_APP_BACKEND_URL + '/auth/' + authType, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + } + ); + if (!response.ok) { + toast.open({ message: 'Invalid credentials', color: 'error' }); + return; + } + const data = await response.json(); + console.log(data); setUser({ - name: name, - pass: pass, + name: data.user.username, }); + setJwt(data.token); + localStorage.setItem('jwt', data.token); }, - [setUser] + [setUser, toast] ); const logout = useCallback(() => { setUser(null); + setJwt(null); + localStorage.removeItem('jwt'); }, []); const getUser = useCallback(() => { @@ -51,11 +115,13 @@ export function AuthProvider({ children }: { children: ReactElement }) { }, [user]); const isLoggedIn = useCallback(() => { - return user !== null; - }, [user]); + return jwt !== null; + }, [jwt]); return ( - + {children} ); diff --git a/frontend/src/library/rulesModal.tsx b/frontend/src/library/rulesModal.tsx index f833948..9ff6078 100644 --- a/frontend/src/library/rulesModal.tsx +++ b/frontend/src/library/rulesModal.tsx @@ -17,7 +17,7 @@ const RulesModal: React.FC = ({ onClose }) => {
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 716f2da..74c74a0 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -4,7 +4,6 @@ import Button from '../library/button'; import Navbar from '../Navbar'; import Modal from '../library/modal'; import '../index.css'; -import { useAuth } from '../contexts/AuthContext'; import RulesModal from '../library/rulesModal'; const Home: React.FC = () => { @@ -22,28 +21,10 @@ const Home: React.FC = () => { setShowModal(true); console.log('Join Game with code'); }; - const auth = useAuth(); - - const handleLogin = () => { - navigate('/login'); - }; - - const handleLogout = () => { - auth.logout(); - }; - const openRulesModal = () => { - setShowRulesModal(true); - }; return (
- +
{ - const [name, setName] = useState(''); - const [password, setPassword] = useState(''); - const [isLoggedIn, setIsLoggedIn] = useState(false); const [username, setUsername] = useState(''); - const navigate = useNavigate(); + const [password, setPassword] = useState(''); const auth = useAuth(); - const user = auth.getUser(); - - const handleLogin = () => { - setUsername(username); - console.log(auth.isLoggedIn()); - }; - - const handleLogout = () => { - setUsername(''); - auth.logout(); - }; + const navigate = useNavigate(); - const handleNameChange = (event: React.ChangeEvent) => { - setName(event.target.value); + const handleUsernameChange = ( + event: React.ChangeEvent + ) => { + setUsername(event.target.value); }; const handlePasswordChange = ( @@ -36,31 +24,17 @@ const Login: React.FC = () => { setPassword(event.target.value); }; - const handleSubmit = (event: React.FormEvent) => { + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); console.log('Form submitted'); - if (user?.name == name && user.pass == password) { - console.log('Successful Login'); - navigate('/home'); - } else { - console.log('Failed Login'); - } + await auth.authenticate(username, password); + navigate('/'); }; - useEffect(() => { - setIsLoggedIn(auth.isLoggedIn()); - console.log(isLoggedIn); - }, [auth, user, isLoggedIn]); - return ( <>
- +
{/*
@@ -83,7 +57,7 @@ const Login: React.FC = () => { { - const [name, setName] = useState(''); + const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [confirmPass, setConfirmPass] = useState(''); - const [isLoggedIn, setIsLoggedIn] = useState(false); - const [username, setUsername] = useState(''); const navigate = useNavigate(); const auth = useAuth(); - const user = auth.getUser(); - - const handleLogin = () => { - setUsername('Username_7'); - setIsLoggedIn(true); - }; - - const handleLogout = () => { - setUsername(''); - setIsLoggedIn(false); - }; + const toast = useToast(); const handelConfirmPassChange = ( event: React.ChangeEvent @@ -34,7 +22,7 @@ const SignUp: React.FC = () => { }; const handleNameChange = (event: React.ChangeEvent) => { - setName(event.target.value); + setUsername(event.target.value); }; const handlePasswordChange = ( @@ -43,34 +31,20 @@ const SignUp: React.FC = () => { setPassword(event.target.value); }; - const handleSubmit = (event: React.FormEvent) => { + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); - console.log('Form submitted'); - if (password === confirmPass) { - auth.login(name, password); - console.log('successfull signup'); - navigate('/'); - } else { - console.log('failed signup'); + if (password !== confirmPass) { + toast.open({ message: 'Passwords do not match', color: 'error' }); + return; } + await auth.authenticate(username, password, true); + navigate('/'); }; - useEffect(() => { - const user = auth.getUser(); - console.log(user); - setIsLoggedIn(auth.isLoggedIn()); - console.log(isLoggedIn); - }, [auth, user, isLoggedIn]); - return ( <>
- +
{/*
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 861b04b..c50e64d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,13 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react-swc'; // https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], -}) +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + return { + define: { + 'process.env': env, + }, + plugins: [react()], + }; +});