token refresher

This commit is contained in:
amNobodyyy
2025-01-29 00:21:08 +05:30
parent 16db1725f3
commit 597e47c2d0
10 changed files with 265 additions and 100 deletions

View File

@@ -13,9 +13,12 @@ import CourseTable from "./Pages/CourseTable";
import ConsolidatedTable from "./Pages/ConsolidatedTable"; import ConsolidatedTable from "./Pages/ConsolidatedTable";
import CourseConsolidated from "./Pages/courseConsolidated"; import CourseConsolidated from "./Pages/courseConsolidated";
import PrivateRoute from "./components/PrivateRoute"; import PrivateRoute from "./components/PrivateRoute";
import TokenRefresher from "./components/TokenRefresher";
function App() { function App() {
return ( return (
<>
<TokenRefresher />
<Routes> <Routes>
<Route path="/" element={<AuthPage />}></Route> <Route path="/" element={<AuthPage />}></Route>
<Route path="/course-form/:id" element={<PrivateRoute element={<CourseForm />} />} /> <Route path="/course-form/:id" element={<PrivateRoute element={<CourseForm />} />} />
@@ -28,6 +31,7 @@ function App() {
<Route path="/consolidated" element={<PrivateRoute element={<ConsolidatedTable />} />} /> <Route path="/consolidated" element={<PrivateRoute element={<ConsolidatedTable />} />} />
<Route path="/courseConsolidated" element={<PrivateRoute element={<CourseConsolidated />} />} /> <Route path="/courseConsolidated" element={<PrivateRoute element={<CourseConsolidated />} />} />
</Routes> </Routes>
</>
); );
} }

View File

@@ -6,39 +6,39 @@ import Navbar from "./Navbar";
const styles = { const styles = {
header: { header: {
background: '#003366', background: "#003366",
color: 'white', color: "white",
padding: '20px 0', padding: "20px 0",
textAlign: 'center', textAlign: "center",
fontSize: '24px', fontSize: "24px",
marginBottom: '0', marginBottom: "0",
}, },
buttonRow: { buttonRow: {
display: 'flex', display: "flex",
justifyContent: 'space-around', justifyContent: "space-around",
alignItems: 'center', alignItems: "center",
padding: '10px 0', padding: "10px 0",
margin: '0', margin: "0",
backgroundColor: '#003366', backgroundColor: "#003366",
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)' boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
}, },
button: { button: {
padding: '10px 20px', padding: "10px 20px",
borderRadius: '5px', borderRadius: "5px",
color: 'white', color: "white",
border: 'none', border: "none",
cursor: 'pointer', cursor: "pointer",
fontSize: '16px', fontSize: "16px",
flex: '1', flex: "1",
}, },
bulkDownloadButton: { bulkDownloadButton: {
backgroundColor: '#3fb5a3', backgroundColor: "#3fb5a3",
}, },
downloadButton: { downloadButton: {
backgroundColor: '#28a745', backgroundColor: "#28a745",
}, },
emailButton: { emailButton: {
backgroundColor: '#ff6f61', backgroundColor: "#ff6f61",
}, },
tableContainer: { tableContainer: {
maxHeight: "70vh", maxHeight: "70vh",
@@ -77,11 +77,9 @@ const styles = {
}, },
main: { main: {
width: "100%", width: "100%",
} },
}; };
const ConsolidatedTable = () => { const ConsolidatedTable = () => {
const [searchQuery, setSearchQuery] = useState(""); // State for search input const [searchQuery, setSearchQuery] = useState(""); // State for search input
const [data, setData] = useState([]); const [data, setData] = useState([]);
@@ -94,7 +92,8 @@ const ConsolidatedTable = () => {
const fetchData = async () => { const fetchData = async () => {
try { try {
const response = await axios.get( const response = await axios.get(
"http://localhost:8080/api/table/consolidated-table" "http://localhost:8080/api/table/consolidated-table",
{ withCredentials: true }
); );
setData(response.data); setData(response.data);
setLoading(false); setLoading(false);
@@ -113,8 +112,7 @@ const ConsolidatedTable = () => {
// Extract unique faculty names and filter based on the search query // Extract unique faculty names and filter based on the search query
const filteredTeachers = [...new Set(data.map((row) => row.Name))].filter( const filteredTeachers = [...new Set(data.map((row) => row.Name))].filter(
(teacher) => (teacher) => teacher.toLowerCase().includes(searchQuery.toLowerCase()) // Filter by search query
teacher.toLowerCase().includes(searchQuery.toLowerCase()) // Filter by search query
); );
// Pagination logic applied to filtered teachers // Pagination logic applied to filtered teachers
@@ -151,7 +149,8 @@ const ConsolidatedTable = () => {
const facultyId = teacherData[0].facultyId; const facultyId = teacherData[0].facultyId;
try { try {
const response = await axios.get( const response = await axios.get(
`http://localhost:8080/api/faculty/${facultyId}` `http://localhost:8080/api/faculty/${facultyId}`,
{ withCredentials: true }
); );
const facultyEmail = response.data.email; const facultyEmail = response.data.email;
const workbook = createExcelBook(teacherData, teacher); const workbook = createExcelBook(teacherData, teacher);
@@ -259,9 +258,7 @@ const ConsolidatedTable = () => {
) )
} }
> >
<h2 <h2 style={{ color: "black", margin: 0, fontSize: "1.5rem" }}>
style={{ color: "black", margin: 0, fontSize: "1.5rem" }}
>
{teacher}'s Table {teacher}'s Table
</h2> </h2>
<div> <div>

View File

@@ -1,4 +1,4 @@
import React, { useEffect , useState } from "react"; import React, { useEffect, useState } from "react";
import { Container, Col, Row } from "react-bootstrap"; import { Container, Col, Row } from "react-bootstrap";
import { FcGoogle } from "react-icons/fc"; import { FcGoogle } from "react-icons/fc";
import axios from "axios"; import axios from "axios";
@@ -18,7 +18,6 @@ function AuthPage() {
const notifyError = (message) => { const notifyError = (message) => {
toast.error(message); toast.error(message);
}; };
function ToggleSign(event) { function ToggleSign(event) {
event.preventDefault(); event.preventDefault();
@@ -40,38 +39,39 @@ function AuthPage() {
async function handleSubmit(event) { async function handleSubmit(event) {
event.preventDefault(); event.preventDefault();
if (!formData.username.trim() && signin) { if (!formData.username.trim() && signin) {
notifyError("Username cannot be empty"); notifyError("Username cannot be empty");
return; return;
} }
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(formData.email)) { if (!emailRegex.test(formData.email)) {
notifyError("Enter a valid email address"); notifyError("Enter a valid email address");
return; return;
} }
// Check password length // Check password length
if (formData.password.length < 8) { if (formData.password.length < 8) {
notifyError("Password must be at least 8 characters long"); notifyError("Password must be at least 8 characters long");
return; return;
} }
try { try {
const response = await axios.post( const response = await axios.post(
`http://localhost:8080/api/${ `http://localhost:8080/api/${!signin ? "login" : "register"}`,
!signin ? "login" : "register" formData,
}`, { withCredentials: true }
formData
); );
const { user } = response.data;
delete user.password;
const gravatarUrl = `https://www.gravatar.com/avatar/${md5(
user.email
)}?d=identicon`;
user.profilePicture = gravatarUrl;
localStorage.setItem("user", JSON.stringify(user)); if (response.status === 200) {
window.location.href = "/Welcom"; const { user } = response.data;
delete user.password;
const gravatarUrl = `https://www.gravatar.com/avatar/${md5(
user.email
)}?d=identicon`;
user.profilePicture = gravatarUrl;
window.location.href = "/Welcome";
}
} catch (error) { } catch (error) {
console.error("Authentication error:", error); console.error("Authentication error:", error);
if ( if (
@@ -88,11 +88,9 @@ function AuthPage() {
const handleGoogleLogin = (event) => { const handleGoogleLogin = (event) => {
event.preventDefault(); event.preventDefault();
window.location.href = window.location.href = "http://localhost:8080/auth/google";
"http://localhost:8080/auth/google";
}; };
return ( return (
<> <>
<ToastContainer /> <ToastContainer />

View File

@@ -1,9 +1,26 @@
import React from "react"; import React from "react";
import { FaUserCircle } from "react-icons/fa"; import { FaUserCircle } from "react-icons/fa";
import { NavLink } from "react-router-dom"; // Import NavLink for navigation import { NavLink, useNavigate } from "react-router-dom"; // Import NavLink for navigation
import "./Navbar.css"; // Navbar-specific styles import "./Navbar.css"; // Navbar-specific styles
import axios from "axios";
const Navbar = () => { const Navbar = () => {
const navigate = useNavigate();
// Handle logout functionality
const handleLogout = async () => {
try {
// Call the logout API
await axios.get("http://localhost:8080/auth/logout", { withCredentials: true });
// Redirect to the login page after successful logout
navigate("/");
} catch (error) {
console.error("Error during logout:", error);
alert("Failed to log out. Please try again.");
}
};
return ( return (
<header className="navbar"> <header className="navbar">
<div className="navbar-container"> <div className="navbar-container">
@@ -21,6 +38,11 @@ const Navbar = () => {
Course Consolidated Course Consolidated
</NavLink> </NavLink>
</div> </div>
<div>
<button className="logout-button" onClick={handleLogout}>
Logout
</button>
</div>
{/* User icon at the right */} {/* User icon at the right */}
<NavLink to="/accounts" className="user-icon-link"> <NavLink to="/accounts" className="user-icon-link">

View File

@@ -17,7 +17,8 @@ const CourseConsolidated = () => {
const fetchData = async () => { const fetchData = async () => {
try { try {
const response = await axios.get( const response = await axios.get(
"http://localhost:8080/api/table/course-consolidated" "http://localhost:8080/api/table/course-consolidated",
{ withCredentials: true }
); );
setData(response.data); setData(response.data);
setLoading(false); setLoading(false);

View File

@@ -1,12 +1,36 @@
import React from 'react'; import React, { useEffect, useState } from "react";
import { Navigate } from 'react-router-dom'; // Use Navigate for redirect import { Navigate } from "react-router-dom";
import Cookies from "js-cookie"; import axios from "axios";
const PrivateRoute = ({ element: Element, ...rest }) => { const PrivateRoute = ({ element: Element, ...rest }) => {
const token = Cookies.get("token"); const [isAuthenticated, setIsAuthenticated] = useState(null); // Track authentication status
// If token exists, render the element. Otherwise, redirect to the login page useEffect(() => {
return token ? Element : <Navigate to="/" />; // Call the server to validate the token
const checkAuth = async () => {
try {
const response = await axios.get("http://localhost:8080/api/auth-check", {
withCredentials: true, // Include cookies in the request
});
if (response.status === 200) {
setIsAuthenticated(true); // User is authenticated
}
} catch (error) {
setIsAuthenticated(false); // User is not authenticated
}
};
checkAuth();
}, []);
// If still checking authentication, render nothing or a loading spinner
if (isAuthenticated === null) {
return <div>Loading...</div>;
}
// If authenticated, render the component. Otherwise, redirect to the login page
return isAuthenticated ? Element : <Navigate to="/" />;
}; };
export default PrivateRoute; export default PrivateRoute;

View File

@@ -0,0 +1,37 @@
import { useEffect } from "react";
import axios from "axios";
import Cookies from "js-cookie";
import { useNavigate } from "react-router-dom";
const TokenRefresher = () => {
const navigate = useNavigate();
useEffect(() => {
// Function to refresh the token
const refreshToken = async () => {
try {
const response = await axios.post("http://localhost:8080/api/refresh", {}, {
withCredentials: true, // Make sure cookies are sent
});
} catch (error) {
console.error("Failed to refresh token", error);
// If refreshing the token fails (e.g., due to expiration), log out the user
Cookies.remove("token"); // Optionally, clear any existing cookies
navigate("/"); // Redirect to login page
}
};
// Set the interval to call refresh every 55 minutes (3300000 ms)
const refreshInterval = setInterval(() => {
refreshToken();
}, 3300000);
// Clean up the interval on component unmount
return () => clearInterval(refreshInterval);
}, [navigate]); // Empty dependency array to run once when component mounts
return null; // This component doesn't render anything
};
export default TokenRefresher;

View File

@@ -12,6 +12,7 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"connect-mongo": "^5.1.0", "connect-mongo": "^5.1.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"csv-writer": "^1.6.0", "csv-writer": "^1.6.0",
@@ -436,6 +437,28 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",

View File

@@ -21,6 +21,7 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"connect-mongo": "^5.1.0", "connect-mongo": "^5.1.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"csv-writer": "^1.6.0", "csv-writer": "^1.6.0",

View File

@@ -7,6 +7,7 @@ const bodyParser = require("body-parser");
const path = require("path"); const path = require("path");
const bcrypt = require("bcryptjs"); const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken"); const jwt = require("jsonwebtoken");
const cookieparser = require("cookie-parser");
require("dotenv").config(); require("dotenv").config();
// Import Routes // Import Routes
@@ -34,6 +35,7 @@ const PORT = 8080;
// Middleware // Middleware
app.use(cors({ origin: "http://localhost:3000", credentials: true })); app.use(cors({ origin: "http://localhost:3000", credentials: true }));
app.use(cookieparser());
app.use(express.json()); app.use(express.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
app.use( app.use(
@@ -54,12 +56,12 @@ require("./config/passport");
// Routes // Routes
app.use("/password", authRoutes); app.use("/password", authRoutes);
app.use("/api/courses", courseRoutes); app.use("/api/courses", courseRoutes);
app.use("/api/faculty", facultyRoutes); app.use("/api/faculty", facultyRoutes);
app.use("/api/appointments", appointmentRoutes); app.use("/api/appointments", appointmentRoutes);
app.use("/api/options", optionsRoutes); app.use("/api/options", optionsRoutes);
app.use("/api/data", consolidatedRoutes); app.use("/api/data", consolidatedRoutes);
app.use("/api/send-email", emailRoutes); app.use("/api/send-email", emailRoutes);
// Google OAuth Routes // Google OAuth Routes
app.get( app.get(
@@ -73,7 +75,7 @@ app.get(
(req, res) => { (req, res) => {
const token = jwt.sign({ userId: req.user._id }, process.env.JWT_SECRET, { expiresIn: "1h" }); const token = jwt.sign({ userId: req.user._id }, process.env.JWT_SECRET, { expiresIn: "1h" });
// Set token as a cookie or send it in the response // Set token as a cookie or send it in the response
res.cookie("token", token, { httpOnly: false, secure: false }); res.cookie("token", token, { httpOnly: true, secure: false, maxAge: 3600000 });
res.redirect("http://localhost:3000/Welcome"); // Redirect to a frontend route after successful login res.redirect("http://localhost:3000/Welcome"); // Redirect to a frontend route after successful login
} }
); );
@@ -101,15 +103,15 @@ app.post("/api/register", async (req, res) => {
await user.save(); await user.save();
} }
req.login(user, (err) => { // Generate a JWT token using the user's ID
if (err) { const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: "1h" });
console.error("Error logging in user after registration:", err);
return res.status(500).send("Internal server error"); // Set the token as a cookie
} res.cookie("token", token, { httpOnly: true, secure: false, maxAge: 3600000 }); // 1 hour expiry
return res.status(200).json({
message: "Registered and logged in successfully", return res.status(200).json({
user, message: "Registered and logged in successfully",
}); user,
}); });
} catch (error) { } catch (error) {
console.error("Error registering user:", error); console.error("Error registering user:", error);
@@ -129,27 +131,83 @@ app.post("/api/login", (req, res, next) => {
if (err) { if (err) {
return res.status(500).json({ message: "Internal server error" }); return res.status(500).json({ message: "Internal server error" });
} }
// Generate a JWT token using the user's ID
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: "1h" });
// Set the token as a cookie
res.cookie("token", token, { httpOnly: true, secure: false, maxAge: 3600000 }); // 1 hour expiry
return res.status(200).json({ message: "Login successful", user }); return res.status(200).json({ message: "Login successful", user });
}); });
})(req, res, next); })(req, res, next);
}); });
app.get("/auth/logout", function (req, res) { app.get("/auth/logout", function (req, res) {
req.logout((err) => { try {
if (err) { // Clear the token cookie
console.log(err); res.clearCookie("token", {
return res.status(500).json({ message: "Error logging out" }); httpOnly: true, // Ensure it matches the cookie options you set earlier
} secure: false, // Match the "secure" option from cookie settings
req.session.destroy(function (err) { sameSite: "lax", // Ensure this matches the original cookie configuration
if (err) {
console.log(err);
return res.status(500).json({ message: "Error destroying session" });
}
res.json({ message: "Logout successful" });
}); });
});
// Destroy the session if used (optional, if sessions are implemented)
if (req.session) {
req.session.destroy((err) => {
if (err) {
console.error("Error destroying session:", err);
return res.status(500).json({ message: "Error logging out" });
}
res.status(200).json({ message: "Logout successful, session destroyed" });
});
} else {
// If no session, simply respond with success
res.status(200).json({ message: "Logout successful, cookie cleared" });
}
} catch (err) {
console.error("Error logging out:", err);
res.status(500).json({ message: "Error logging out" });
}
}); });
// Refresh Token Endpoint
app.post("/api/refresh", (req, res) => {
const refreshToken = req.cookies.token;
console.log(refreshToken);
if (!refreshToken) {
return res.status(401).json({ message: "No refresh token, authorization denied" });
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
const newToken = jwt.sign({ userId: decoded.userId }, process.env.JWT_SECRET, { expiresIn: "1h" });
return res
.cookie("token", newToken, { httpOnly: true, maxAge: 3600000 }) // Set new access token
.status(200)
.json({ message: "Token refreshed" });
} catch (err) {
console.error("Error refreshing token:", err);
res.status(401).json({ message: "Invalid or expired refresh token" });
}
});
app.get("/api/auth-check", (req, res) => {
const token = req.cookies.token; // Retrieve the httpOnly cookie
if (!token) {
return res.status(401).json({ message: "Unauthorized" });
}
try {
jwt.verify(token, process.env.JWT_SECRET); // Verify the token
res.status(200).json({ authenticated: true }); // Valid token
} catch (err) {
res.status(401).json({ message: "Unauthorized" }); // Invalid token
}
});
// User Profile Route // User Profile Route
app.get("/api/user/profile", async (req, res) => { app.get("/api/user/profile", async (req, res) => {
try { try {