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/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..db8aaf77 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -17,6 +17,7 @@ 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"; // Create and export the router configuration const router = createBrowserRouter([ @@ -65,6 +66,10 @@ const router = createBrowserRouter([ path: "coordinator/request/:id", element: , }, + { + path: "renew-token/:token", + element: , + }, ], }, ]); diff --git a/server/jobs/cronJobsConfig.js b/server/jobs/cronJobsConfig.js index 2cb9e5c6..ae83c923 100644 --- a/server/jobs/cronJobsConfig.js +++ b/server/jobs/cronJobsConfig.js @@ -1,10 +1,12 @@ const CronJob = require("../models/CronJob"); const { coordinatorReminder, supervisorReminder } = require("./reminderEmail"); +const { checkAndSendReminders } = require("./tokenExpiryCheck"); // Map of job names to their corresponding functions const jobFunctions = { coordinatorApprovalReminder: coordinatorReminder, supervisorApprovalReminder: supervisorReminder, + tokenExpiryReminder: checkAndSendReminders, // 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/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/token.js b/server/routes/token.js index 247fda86..e9bbc8bb 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -233,4 +233,42 @@ router.delete("/deactivate", async (req, res) => { } }); +router.post('/renew', async (req, res) => { + const { token } = req.body; + + try { + + if (!token) return res.status(400).json({ error: "Token is missing." }); + console.log("Received token:", token); + const userToken = await TokenRequest.findOne({ token: token }); + + if (!userToken) { + return res.status(404).json({ message: 'Token not found or invalid.' }); + } + + if (userToken.status !== 'activated') { + return res.status(400).json({ message: 'Token is not activated, cannot renew.' }); + } + + // Renew the token logic (extend expiry, etc.) + const currentDate = new Date(); + userToken.expiresAt = new Date(currentDate.setMonth(currentDate.getMonth() + 6)); + await userToken.save(); + + // Send confirmation email (optional) + await emailService.sendEmail({ + to: userToken.ouEmail, + subject: 'Your Token Has Been Renewed', + html: `

Your token has been successfully renewed and will now expire on ${userToken.expiresAt.toLocaleDateString()}.

`, + }); + + console.log('Token successfully renewed!') + return res.status(200).json({ message: 'Token successfully renewed!' }); + + } catch (error) { + console.error('Error renewing token:', error); + return res.status(500).json({ message: 'An error occurred while renewing the token.' }); + } +}); + module.exports = router;