Skip to content

Commit

Permalink
auth: Implement complete auth flow.
Browse files Browse the repository at this point in the history
- Synced  navbar with auth state.
- Added endpoint to verify a token.
- Storing auth token in localStorage and retrieving it.
- Attempt to login with existing jwt on app load.
- Redirect to home page after login/signup
- Added frontend env var for api url.
  • Loading branch information
kuv2707 committed Jun 14, 2024
1 parent ec21f99 commit 9e336d9
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 172 deletions.
35 changes: 23 additions & 12 deletions backend/src/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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<void> => {
const { username, password }: IUser = req.body;
Expand All @@ -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,
});
}
);
Expand All @@ -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<void> => {
const user = req.user as IUser;
res.status(200).json({ user });
}
);

export { registerUser, loginUser, verifyUser };
2 changes: 1 addition & 1 deletion backend/src/middlewares/authMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down
8 changes: 7 additions & 1 deletion backend/src/routes/userRoutes.js
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 7 additions & 3 deletions backend/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
export type ControllerFunction = (
req: AuthRequest,
res: Response
) => Promise<void>;

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) {
Expand Down
68 changes: 28 additions & 40 deletions frontend/src/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -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<NavbarProps> = ({
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 = () => {
Expand All @@ -53,28 +38,31 @@ const Navbar: React.FC<NavbarProps> = ({
<>
<div className="text-xl font-bold mt-2">
<Button
text={user ? user.name : 'Noname'}
text={auth.getUser()!.name}
buttonSize="w-56 h-11"
className="border-4"
rounded="rounded-2xl"
onClick={goToLogin}
/>
</div>
</>
) : (
<>
<div className="text-xl font-bold mt-2">
<Button
text="Sign In /Login"
buttonSize="w-56 h-11"
className="border-4"
rounded="rounded-2xl"
onClick={goToLogin}
/>
</div>
{showLoginBtn && (
<div className="text-xl font-bold mt-2">
<Button
text="Login"
onClick={() => navigate('/login')}
buttonSize="w-56 h-11"
className="border-4"
rounded="rounded-2xl"
/>
</div>
)}
</>
)}
</div>
{isOpen && (
{sidebarOpen && (
<div className="fixed inset-0 flex z-20">
<div className="bg-gray-300 text-black border-gray-600 w-64 p-4 shadow-lg pl-10 rounded-r-lg relative z-30">
<button
Expand All @@ -87,14 +75,14 @@ const Navbar: React.FC<NavbarProps> = ({
className="w-7 h-7 sm:w-8 sm:h-8 md:w-8 md:h-8 lg:w-10 lg:h-10"
/>
</button>
{isLoggedIn ? (
{auth.isLoggedIn() ? (
<>
<div className="mb-2 mt-20 text-2xl font-kavoon align-center text-stroke-2 text-white font-bold">
{username}
{auth.getUser()?.name}
</div>
<Button
text="Logout"
onClick={onLogout}
onClick={auth.logout}
className="border-4 mb-2 mt-5"
fontSize="text-2xl"
buttonSize="w-[170px] h-12"
Expand All @@ -105,7 +93,7 @@ const Navbar: React.FC<NavbarProps> = ({
<>
<Button
text="Login"
onClick={onLogin}
onClick={() => navigate('/login')}
className="border-4 mb-2 mt-20"
fontSize="text-2xl"
buttonSize="w-[170px] h-12"
Expand All @@ -121,7 +109,7 @@ const Navbar: React.FC<NavbarProps> = ({
buttonSize="w-[170px] h-12"
px="px-0"
onClick={() => {
navigate('/error');
navigate('/about');
}}
/>
</div>
Expand All @@ -133,8 +121,8 @@ const Navbar: React.FC<NavbarProps> = ({
buttonSize="w-[170px] h-12"
px="px-0"
onClick={() => {
toggleMenu();
onOpenRulesModal?.();
setShowRulesModal(true);
setSidebarOpen(false);
}}
/>
</div>
Expand Down
88 changes: 77 additions & 11 deletions frontend/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
logout: () => void;
isLoggedIn: () => boolean;
};
export const AuthContext = createContext<AuthContextProps>({
getUser: () => null,
login: () => {},
authenticate: () => new Promise(() => {}),
logout: () => {},
isLoggedIn: () => false,
});
Expand All @@ -31,31 +36,92 @@ export function useAuth() {

export function AuthProvider({ children }: { children: ReactElement }) {
const [user, setUser] = useState<User | null>(null);
const [jwt, setJwt] = useState<string | null>(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(() => {
return user;
}, [user]);

const isLoggedIn = useCallback(() => {
return user !== null;
}, [user]);
return jwt !== null;
}, [jwt]);

return (
<AuthContext.Provider value={{ getUser, login, logout, isLoggedIn }}>
<AuthContext.Provider
value={{ getUser, authenticate, logout, isLoggedIn }}
>
{children}
</AuthContext.Provider>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/library/rulesModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const RulesModal: React.FC<ModalProps> = ({ onClose }) => {
<div
ref={modalRef}
onClick={closeModal}
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm flex items-center justify-center"
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm flex items-center justify-center z-10"
>
<div className="bg-[rgb(222,209,209)] p-5 rounded-xl border-4 border-black shadow-md flex flex-col gap-10 w-1/2 h-1/2 items-center justify-center">
<div className="relative flex flex-col h-full overflow-y-auto">
Expand Down
Loading

0 comments on commit 9e336d9

Please sign in to comment.