From be80fffd29b3377b41e3e1bd173e7920225c10cd Mon Sep 17 00:00:00 2001 From: DHANUSHWI Date: Thu, 17 Apr 2025 22:36:06 -0500 Subject: [PATCH 1/7] Token Renewal API with secure validation and expiry extension --- package.json | 5 ++ server/routes/token.js | 106 +++++++++++++++++++++++++++-------------- 2 files changed, 74 insertions(+), 37 deletions(-) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 00000000..1bb6abe9 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "node-cron": "^3.0.3" + } +} diff --git a/server/routes/token.js b/server/routes/token.js index 247fda86..b6809181 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -16,8 +16,7 @@ const hashToken = (token) => { router.post("/request", async (req, res) => { try { - const { fullName, ouEmail, password, semester, academicAdvisor, role } = - req.body; + const { fullName, ouEmail, password, semester, academicAdvisor, role } = req.body; if (!fullName || !ouEmail || !password || !semester) { return res.status(400).json({ error: "All fields are required." }); @@ -25,9 +24,7 @@ router.post("/request", async (req, res) => { const existing = await TokenRequest.findOne({ ouEmail }); if (existing) { - return res - .status(400) - .json({ error: "Token request already exists for this email." }); + return res.status(400).json({ error: "Token request already exists for this email." }); } const plainToken = jwt.sign({ ouEmail }, JWT_SECRET, { expiresIn: "180d" }); @@ -35,7 +32,7 @@ router.post("/request", async (req, res) => { const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); const requestedAt = new Date(); - const expiresAt = new Date(requestedAt.getTime() + 5 * 24 * 60 * 60 * 1000); // 5 days + const expiresAt = new Date(requestedAt.getTime() + 5 * 24 * 60 * 60 * 1000); const request = new TokenRequest({ fullName, @@ -85,16 +82,12 @@ router.post("/activate", async (req, res) => { const { token } = req.body; if (!token) return res.status(400).json({ error: "Token is missing." }); const hashedToken = hashToken(token); - console.log("Received token:", token); const user = await TokenRequest.findOne({ token: hashedToken }); if (!user) return res.status(404).json({ error: "Token not found." }); - if (user.deletedAt) - return res.status(403).json({ error: "Token has been deactivated." }); - if (user.isActivated) - return res.status(400).json({ error: "Token already activated." }); - if (new Date() > user.expiresAt) - return res.status(400).json({ error: "Token has expired." }); + if (user.deletedAt) return res.status(403).json({ error: "Token has been deactivated." }); + if (user.isActivated) return res.status(400).json({ error: "Token already activated." }); + if (new Date() > user.expiresAt) return res.status(400).json({ error: "Token has expired." }); user.isActivated = true; user.activatedAt = new Date(); @@ -120,10 +113,8 @@ router.post("/login", async (req, res) => { const user = await TokenRequest.findOne({ token: hashedToken }); if (!user) return res.status(404).json({ error: "Invalid token." }); - if (user.deletedAt) - return res.status(403).json({ error: "Token is deactivated." }); - if (!user.isActivated) - return res.status(403).json({ error: "Token not activated." }); + if (user.deletedAt) return res.status(403).json({ error: "Token is deactivated." }); + if (!user.isActivated) return res.status(403).json({ error: "Token not activated." }); res.json({ message: "Login successful", user }); } catch (err) { @@ -131,40 +122,32 @@ router.post("/login", async (req, res) => { } }); -// login api router.post("/user-login", async (req, res) => { const { ouEmail, password, role } = req.body; - console.log(role); + if (!ouEmail || !password || !role) { return res.status(400).json({ message: "All fields are required" }); } + try { const user = await TokenRequest.findOne({ ouEmail }); if (!user) { - return res - .status(401) - .json({ message: "Email or password is incorrect" }); + return res.status(401).json({ message: "Email or password is incorrect" }); } const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { - return res - .status(401) - .json({ message: "Email or password is incorrect" }); + return res.status(401).json({ message: "Email or password is incorrect" }); } - // First, check if the entered role matches the user's actual role if (user.role.toLowerCase() !== role.toLowerCase()) { return res.status(403).json({ message: "User role mismatch." }); } - // If the role is student, do additional token checks if (role.toLowerCase() === "student") { if (!user.isStudent) { - return res - .status(403) - .json({ message: "User is not registered as a student." }); + return res.status(403).json({ message: "User is not registered as a student." }); } if (!user.token || user.token === "") { @@ -179,9 +162,9 @@ router.post("/user-login", async (req, res) => { const tokenExpiry = new Date(user.expiresAt); if (tokenExpiry < now) { - return res - .status(403) - .json({ message: "Token has expired. Please request a new one." }); + return res.status(403).json({ + message: "Token has expired. Please request a new one.", + }); } } @@ -196,14 +179,11 @@ router.delete("/deactivate", async (req, res) => { try { const { token, ouEmail } = req.body; if (!token && !ouEmail) { - return res - .status(400) - .json({ error: "Token or Email is required for deactivation." }); + return res.status(400).json({ error: "Token or Email is required for deactivation." }); } let filter = {}; - // Only hash the token if it exists if (token) { if (typeof token !== "string") { return res.status(400).json({ error: "Token must be a string." }); @@ -213,6 +193,7 @@ router.delete("/deactivate", async (req, res) => { } else { filter = { ouEmail }; } + const user = await TokenRequest.findOne(filter); if (!user) { return res.status(404).json({ error: "Token not found." }); @@ -233,4 +214,55 @@ router.delete("/deactivate", async (req, res) => { } }); +router.post("/renew", async (req, res) => { + try { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ error: "Token is required." }); + } + + const hashedToken = hashToken(token); + const user = await TokenRequest.findOne({ token: hashedToken }); + + if (!user) { + return res.status(404).json({ error: "Token not found." }); + } + + if (user.deletedAt || user.status === "deleted") { + return res.status(403).json({ error: "Token has been deactivated." }); + } + + if (!user.isActivated || user.status !== "activated") { + return res.status(403).json({ error: "Token is not activated." }); + } + + if (new Date() > user.expiresAt) { + return res.status(403).json({ error: "Token has already expired." }); + } + + const newToken = jwt.sign({ ouEmail: user.ouEmail }, JWT_SECRET, { expiresIn: "180d" }); + const hashedNewToken = hashToken(newToken); + + const newExpiresAt = new Date(); + newExpiresAt.setMonth(newExpiresAt.getMonth() + 6); + + user.token = hashedNewToken; + user.expiresAt = newExpiresAt; + user.status = "activated"; + + await user.save(); + + res.status(200).json({ + message: "Your token has been updated. You can now securely login.", + redirectUrl: `${FRONTEND_URL}/renewal-success`, + token: newToken, + expiresAt: newExpiresAt, + }); + } catch (error) { + console.error("Token renewal error:", error); + res.status(500).json({ error: "Internal server error." }); + } +}); + module.exports = router; From fbd3186b6616fc1f6a2db8c8016388215f12f5ed Mon Sep 17 00:00:00 2001 From: kushi-3 <84432650+kushi-3@users.noreply.github.com> Date: Fri, 18 Apr 2025 12:35:52 -0500 Subject: [PATCH 2/7] Add page for redirection after successful token renewal --- client/src/pages/RenewalSuccess.jsx | 24 ++++++++++++++++++++++++ client/src/router.js | 6 +++++- server/routes/token.js | 6 ++++-- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/RenewalSuccess.jsx diff --git a/client/src/pages/RenewalSuccess.jsx b/client/src/pages/RenewalSuccess.jsx new file mode 100644 index 00000000..e07ccd66 --- /dev/null +++ b/client/src/pages/RenewalSuccess.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; + + +const RenewalSuccess = () => { + const navigate = useNavigate(); + + const handleGoHome = () => { + navigate("/"); + }; + + return ( +
+
+

Success!

+

+ Your token has been updated. You can now securely login. +

+
+
+ ); +}; + +export default RenewalSuccess; diff --git a/client/src/router.js b/client/src/router.js index c1bfe4ef..c7d8f656 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -17,7 +17,7 @@ import A4PresentationEvaluationForm from "./pages/A4PresentationEvaluationForm"; import SupervisorDashboard from "./pages/SupervisorDashboard"; import CoordinatorDashboard from "./pages/CoordinatorDashboard"; import CoordinatorRequestDetailView from "./pages/CoordinatorRequestDetailView"; - +import RenewalSuccess from "./pages/RenewalSuccess"; // Create and export the router configuration const router = createBrowserRouter([ { @@ -65,6 +65,10 @@ const router = createBrowserRouter([ path: "coordinator/request/:id", element: , }, + { + path: "renewal-success", + element: , + } ], }, ]); diff --git a/server/routes/token.js b/server/routes/token.js index b6809181..8c5eab9c 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -222,8 +222,8 @@ router.post("/renew", async (req, res) => { return res.status(400).json({ error: "Token is required." }); } - const hashedToken = hashToken(token); - const user = await TokenRequest.findOne({ token: hashedToken }); + // const hashedToken = hashToken(token); + const user = await TokenRequest.findOne({ token: token }); if (!user) { return res.status(404).json({ error: "Token not found." }); @@ -259,6 +259,8 @@ router.post("/renew", async (req, res) => { token: newToken, expiresAt: newExpiresAt, }); + + res.redirect(`${FRONTEND_URL}/renewal-success`); } catch (error) { console.error("Token renewal error:", error); res.status(500).json({ error: "Internal server error." }); From 05a2bd3fdff5f5f8f1afcb6fd60eb1fef5c9349d Mon Sep 17 00:00:00 2001 From: kushi-3 <84432650+kushi-3@users.noreply.github.com> Date: Fri, 18 Apr 2025 12:49:41 -0500 Subject: [PATCH 3/7] Remove unused variable --- client/src/pages/RenewalSuccess.jsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/client/src/pages/RenewalSuccess.jsx b/client/src/pages/RenewalSuccess.jsx index e07ccd66..947e2bf4 100644 --- a/client/src/pages/RenewalSuccess.jsx +++ b/client/src/pages/RenewalSuccess.jsx @@ -1,13 +1,6 @@ import React from "react"; -import { useNavigate } from "react-router-dom"; - const RenewalSuccess = () => { - const navigate = useNavigate(); - - const handleGoHome = () => { - navigate("/"); - }; return (
From d7abafad89b8c03c22ac0c77d887da7c83b68ef9 Mon Sep 17 00:00:00 2001 From: kushi-3 <84432650+kushi-3@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:02:46 -0500 Subject: [PATCH 4/7] Revert "Remove unused variable" This reverts commit 05a2bd3fdff5f5f8f1afcb6fd60eb1fef5c9349d. --- client/src/pages/RenewalSuccess.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/src/pages/RenewalSuccess.jsx b/client/src/pages/RenewalSuccess.jsx index 947e2bf4..e07ccd66 100644 --- a/client/src/pages/RenewalSuccess.jsx +++ b/client/src/pages/RenewalSuccess.jsx @@ -1,6 +1,13 @@ import React from "react"; +import { useNavigate } from "react-router-dom"; + const RenewalSuccess = () => { + const navigate = useNavigate(); + + const handleGoHome = () => { + navigate("/"); + }; return (
From 86868e634ae185bfebf32a8bf0be05d66463b478 Mon Sep 17 00:00:00 2001 From: kushi-3 <84432650+kushi-3@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:05:10 -0500 Subject: [PATCH 5/7] Revert "Add page for redirection after successful token renewal" This reverts commit fbd3186b6616fc1f6a2db8c8016388215f12f5ed. --- client/src/pages/RenewalSuccess.jsx | 24 ------------------------ client/src/router.js | 6 +----- server/routes/token.js | 6 ++---- 3 files changed, 3 insertions(+), 33 deletions(-) delete mode 100644 client/src/pages/RenewalSuccess.jsx diff --git a/client/src/pages/RenewalSuccess.jsx b/client/src/pages/RenewalSuccess.jsx deleted file mode 100644 index e07ccd66..00000000 --- a/client/src/pages/RenewalSuccess.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import { useNavigate } from "react-router-dom"; - - -const RenewalSuccess = () => { - const navigate = useNavigate(); - - const handleGoHome = () => { - navigate("/"); - }; - - return ( -
-
-

Success!

-

- Your token has been updated. You can now securely login. -

-
-
- ); -}; - -export default RenewalSuccess; diff --git a/client/src/router.js b/client/src/router.js index c7d8f656..c1bfe4ef 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -17,7 +17,7 @@ import A4PresentationEvaluationForm from "./pages/A4PresentationEvaluationForm"; import SupervisorDashboard from "./pages/SupervisorDashboard"; import CoordinatorDashboard from "./pages/CoordinatorDashboard"; import CoordinatorRequestDetailView from "./pages/CoordinatorRequestDetailView"; -import RenewalSuccess from "./pages/RenewalSuccess"; + // Create and export the router configuration const router = createBrowserRouter([ { @@ -65,10 +65,6 @@ const router = createBrowserRouter([ path: "coordinator/request/:id", element: , }, - { - path: "renewal-success", - element: , - } ], }, ]); diff --git a/server/routes/token.js b/server/routes/token.js index 8c5eab9c..b6809181 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -222,8 +222,8 @@ router.post("/renew", async (req, res) => { return res.status(400).json({ error: "Token is required." }); } - // const hashedToken = hashToken(token); - const user = await TokenRequest.findOne({ token: token }); + const hashedToken = hashToken(token); + const user = await TokenRequest.findOne({ token: hashedToken }); if (!user) { return res.status(404).json({ error: "Token not found." }); @@ -259,8 +259,6 @@ router.post("/renew", async (req, res) => { token: newToken, expiresAt: newExpiresAt, }); - - res.redirect(`${FRONTEND_URL}/renewal-success`); } catch (error) { console.error("Token renewal error:", error); res.status(500).json({ error: "Internal server error." }); From 9205dc3b16a8b335c112a921f36591f5b21857e2 Mon Sep 17 00:00:00 2001 From: kushi-3 <84432650+kushi-3@users.noreply.github.com> Date: Sat, 19 Apr 2025 16:35:14 -0500 Subject: [PATCH 6/7] Update code to search for given token --- server/routes/token.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/server/routes/token.js b/server/routes/token.js index bfbdb217..17dc8b7c 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -227,26 +227,25 @@ router.post("/renew", async (req, res) => { const { token } = req.body; if (!token) { - return res.status(400).json({ error: "Token is required." }); + return res.status(400).json({ message: "Token is required." }); } - const hashedToken = hashToken(token); - const user = await TokenRequest.findOne({ token: hashedToken }); + const user = await TokenRequest.findOne({ token: token }); if (!user) { - return res.status(404).json({ error: "Token not found." }); + return res.status(404).json({ message: "Token not found." }); } if (user.deletedAt || user.status === "deleted") { - return res.status(403).json({ error: "Token has been deactivated." }); + return res.status(403).json({ message: "Token has been deactivated." }); } if (!user.isActivated || user.status !== "activated") { - return res.status(403).json({ error: "Token is not activated." }); + return res.status(403).json({ message: "Token is not activated." }); } if (new Date() > user.expiresAt) { - return res.status(403).json({ error: "Token has already expired." }); + return res.status(403).json({ message: "Token has already expired." }); } const newToken = jwt.sign({ ouEmail: user.ouEmail }, JWT_SECRET, { expiresIn: "180d" }); @@ -269,7 +268,7 @@ router.post("/renew", async (req, res) => { }); } catch (error) { console.error("Token renewal error:", error); - res.status(500).json({ error: "Internal server error." }); + res.status(500).json({ message: "Internal server error." }); } }); From 72e6c5f8d14598aaf8969af9aef8df382cef60b7 Mon Sep 17 00:00:00 2001 From: kushi-3 <84432650+kushi-3@users.noreply.github.com> Date: Sat, 19 Apr 2025 17:41:34 -0500 Subject: [PATCH 7/7] Add sooner id to the sign up --- client/src/pages/SignUp.js | 24 ++++++++++++++++++++++++ server/models/TokenRequest.js | 11 ++++++++--- server/routes/token.js | 5 +++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/client/src/pages/SignUp.js b/client/src/pages/SignUp.js index 34a9ac73..ec417986 100644 --- a/client/src/pages/SignUp.js +++ b/client/src/pages/SignUp.js @@ -15,6 +15,7 @@ function SignUp() { const [step, setStep] = useState(1); const [fullName, setFullName] = useState(""); const [ouEmail, setOuEmail] = useState(""); + const [soonerId, setSoonerId] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); @@ -61,12 +62,22 @@ function SignUp() { return; } + if (!/^\d{9}$/.test(soonerId)) { + Swal.fire({ + icon: "error", + title: "Invalid Sooner ID", + text: "Sooner ID must be a 9-digit number.", + }); + return; + } + try { const response = await axios.post( `${process.env.REACT_APP_API_URL}/api/token/request`, { fullName, ouEmail, + soonerId, password, semester, academicAdvisor: role === "student" ? academicAdvisor : "", @@ -255,6 +266,19 @@ function SignUp() { required />
+ +
+ + setSoonerId(e.target.value)} + placeholder="Enter your 9-digit Sooner ID" + required + /> +
+
diff --git a/server/models/TokenRequest.js b/server/models/TokenRequest.js index dcae8a0a..78cb6b1e 100644 --- a/server/models/TokenRequest.js +++ b/server/models/TokenRequest.js @@ -1,5 +1,3 @@ -// models/UserTokenRequest.js - const mongoose = require('mongoose'); /** @@ -12,6 +10,7 @@ const mongoose = require('mongoose'); * - fullName: Student's full name. * - password: Encrypted password for login authentication. * - ouEmail: Unique OU email for login. + * - soonerId: Unique 9-character ID assigned to the student. * - semester: The semester in which the internship is active. * - academicAdvisor: Reference to the academic advisor (if using a separate collection). * - token: Unique access token used for login. @@ -23,7 +22,7 @@ const mongoose = require('mongoose'); * - status: Optional string enum for tracking token state. * - activationLinkSentAt: Timestamp when the activation email was sent. * - password: Encrypted password for login authentication. - * + * Additional Features: * - Automatically sets `expiresAt` to 6 months from `requestedAt`. * - Uses `timestamps` to auto-generate `createdAt` and `updatedAt`. @@ -51,6 +50,12 @@ const userTokenRequestSchema = new mongoose.Schema( lowercase: true, match: [/^[\w-.]+@ou\.edu$/, 'Email must be a valid OU address'], }, + soonerId: { + type: String, + required: [true, 'Sooner ID is required'], + unique: true, + match: [/^\d{9}$/, 'Sooner ID must be exactly 9 digits'], + }, role: { type: String, required: true, diff --git a/server/routes/token.js b/server/routes/token.js index 17dc8b7c..d7270f44 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -17,9 +17,9 @@ const hashToken = (token) => { router.post("/request", async (req, res) => { try { - const { fullName, ouEmail, password, semester, academicAdvisor, role } = req.body; + const { fullName, ouEmail, soonerId, password, semester, academicAdvisor, role } = req.body; - if (!fullName || !ouEmail || !password || !semester) { + if (!fullName || !ouEmail || !soonerId || !password || !semester) { return res.status(400).json({ error: "All fields are required." }); } @@ -38,6 +38,7 @@ router.post("/request", async (req, res) => { const request = new TokenRequest({ fullName, ouEmail, + soonerId, password: hashedPassword, semester, role,