From 303c2d031e7166c1ecbf21eefbe758eb7cbb4646 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Wed, 8 Feb 2023 16:01:23 -0600 Subject: [PATCH] refactoring to use jwt with cookies --- client/package.json | 1 + client/src/App.jsx | 17 +++++++++++++ client/src/context/AuthContext.jsx | 4 +++ client/src/context/AuthProvider.jsx | 17 +++++++++++++ client/src/context/useAuthContext.jsx | 14 ++++++++++ client/src/main.jsx | 9 ++++--- client/src/pages/Auth.jsx | 13 ++++++++++ client/src/pages/Home.jsx | 24 +++++------------- client/src/pages/protected/Items.jsx | 12 ++++++--- client/src/pages/protected/SingleItem.jsx | 6 +++-- client/src/pages/protected/Welcome.jsx | 14 ++++++++-- client/src/util/API.js | 31 +++++++++++++---------- client/src/util/axiosInstance.js | 27 +++++++++++++++++--- server/routes/auth.js | 9 +++++-- server/routes/base.js | 25 ++++++++++++++++++ server/routes/index.js | 28 +++++++++++++------- server/routes/item.js | 6 ++--- server/routes/jwt.js | 22 ++++++++++++++++ 18 files changed, 222 insertions(+), 57 deletions(-) create mode 100644 client/src/context/AuthContext.jsx create mode 100644 client/src/context/AuthProvider.jsx create mode 100644 client/src/context/useAuthContext.jsx create mode 100644 server/routes/base.js create mode 100644 server/routes/jwt.js diff --git a/client/package.json b/client/package.json index bee806a..d2f1d4c 100644 --- a/client/package.json +++ b/client/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", + "proxy": "http://localhost:8080", "scripts": { "dev": "vite", "build": "vite build", diff --git a/client/src/App.jsx b/client/src/App.jsx index 403ae94..2e18836 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,10 +1,27 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import API from './util/API'; import Home from './pages/Home'; import SingleItem from './pages/protected/SingleItem'; import Items from './pages/protected/Items'; import './App.css' +import { useCallback, useEffect, useState } from 'react'; +import useAuthContext from './context/useAuthContext'; +import jwtDecode from 'jwt-decode'; function App() { + const { setUser, token, setToken } = useAuthContext(); + + useEffect(() => { + if (document.cookie) { + console.log(document.cookie); + + setToken(document.cookie.split("=")[1]); + setUser(jwtDecode(document.cookie.split("=")[1]).user); + + console.log(token); + } + }, []); + return (
diff --git a/client/src/context/AuthContext.jsx b/client/src/context/AuthContext.jsx new file mode 100644 index 0000000..197bab8 --- /dev/null +++ b/client/src/context/AuthContext.jsx @@ -0,0 +1,4 @@ +import { createContext } from "react"; + +const AuthContext = createContext(); +export default AuthContext; diff --git a/client/src/context/AuthProvider.jsx b/client/src/context/AuthProvider.jsx new file mode 100644 index 0000000..a37d778 --- /dev/null +++ b/client/src/context/AuthProvider.jsx @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react" +import AuthContext from "./authContext"; + +const AuthProvider = ({ children }) => { + const [user, setUser] = useState(); + const [token, setToken] = useState(); + + const value = { user, setUser, token, setToken } + + return ( + + { children } + + ) +} + +export default AuthProvider; \ No newline at end of file diff --git a/client/src/context/useAuthContext.jsx b/client/src/context/useAuthContext.jsx new file mode 100644 index 0000000..f28b891 --- /dev/null +++ b/client/src/context/useAuthContext.jsx @@ -0,0 +1,14 @@ +import { useContext, useEffect } from "react" +import AuthContext from "./authContext" + +const useAuthContext = () => { + const value = useContext(AuthContext); + + useEffect(() => { + console.log(value); + }, [value]) + + return value; +} + +export default useAuthContext \ No newline at end of file diff --git a/client/src/main.jsx b/client/src/main.jsx index 5cc5991..79e022d 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -1,10 +1,13 @@ import React from 'react' +import AuthProvider from './context/AuthProvider' import ReactDOM from 'react-dom/client' import App from './App' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( - - - , + + + + + ) diff --git a/client/src/pages/Auth.jsx b/client/src/pages/Auth.jsx index f8b5a5a..1eae66e 100644 --- a/client/src/pages/Auth.jsx +++ b/client/src/pages/Auth.jsx @@ -1,10 +1,23 @@ import { useState } from "react"; import { LoginForm, RegisterForm } from "../components/Form"; +import useAuthContext from "../context/useAuthContext"; +import API from "../util/API"; function Auth({ handleLogin, handleRegister }) { const [info, setInfo] = useState({ email: "", password: "" }); const [register, setRegister] = useState({ username: "", email: "", password: "" }) + const { setUser, setToken } = useAuthContext(); + + async function handleLogin() { + if (!info.email || !info.password) return; + + const response = await API.login(info); + + setUser(response.user); + setToken(response.token); + } + return (
diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx index 44cf4f9..8e6d59c 100644 --- a/client/src/pages/Home.jsx +++ b/client/src/pages/Home.jsx @@ -1,26 +1,24 @@ import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; -import jwt_decode from "jwt-decode"; +import useAuthContext from "../context/useAuthContext" import API from "../util/API"; import Auth from "./Auth"; import Welcome from "./protected/Welcome"; function Home() { - const [user, setUser] = useState(null); const [contents, setContents] = useState(<>); const [update, setUpdate] = useState(false); + const { user, setUser, token, setToken } = useAuthContext(); const navigate = useNavigate(); async function handleLogin(info) { if (!info.email || !info.password) return; const response = await API.login(info); - const user = jwt_decode(response.token); - - console.log(user); - localStorage.setItem('user', JSON.stringify(user)); - localStorage.setItem('token', response.token); + setUser(response.user); + setToken(response.token); + setUpdate(!update); } @@ -46,16 +44,8 @@ function Home() { } useEffect(() => { - let item = localStorage.getItem('user'); - if (item) { - item = JSON.parse(item); - setUser(item.user); - } - }, [update]) - - useEffect(() => { - setContents(user ? : ) - }, [user, update]) + setContents(user ? : ) + }, [token]) return (
diff --git a/client/src/pages/protected/Items.jsx b/client/src/pages/protected/Items.jsx index d42c69e..d9cad6a 100644 --- a/client/src/pages/protected/Items.jsx +++ b/client/src/pages/protected/Items.jsx @@ -1,15 +1,21 @@ import { useState, useEffect } from "react"; import API from "../../util/API"; import { v4 } from "uuid"; +import useAuthContext from "../../context/useAuthContext"; function Items() { const [items, setItems] = useState(

Loading...

); + const { token, setToken } = useAuthContext(); const [content, setContent] = useState(); useEffect(() => { - (async() => { + console.log(token); + + token && (async() => { try { - const data = await API.getItems(); + const data = await API.getItems(token); + console.log(data); + setItems(data.map(item => (
{item.name} @@ -25,7 +31,7 @@ function Items() { ) } })(); - }, []) + }, [token, setToken]) return (
diff --git a/client/src/pages/protected/SingleItem.jsx b/client/src/pages/protected/SingleItem.jsx index 3be1c60..c8ba978 100644 --- a/client/src/pages/protected/SingleItem.jsx +++ b/client/src/pages/protected/SingleItem.jsx @@ -1,15 +1,17 @@ import { useState, useEffect } from "react"; import { useParams } from "react-router-dom" +import useAuthContext from "../../context/useAuthContext"; import API from "../../util/API"; function SingleItem() { const [content, setContent] = useState(

Loading...

); + const { token, setToken } = useAuthContext(); const { id } = useParams(); useEffect(() => { (async() => { try { - const data = await API.getOneItem(id); + const data = await API.getOneItem(id, token); console.log(data); setContent( <> @@ -26,7 +28,7 @@ function SingleItem() { ) } })(); - }, []); + }, [token, setToken]); return (
diff --git a/client/src/pages/protected/Welcome.jsx b/client/src/pages/protected/Welcome.jsx index 1ff9dc9..b541d2a 100644 --- a/client/src/pages/protected/Welcome.jsx +++ b/client/src/pages/protected/Welcome.jsx @@ -1,11 +1,21 @@ import { useNavigate } from "react-router-dom" +import API from "../../util/API"; +import useAuthContext from "../../context/useAuthContext"; -function Welcome({ user, handleLogout }) { +function Welcome() { const navigate = useNavigate(); + const { user, setUser, setToken } = useAuthContext(); + + async function handleLogout() { + await API.logout(); + setUser(null); + setToken(null); + navigate('/'); + } return (
-

Welcome, {user.username}!

+

Welcome, {user?.username}!


Check out some cool protected actions:

diff --git a/client/src/util/API.js b/client/src/util/API.js index 3fe237a..18e4d1b 100644 --- a/client/src/util/API.js +++ b/client/src/util/API.js @@ -1,11 +1,15 @@ import { default as _api } from './axiosInstance'; export default class API { - static async validate() { + static async validate(token) { try { - const response = await _api.get('/'); - const data = Promise.resolve(response.data); - console.log(data); + const response = await _api.get('/', { + headers: { + "Content-Type": "application/json", + "Authorization": ("Bearer " + token) + } + }); + const data = Promise.resolve(response); return data; } catch (error) { console.log(error); @@ -15,7 +19,8 @@ export default class API { static async login(data) { try { const response = await _api.post('/auth/login', data); - return Promise.resolve(response.data); + console.log(response); + return Promise.resolve(response); } catch(err) { console.log(err); } @@ -25,7 +30,10 @@ export default class API { try { const response = await _api.delete('/auth/logout'); console.log(response); - return Promise.resolve(response.data); + + document.cookie = `token=;expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + console.log(document.cookie); + return null; } catch(err) { console.log(err); } @@ -40,21 +48,18 @@ export default class API { } } - static async getItems() { - const token = localStorage.getItem("token"); - + static async getItems(token) { const response = await _api.get('/app/item', { headers: { "Content-Type": "application/json", - "Authorization": ("Bearer " + token) + "Authorization": `Bearer ${token}` } }); + return Promise.resolve(response.data); } - static async getOneItem(id) { - const token = localStorage.getItem("token"); - + static async getOneItem(id, token) { const response = await _api.get(`/app/item/${id}`, { headers: { "Content-Type": "application/json", diff --git a/client/src/util/axiosInstance.js b/client/src/util/axiosInstance.js index 55579a4..6928774 100644 --- a/client/src/util/axiosInstance.js +++ b/client/src/util/axiosInstance.js @@ -1,7 +1,28 @@ import axios from 'axios' +import jwt_decode from 'jwt-decode' const apiUrl = import.meta.env.VITE_APIURL; -export default axios.create({ - baseURL: "http://localhost:8080" -}) \ No newline at end of file +const instance = axios.create({ + baseURL: apiUrl +}); + +instance.interceptors.response.use((res) => { + if (res?.data.token) { + document.cookie = `token=${res.data.token}`; + + return { + token: res.data.token, + user: jwt_decode(res.data.token).user, + data: res.data.data || null + } + } else { + return { + data: res?.data || null + } + } +}, (err) => { + return Promise.reject(err); +}) + +export default instance; \ No newline at end of file diff --git a/server/routes/auth.js b/server/routes/auth.js index aaadb02..0c9e7af 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -30,6 +30,8 @@ async function authRoute(app, passport) { response = response.data; } + console.log(response); + req.user = response; req.session.user = response; @@ -43,12 +45,14 @@ async function authRoute(app, passport) { } const token = jwt.sign({ user: safeUserData }, secret); - req.session.token = token; req.session.save((err) => { return next(err); }) + console.log(req.session); + + res.cookie('token', token, { httpOnly: true }); res.json({ token }); } } catch (error) { @@ -60,7 +64,8 @@ async function authRoute(app, passport) { try { req.session = null; req.user = null; - res.status(200).clearCookie('connect.sid'); + res.clearCookie('connect.sid').clearCookie('token'); + res.status(204).send("logout successful"); res.end(); } catch (error) { console.log(error); diff --git a/server/routes/base.js b/server/routes/base.js new file mode 100644 index 0000000..275d309 --- /dev/null +++ b/server/routes/base.js @@ -0,0 +1,25 @@ +const router = require('express').Router(); +require('dotenv').config(); + +const secret = process.env.SECRET; + +async function baseRoute(app, passport) { + router.get('/', async (req, res, next) => { + try { + console.log(req.session); + const user = undefined; + + if (!user) { + res.status(403).send("Not authorized"); + } else { + res.status(200).send({ token: req.token, user: req.user }); + } + } catch (error) { + console.log(error); + } + }) + + return router; +} + +module.exports = baseRoute; \ No newline at end of file diff --git a/server/routes/index.js b/server/routes/index.js index aeeeff3..1d45eb6 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,22 +1,32 @@ const jwt = require('jsonwebtoken'); const authRoute = require("./auth"); const itemRoute = require("./item"); +const jwtRoute = require('./jwt'); +const baseRoute = require('./base'); async function routesLoader(app, passport) { app.use('/app', (req, res, next) => { - const token = req.headers['authorization'].split(" ")[1]; - jwt.verify(token, process.env.SECRET, (err, data) => { - if (err) { - res.status(403).send(err); - } else { - req.user = data; - next(); - } - }) + const token = req.headers['authorization']?.split(" ")[1]; + + if (!token) { + res.status(403).send("Unauthorized"); + } else { + jwt.verify(token, process.env.SECRET, (err, data) => { + if (err) { + res.status(403).send(err); + } else { + req.user = data; + next(); + } + }) + } }) + app.use('/', await baseRoute(app, passport)); + app.use('/auth', await authRoute(app, passport)); app.use('/app', await itemRoute(app, passport)); + app.use('/jwt', await jwtRoute(app, passport)); } module.exports = routesLoader; \ No newline at end of file diff --git a/server/routes/item.js b/server/routes/item.js index b364b34..02da661 100644 --- a/server/routes/item.js +++ b/server/routes/item.js @@ -1,10 +1,10 @@ -// const jwt = require('jsonwebtoken'); +const jwt = require('jsonwebtoken'); require('dotenv').config(); const router = require('express').Router(); const ItemController = require('../controllers/ItemController'); async function itemRoute(app, passport) { - /* router.use('/', (req, res, next) => { + router.use('/', (req, res, next) => { const token = req.headers['authorization'].split(" ")[1]; jwt.verify(token, process.env.SECRET, (err, data) => { if (err) { @@ -14,7 +14,7 @@ async function itemRoute(app, passport) { next(); } }) - }) */ + }) router.get('/item', async (req, res) => { const response = await ItemController.getAll(); diff --git a/server/routes/jwt.js b/server/routes/jwt.js new file mode 100644 index 0000000..47fb2d6 --- /dev/null +++ b/server/routes/jwt.js @@ -0,0 +1,22 @@ +const router = require('express').Router(); +const jwt = require('jsonwebtoken'); +require('dotenv').config(); + +const secret = process.env.SECRET; + +async function jwtRoute(app, passport) { + router.get('/', (req, res) => { + const user = req.user; + if (!user) { + res.status(403).send("Unauthorized"); + } else { + const token = jwt.sign({ user: req.user }, secret); + res.cookie('token', token, { httpOnly: true }); + res.json({ token }); + } + }); + + return router; +} + +module.exports = jwtRoute; \ No newline at end of file