Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HER-32 remove jwt at unauthorized response #32

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/current_user_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class CurrentUserController < ApplicationController
before_action :authenticate_user!

# If a user is authenticated, they are set as the current user.
def index
def show
render json: UserSerializer.new(current_user).serializable_hash[:data][:attributes], status: :ok
end
end
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Rails.application.routes.draw do
get "/current_user", to: "current_user#index"
get "/current_user", to: "current_user#show"
devise_for :users, path: "", path_names: {
sign_in: "login",
sign_out: "logout",
Expand Down
88 changes: 12 additions & 76 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import './App.css';
import {BrowserRouter as Router, Routes, Route, Navigate} from "react-router-dom";
import { useContext } from 'react';
import { Routes, Route, Navigate } from "react-router-dom";
import PageWrapper from './components/PageWrapper';
import Home from './components/pages/Home';
import About from './components/pages/About';
Expand All @@ -9,107 +10,43 @@ import RequestKit from './components/pages/RequestKit'
import Registration from './components/auth/Registration';
import Login from './components/auth/Login';
import ScrollToHash from './components/ScrollToHash';
import { useState, useEffect } from 'react';
import Confirmation from './components/pages/Confirmation';
import Donation from './components/pages/Donation';
import RequestSpeaker from './components/pages/RequestSpeaker';
import { jwtDecode } from 'jwt-decode';
import AdminDashboard from './components/pages/AdminDashboard';
import NewForms from './components/NewForms';
import NewKit from './components/NewKit';
import NewUser from './components/NewUser';
import AddNew from './components/pages/AddNew';
import NewKitItem from './components/NewKitItem';
import AddItemToKit from './components/AddItemToKit';
import { AuthContext } from './components/auth/AuthContext';





function App() {
const [loggedIn, setLoggedIn] = useState();
const [user, setUser] = useState(null);
const [tokenExpiration, setTokenExpiration] = useState(null);

// Method handles login state and token, checking for existence or expiration
useEffect(() => {
const token = localStorage.getItem('jwt');

if (token) {
try {
const decoded = jwtDecode(token);
const now = Date.now() / 1000; // Current time in seconds

if (decoded.exp > now) {
setLoggedIn(true); // Token is valid
setUser(decoded.user ? decoded.user : null); // Set user data

// Calculate remaining time until token expiration
const timeUntilExpiration = (decoded.exp - now) * 1000;

// Notify 5 minutes before expiration
if (timeUntilExpiration < 300000) {
alert("Your session is about to expire. Please save your work.");
}

// Set token expiration time in state
setTokenExpiration(timeUntilExpiration);

} else {
// Token is expired, clear it
console.log("Token has expired, clearing JWT.");
localStorage.removeItem('jwt');
setLoggedIn(false);
setUser(null);
alert("Your session has expired. Please log in again.");
}
} catch (error) {
console.error('Token decoding failed:', error);
localStorage.removeItem('jwt');
setLoggedIn(false);
setUser(null);
}
} else {
// No token, set to logged out state
setLoggedIn(false);
setUser(null);
}
}, []);

// Logs out the user when the token expires
useEffect(() => {
if (tokenExpiration) {
const timer = setTimeout(() => {
console.log("Token has expired, logging out.");
localStorage.removeItem('jwt');
setLoggedIn(false);
setUser(null);
alert("Your session has expired. Please log in again.");
}, tokenExpiration);

return () => clearTimeout(timer);
}
}, [tokenExpiration]);
const { user } = useContext(AuthContext);

return (
// Sets routes for app navigation and passes props to the necessary components
<>
<div className="App">
<Router>
<PageWrapper loggedIn={loggedIn} setLoggedIn={setLoggedIn} setUser={setUser} user={user}>
<PageWrapper>
<ScrollToHash/>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact user={user} />} />
<Route path="/kits" element={<Kits user={user} />} />
<Route path="/orders" element={<RequestKit user={user} setUser={setUser} />} />
<Route path="/contact" element={<Contact />} />
<Route path="/kits" element={<Kits />} />
<Route path="/orders" element={<RequestKit />} />
<Route path="/registration" element={<Registration />} />
<Route path="/login" element={<Login setLoggedIn={setLoggedIn} />}/>
<Route path="/confirmation" element={<Confirmation user={user}/> } />
<Route path="/donation" element={<Donation user={user}/>} />
<Route path="/login" element={<Login />}/>
<Route path="/confirmation" element={<Confirmation /> } />
<Route path="/donation" element={<Donation />} />
<Route path="/speaker" element={<RequestSpeaker/>}/>
<Route path="/admin" element={user && user.role === 'admin' ? <AdminDashboard user={user} /> : <Navigate to="/" />} />
<Route path="/admin" element={user && user.role === 'admin' ? <AdminDashboard /> : <Navigate to="/" />} />
<Route path="/new_forms" element={<NewForms/>} >
<Route path="add_user" element={<AddNew header="Add New User"><NewUser /></AddNew>} />
<Route path="add_kit" element={<AddNew header="Add New Kit"><NewKit /></AddNew>} />
Expand All @@ -118,7 +55,6 @@ function App() {
</Route>
</Routes>
</PageWrapper>
</Router>
</div>
</>
);
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/DashCardSet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ const DashCardSet = () => {
}
} catch (error) {
console.error('Error fetching dashboard data:', error);
alert("An error occurred.")
}
};

fetchDashboardData();
}, []);
}, [dashUrl]);

return (
// Displays data using the dashboard card component
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/DashTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const DashTable = ({ apiEndpoint, headers, handleShow }) => {
} catch (err) {
setError(err.message);
console.error("Error fetching data:", err);
alert("A network error occurred.")
} finally {
setLoading(false);
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/EditModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ if (recordType === 'user') {
console.log(`${recordType} updated successfully!`);
alert(`${recordType} updated successfully!`);
handleClose(); // Close the modal after success
} else {
alert("An error occurred with the update.")
}
};

Expand Down
12 changes: 7 additions & 5 deletions frontend/src/components/Navigation.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from "react"
import React, { useContext } from "react"
import { Link, useNavigate } from "react-router-dom";
import CurrentUser from "./auth/CurrentUser";
import { AuthContext } from "./auth/AuthContext";
import Logout from "./auth/Logout";

// Passed in logged in and user state
function Navigation({ loggedIn, setLoggedIn, setUser, user }) {
function Navigation() {
const { loggedIn, user } = useContext(AuthContext);
const navigate = useNavigate();
const handleDonateClick = (e) => {
e.preventDefault();
Expand All @@ -13,7 +15,7 @@ function Navigation({ loggedIn, setLoggedIn, setUser, user }) {
if (!user) {
// If user not logged in, navigate to login page
alert("You must be logged in to make a donation. Please log in or register if you haven't already.");
navigate("/login")
navigate("/login");
} else {
// If user is logged in, navigate to the donation page
navigate("/donation");
Expand All @@ -32,7 +34,7 @@ function Navigation({ loggedIn, setLoggedIn, setUser, user }) {
<ul className="navbar-nav text-uppercase ms-auto py-4 py-lg-0">
{loggedIn && (
<>
<li className="nav-item"><span className="nav-link"><CurrentUser setLoggedIn={setLoggedIn} setUser={setUser} user={user} /></span></li>
<li className="nav-item"><span className="nav-link"><CurrentUser /></span></li>

</>
)}
Expand All @@ -48,7 +50,7 @@ function Navigation({ loggedIn, setLoggedIn, setUser, user }) {

{loggedIn ? (
<>
<li><Logout setLoggedIn={setLoggedIn} setUser={setUser} /></li>
<li><Logout /></li>
</>
) : (
<>
Expand Down
6 changes: 0 additions & 6 deletions frontend/src/components/NewKit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ const NewKit = () => {

console.log("New Kit added successfully!");
alert("New Kit added successfully!");

// Clear input fields
setName("");
setDescription("");
setGradeLevel("");
setImage('');
// Redirect to admin page
navigate("/admin");
} else {
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/NewKitItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom';
import { API_URL } from '../constants';

const NewKitItem = () => {

const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [image, setImage] = useState(null);
Expand Down
11 changes: 6 additions & 5 deletions frontend/src/components/PageWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import React from 'react'
import Navigation from './Navigation';
import Footer from './Footer';

// Passed in the content for the middle of the page as children and the logged in and user state. Navbar and footer displayed on every page.
const PageWrapper = ({children, loggedIn, setLoggedIn, setUser, user }) => (
// Passed in the content for the middle of the page as children. Navbar and footer displayed on every page.
const PageWrapper = ({children }) => {


return (
<div id="page-top">
<Navigation loggedIn={loggedIn} setLoggedIn={setLoggedIn} setUser={setUser} user={user} />
<Navigation />
{children}
<Footer/>
</div>
);

);
};

export default PageWrapper;
72 changes: 72 additions & 0 deletions frontend/src/components/auth/AuthContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { createContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { jwtDecode } from 'jwt-decode';
import { API_URL2 } from '../../constants';

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
const [loggedIn, setLoggedIn] = useState(false);
const [user, setUser] = useState(null);
const navigate = useNavigate();

useEffect(() => {
const checkAuth = async () => {
const token = localStorage.getItem('jwt');
if (token) {
try {
const decoded = jwtDecode(token);
const now = Date.now() / 1000;

if (decoded.exp > now) {
const response = await fetch(`${API_URL2}/current_user`, {
headers: {
Authorization: `Bearer ${token}`,
},
});

if (response.ok) {
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
setUser(data);
console.log("Current user data: ", data)
setLoggedIn(true);
} else {
throw new Error('Invalid response format');
}
} else {
handleUnauthorized();
}
} else {
handleUnauthorized();
}
} catch (error) {
console.error('Token decoding failed:', error);
handleUnauthorized();
}
} else {
handleUnauthorized();
}
};

const handleUnauthorized = () => {
logout();
};

checkAuth();
}, [navigate]);

const logout = () => {
setLoggedIn(false);
setUser(null);
localStorage.removeItem('jwt');
navigate("/login");
};

return (
<AuthContext.Provider value={{ loggedIn, user, setLoggedIn, setUser, logout }}>
{children}
</AuthContext.Provider>
);
};
39 changes: 6 additions & 33 deletions frontend/src/components/auth/CurrentUser.jsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,19 @@
import React, { useEffect, useState } from 'react';
import { API_URL2 } from '../../constants';
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { AuthContext } from './AuthContext';

const CurrentUser = ({ setLoggedIn, setUser, user }) => {
const CurrentUser = () => {
const { user } = useContext(AuthContext);

const userUrl = `${API_URL2}/current_user`
// Fetches the current user whenever someone logs in
useEffect(() => {
const fetchUser = async () => {
if(!user) {
try {
const response = await fetch(userUrl, {
headers: {
Authorization: `Bearer ${localStorage.getItem('jwt')}`,
},
});
if (response.ok) {
const data = await response.json();
setUser(data);
} else {
console.log("No user logged in.")
setUser(null);
const error = await response.json()
console.log(error)
}
} catch (error) {
console.error("Error fetching current user");
}
}};

fetchUser();
}, [setLoggedIn, user, setUser, userUrl]);
// Stretch Goal: Add admin dashboard
if (!user) return null;

return (
// Displays a welcome message and if admin, a link to access admin dashboard.
<div className='m-0 p-0 d-inline-flex'>
<p className='text-white bold' style={{ marginRight: 100 }}>
<em>Welcome, {user.first_name}!</em>
<em>Welcome, {user.name ? user.name.split(" ")[0] : "Guest"}!</em>
</p>
{user.role === 'admin' && <Link to="/admin"><i className="fas fa-user-shield"></i>
{user && user.role === 'admin' && <Link to="/admin"><i className="fas fa-user-shield"></i>
</Link>}
</div>
);
Expand Down
Loading
Loading