diff --git a/client/package.json b/client/package.json index 75898d4e..1884b6fa 100644 --- a/client/package.json +++ b/client/package.json @@ -9,6 +9,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.8.2", "bootstrap": "^5.3.5", + "client": "file:", "react": "^19.0.0", "react-bootstrap": "^2.10.9", "react-dom": "^19.0.0", @@ -42,6 +43,6 @@ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" - ] + ] } } diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js index c8ed3df5..1c08db67 100644 --- a/client/src/pages/Home.js +++ b/client/src/pages/Home.js @@ -34,10 +34,10 @@ function Home() { const handleSubmit = async (e) => { e.preventDefault(); - console.log(`${formData.role} sign in attempted`, formData); const { email: ouEmail, password, role } = formData; + if (!ouEmail || !password || !role) { return Swal.fire({ icon: "warning", @@ -61,25 +61,50 @@ function Home() { const data = await response.json(); if (response.ok) { - Swal.fire({ - icon: "success", - title: "Login Successful 🌟", - text: `Welcome back, ${role}!`, - }); - - // Redirect user based on role - if (role === "coordinator") { - navigate("/coordinator-dashboard"); - } else if (role === "student") { - navigate("/student-dashboard"); - } else if (role === "supervisor") { + const user = data.user; + if(role === "student"){ + // Store only required fields + const limitedUserInfo = { + fullName: user.fullName, + id: user._id, + email:user.ouEmail + }; + + localStorage.setItem("ipmsUser", JSON.stringify(limitedUserInfo)); + navigate("/student-dashboard"); + }else if(role === "supervisor"){ + Swal.fire({ + icon: "success", + title: "Login Successful 🌟", + text: `Welcome back, ${role}!`, + }); navigate("/supervisor-dashboard"); + }else{ + Swal.fire({ + icon: "success", + title: "Login Successful 🌟", + text: `Welcome back, ${role}!`, + }); + navigate("/coordinator-dashboard"); } + + + + // Swal.fire({ + // icon: "success", + // title: "Login Successful", + // text: `Welcome back, `, + // }); + + } else { Swal.fire({ icon: "error", title: "Login Failed", - text: data.message || "Something went wrong ", + html: data.message + " " + + (data.renewalLink + ? `Please click here to request a new token.` + : "Something went wrong."), }); } } catch (error) { @@ -126,6 +151,7 @@ function Home() { role: r, }) } + >

diff --git a/client/src/pages/ProtectedRouteStudent.jsx b/client/src/pages/ProtectedRouteStudent.jsx new file mode 100644 index 00000000..02a0396e --- /dev/null +++ b/client/src/pages/ProtectedRouteStudent.jsx @@ -0,0 +1,16 @@ +// src/components/ProtectedRouteStudent.js +import React from "react"; +import { Navigate } from "react-router-dom"; + +const ProtectedRouteStudent = ({ children }) => { + const user = JSON.parse(localStorage.getItem("ipmsUser")); + + // Check if user info is missing + if (!user || !user.id || !user.fullName) { + return ; + } + + return children; + }; + +export default ProtectedRouteStudent; diff --git a/client/src/pages/SignUp.js b/client/src/pages/SignUp.js index 34a9ac73..3346fac1 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 (role=== "student" && !/^\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 : role === "student" ? soonerId : "", password, semester, academicAdvisor: role === "student" ? academicAdvisor : "", @@ -88,6 +99,7 @@ function SignUp() { // Clear form setFullName(""); setOuEmail(""); + setSoonerId(""); setPassword(""); setConfirmPassword(""); setSemester(""); @@ -100,13 +112,22 @@ function SignUp() { } catch (error) { console.error("Error creating user:", error); - if (error.response && error.response.status === 400) { + if (error.response && error.response.status === 401) { Swal.fire({ icon: "error", title: "Email Already Exists", text: "The provided email ID is already registered. Try logging in.", }); - } else { + } + else if(role=== "student" && error.response && error.response.status === 402){ + Swal.fire({ + icon: "error", + title: "Sooner ID Already Exists", + text: "The provided Sooner ID is already registered.", + }); + } + else { + console.log("Error response:", error.response); Swal.fire({ icon: "error", title: "Something went wrong", @@ -255,6 +276,19 @@ function SignUp() { required /> + + {role === "student" &&

+ + setSoonerId(e.target.value)} + placeholder="Enter your 9-digit Sooner ID" + required + /> +
} +
diff --git a/client/src/pages/StudentDashboard.jsx b/client/src/pages/StudentDashboard.jsx new file mode 100644 index 00000000..276bbb65 --- /dev/null +++ b/client/src/pages/StudentDashboard.jsx @@ -0,0 +1,141 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import "../styles/StudentDashboard.css"; // Make sure you create this CSS + +const StudentDashboard = () => { + const navigate = useNavigate(); + + const user = JSON.parse(localStorage.getItem("ipmsUser")); + const ouEmail = user?.email; + const [approvalStatus, setApprovalStatus] = useState("not_submitted"); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await fetch(`${process.env.REACT_APP_API_URL}/api/student`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ouEmail }), + }); + + const data = await res.json(); + setApprovalStatus(data.approvalStatus); + } catch (err) { + console.error("Error fetching internship data", err); + } + }; + + if (ouEmail) { + fetchData(); + } + }, [ouEmail]); + console.log(approvalStatus); + + return ( +
+
+

Welcome, {user.fullName}

+
+ +
+ {/* ------ FORM A1 Card ------ */} +
+
+

Request Internship (FORM A1)

+

Track your internship journey

+ + {approvalStatus === "not_submitted" && ( +

+ You have not submitted the form yet +

+ )} + + {(approvalStatus === "submitted" || + approvalStatus === "pending manual review") && ( +

+ Your form is submitted and under review +

+ )} + + {approvalStatus === "approved" && ( +

Approved

+ )} +
+ + +
+ + {/* ------ FORM A2 Card ------ */} +
+
+

Weekly Report (Form A2)

+ + {approvalStatus === "not_submitted" && ( +

+ Please fill your Form A1 first +

+ )} + + {approvalStatus === "draft" && ( +

+ Finish your Form A1 first +

+ )} + + {(approvalStatus === "submitted" || + approvalStatus === "pending manual review") && ( +

+ Wait for your Form A1 to be approved +

+ )} +
+ + +
+
+
+ ); +}; + +export default StudentDashboard; diff --git a/client/src/pages/TokenRenewal.jsx b/client/src/pages/TokenRenewal.jsx new file mode 100644 index 00000000..8db7edeb --- /dev/null +++ b/client/src/pages/TokenRenewal.jsx @@ -0,0 +1,59 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; + +const TokenRenewal = () => { + const { token } = useParams(); + const [responseMessage, setResponseMessage] = useState(''); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const renewToken = async () => { + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/api/token/renew`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token }), + }); + + const data = await response.json(); + + if (response.ok) { + setResponseMessage({ text: '✅ Success: Your token has been renewed!', success: true }); + } else { + setResponseMessage({ text: `❌ Error: ${data.message}`, success: false }); + } + } catch (error) { + setResponseMessage({ text: '❌ Error: Unable to process your request.', success: false }); + } finally { + setLoading(false); + } + }; + + if (token) { + renewToken(); + } else { + setResponseMessage({ text: '❌ Error: No token found in the URL.', success: false }); + setLoading(false); + } + }, [token]); + + return ( +
+

Token Renewal

+ {loading ? ( +

⏳ Processing your token renewal...

+ ) : ( +
+ {responseMessage.text} +
+ )} +
+ ); +}; + +export default TokenRenewal; diff --git a/client/src/router.js b/client/src/router.js index c1bfe4ef..8fac8de1 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -17,9 +17,12 @@ import A4PresentationEvaluationForm from "./pages/A4PresentationEvaluationForm"; import SupervisorDashboard from "./pages/SupervisorDashboard"; import CoordinatorDashboard from "./pages/CoordinatorDashboard"; import CoordinatorRequestDetailView from "./pages/CoordinatorRequestDetailView"; +import TokenRenewal from "./pages/TokenRenewal"; +import StudentDashboard from "./pages/StudentDashboard"; +import ProtectedRouteStudent from "./pages/ProtectedRouteStudent"; // Create and export the router configuration -const router = createBrowserRouter([ +const router = createBrowserRouter([ { path: "/", element: , @@ -32,11 +35,20 @@ const router = createBrowserRouter([ { path: "signup", element: , - }, + }, { path: "weekly-report", element: , }, + { + path: "/student-dashboard", + element: ( + + + + ) + }, + { path: "a1-form", element: , @@ -65,6 +77,10 @@ const router = createBrowserRouter([ path: "coordinator/request/:id", element: , }, + { + path: "renew-token/:token", + element: , + }, ], }, ]); diff --git a/client/src/styles/StudentDashboard.css b/client/src/styles/StudentDashboard.css new file mode 100644 index 00000000..71975b3e --- /dev/null +++ b/client/src/styles/StudentDashboard.css @@ -0,0 +1,71 @@ +.student-dashboard { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem 1rem; + background-color: #f7f7f7; + min-height: 90vh; + } + + .dashboard-header { + width: 100%; + max-width: 900px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + } + + .user-role { + font-weight: bold; + font-size: 1rem; + color: #5c0a0a; + } + + .dashboard-card { + background-color: white; + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); + padding: 2rem; + width: 100%; + max-width: 900px; + } + + .card-section { + background-color: #842020; + border-radius: 10px; + padding: 1.5rem; + margin-bottom: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + color: white; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1); + } + + .card-content h3 { + margin: 0; + font-size: 1.2rem; + font-weight: 600; + } + + .card-content p { + margin: 5px 0 0; + font-size: 0.9rem; + } + + .card-button { + background-color: white; + color: #842020; + border: none; + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: 0.3s ease-in-out; + } + + .card-button:hover { + background-color: #e6e6e6; + } + \ No newline at end of file diff --git a/server/index.js b/server/index.js index 45104393..c64c5d22 100644 --- a/server/index.js +++ b/server/index.js @@ -10,6 +10,7 @@ const formRoutes = require("./routes/formRoutes"); const emailRoutes = require("./routes/emailRoutes"); const tokenRoutes = require("./routes/token"); const approvalRoutes = require("./routes/approvalRoutes"); +const studentRoutes = require("./routes/studentRoutes") const outcomeRoutes = require("./routes/outcomeRoutes"); @@ -78,6 +79,7 @@ app.use("/api/token", tokenRoutes); app.use("/api", approvalRoutes); app.use("/api/reports", weeklyReportRoutes); +app.use("/api/student",studentRoutes) app.post("/api/createUser", async (req, res) => { try { const { userName, email, password, role } = req.body; diff --git a/server/jobs/autoDeactivateCronjobs.js b/server/jobs/autoDeactivateCronjobs.js new file mode 100644 index 00000000..1a27f2a7 --- /dev/null +++ b/server/jobs/autoDeactivateCronjobs.js @@ -0,0 +1,30 @@ +const TokenRequest = require("../models/TokenRequest"); + +const autoDeactivateCronjobs = async () => { + try { + const now = new Date(); + const startOfToday = new Date(now.setHours(0, 0, 0, 0)); + const endOfToday = new Date(now.setHours(23, 59, 59, 999)); + + const result = await TokenRequest.updateMany( + { + expiresAt: { + $gte: startOfToday, + $lt: endOfToday, }, + status: "activated", + }, + { + $set: { + status: "deactivated", + deactivationReason: "token_expired", + }, + } + ); + + console.log(`Auto-deactivated ${result.modifiedCount} users`); + } catch (error) { + console.error("Error in auto-deactivation:", error); + } +}; + +module.exports = autoDeactivateCronjobs; diff --git a/server/jobs/cronJobsConfig.js b/server/jobs/cronJobsConfig.js index 2cb9e5c6..3946910f 100644 --- a/server/jobs/cronJobsConfig.js +++ b/server/jobs/cronJobsConfig.js @@ -1,10 +1,14 @@ const CronJob = require("../models/CronJob"); const { coordinatorReminder, supervisorReminder } = require("./reminderEmail"); +const { checkAndSendReminders } = require("./tokenExpiryCheck"); +const autoDeactivateCronjobs = require("./autoDeactivateCronjobs"); // Map of job names to their corresponding functions const jobFunctions = { coordinatorApprovalReminder: coordinatorReminder, supervisorApprovalReminder: supervisorReminder, + tokenExpiryReminder: checkAndSendReminders, + autoDeactivateCronjobs: autoDeactivateCronjobs, // Add more job functions here as needed }; diff --git a/server/jobs/tokenExpiryCheck.js b/server/jobs/tokenExpiryCheck.js new file mode 100644 index 00000000..ebdc6374 --- /dev/null +++ b/server/jobs/tokenExpiryCheck.js @@ -0,0 +1,57 @@ +const mongoose = require("mongoose"); +const UserTokenRequest = require("../models/TokenRequest"); // Adjust the path if needed +const emailService = require("../services/emailService"); +require("dotenv").config(); + +const checkAndSendReminders = async () => { + try { + await mongoose.connect(process.env.MONGO_URI); + const now = new Date(); + + // Tokens expiring in 3 and 7 days + const daysToCheck = [3, 7]; + + for (const days of daysToCheck) { + const targetDate = new Date(now); + targetDate.setDate(now.getDate() + days); + + const expiringTokens = await UserTokenRequest.find({ + isActivated: true, + status: "activated", + expiresAt: { + $gte: new Date(targetDate.setHours(0, 0, 0, 0)), + $lt: new Date(targetDate.setHours(23, 59, 59, 999)), + }, + }); + + for (const token of expiringTokens) { + const subject = `Reminder: Your Internship Token Expires in ${days} Day${days > 1 ? "s" : ""}`; + const renewalLink = `${process.env.FRONTEND_URL}/renew-token/${token.token}`; // Update with actual renewal path + + const html = ` +

Hello ${token.fullName},

+

This is a reminder that your internship access token will expire in ${days} day${days > 1 ? "s" : ""} on ${token.expiresAt.toDateString()}.

+

If your token expires, you will lose access to the internship management system.

+

Click here to renew your token securely.

+

Thank you,
Internship Program Management Team

+ `; + + const emailResponse = await emailService.sendEmail({ + to: token.ouEmail, + subject, + html, + }); + + console.log(`Email sent to ${token.ouEmail} for ${days}-day reminder:`, emailResponse); + } + } + + await mongoose.disconnect(); + } catch (error) { + console.error("Error during token expiry check:", error); + } +} + +module.exports = { + checkAndSendReminders, +}; diff --git a/server/models/InternshipRequest.js b/server/models/InternshipRequest.js index 3732f04d..b9470465 100644 --- a/server/models/InternshipRequest.js +++ b/server/models/InternshipRequest.js @@ -16,7 +16,7 @@ const formA1 = new mongoose.Schema({ student: { // get student's name, email, id from User type: ObjectId, required: true, - ref: 'User' + ref: 'UserTokenRequest' }, workplace: { name: { diff --git a/server/models/TokenRequest.js b/server/models/TokenRequest.js index 32eeb74d..d7b52670 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,14 @@ const userTokenRequestSchema = new mongoose.Schema( lowercase: true, match: [/^[\w-.]+@ou\.edu$/, 'Email must be a valid OU address'], }, + soonerId: { + type: String, + required: function () { + return this.role === 'student'; + }, + unique: true, + match: [/^\d{9}$/, 'Sooner ID must be exactly 9 digits'], + }, role: { type: String, required: true, @@ -98,7 +105,7 @@ const userTokenRequestSchema = new mongoose.Schema( }, status: { type: String, - enum: ['pending', 'activated', 'expired', 'deleted'], + enum: ['pending', 'activated', 'expired', 'deleted','deactivated'], default: 'pending', }, }, diff --git a/server/package.json b/server/package.json index 20f54234..2e950867 100644 --- a/server/package.json +++ b/server/package.json @@ -24,7 +24,8 @@ "mongodb": "^6.14.2", "mongoose": "^8.13.2", "node-cron": "^3.0.3", - "nodemailer": "^6.10.0" + "nodemailer": "^6.10.0", + "server": "file:" }, "devDependencies": { "jest": "^29.7.0" diff --git a/server/routes/studentRoutes.js b/server/routes/studentRoutes.js new file mode 100644 index 00000000..26716b7c --- /dev/null +++ b/server/routes/studentRoutes.js @@ -0,0 +1,38 @@ +const express = require("express"); +const router = express.Router(); +const InternshipRequest = require("../models/InternshipRequest"); +const User = require("../models/User"); +const TokenRequest = require("../models/TokenRequest"); + + +// GET internship request by student's ouEmail +router.post("/", async (req, res) => { + const { ouEmail } = req.body; + console.log("Received email:", ouEmail); + + try { + const studentUser = await TokenRequest.findOne({ ouEmail }); + + if (!studentUser) { + return res.status(404).json({ message: "Student not found in TokenRequest" }); + } + + const internshipData = await InternshipRequest.findOne({ student: studentUser._id }); + + if (!internshipData) { + // No record found, return a specific flag + return res.status(200).json({ message: "No internship request found", approvalStatus: "not_submitted" }); + } + + const approvalStatus = internshipData.status; + + return res.status(200).json({ message: "Success", approvalStatus }); + } catch (error) { + console.error("Error fetching internship request:", error); + return res.status(500).json({ message: "Server error" }); + } +}); + + + +module.exports = router; \ No newline at end of file diff --git a/server/routes/token.js b/server/routes/token.js index 247fda86..5b663911 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -5,6 +5,7 @@ const crypto = require("crypto"); const bcrypt = require("bcrypt"); const TokenRequest = require("../models/TokenRequest"); const emailService = require("../services/emailService"); +const User = require("../models/User") const JWT_SECRET = process.env.JWT_SECRET; const FRONTEND_URL = process.env.FRONTEND_URL; @@ -16,18 +17,21 @@ 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) { return res.status(400).json({ error: "All fields are required." }); } const existing = await TokenRequest.findOne({ ouEmail }); if (existing) { - return res - .status(400) - .json({ error: "Token request already exists for this email." }); + return res.status(401).json({ error: "Token request already exists for this email." }); + } + + if(role==="student"){ + const existingSoonerId = await TokenRequest.findOne({ soonerId }); + if(existingSoonerId){ + return res.status(402).json({ error: "Token request already exists for this Sooner ID." }); + } } const plainToken = jwt.sign({ ouEmail }, JWT_SECRET, { expiresIn: "180d" }); @@ -35,11 +39,12 @@ 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, ouEmail, + soonerId: role === "student" ? soonerId : "", password: hashedPassword, semester, role, @@ -51,7 +56,10 @@ router.post("/request", async (req, res) => { activationLinkSentAt: new Date(), }); + + await request.save(); + const activationLink = `${FRONTEND_URL}/activate/${plainToken}`; const emailBody = ` @@ -85,16 +93,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 +124,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,57 +133,54 @@ 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 === "") { return res.status(403).json({ message: "Token not issued yet." }); } - if (user.status !== "activated") { + if (!user.isActivated) { return res.status(403).json({ message: "Token is not activated yet." }); } const now = new Date(); const tokenExpiry = new Date(user.expiresAt); - if (tokenExpiry < now) { - return res - .status(403) - .json({ message: "Token has expired. Please request a new one." }); + if (tokenExpiry < now || user.status === "deactivated") { + if(!user.status === "deactivated"){ + user.status = "deactivated"; + await user.save(); + } + return res.status(403).json({ + message : "Your account is deactivated due to token expiry.", + renewalLink: `${FRONTEND_URL}/renew-token/${user.token}` + }); } } @@ -196,14 +195,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 +209,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 +230,46 @@ router.delete("/deactivate", async (req, res) => { } }); +router.post("/renew", async (req, res) => { + try { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ message: "Token is required." }); + } + + const user = await TokenRequest.findOne({ token: token }); + + if (!user) { + return res.status(404).json({ message: "Token not found." }); + } + + if (user.deletedAt || user.status === "deleted") { + return res.status(403).json({ message: "Token has been deleted." }); + } + + 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({ message: "Internal server error." }); + } +}); + module.exports = router;