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;