-
-
Welcome, {user.fullName}
-
+
- {/* ------ FORM A1 Card ------ */}
+
+
+ {/* Avatar Icon */}
+
+ {/* Bootstrap "person" icon, can use font-awesome as well */}
+
+
+
+
+ {/* Student Info */}
+
+
+ {user.fullName}
+
+
+
+ Advisor: [{user.academicAdvisor}]
+
+
+
+
+ Semester: [{user.semester}]
+
+
+
+
+
+ {/* 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
- )}
+
Status: {approvalStatus.replace("_", " ")}
-
{
- if (
- approvalStatus === "draft" ||
- approvalStatus === "not_submitted"
- ) {
+
navigate("/a1-form");
- }
+
}}
disabled={
- approvalStatus !== "draft" && approvalStatus !== "not_submitted"
+ approvalStatus === "pending" ||
+ approvalStatus === "approved"
}
- style={{
- backgroundColor:
- approvalStatus !== "draft" && approvalStatus !== "not_submitted"
- ? "#ccc"
- : "",
- cursor:
- approvalStatus !== "draft" && approvalStatus !== "not_submitted"
- ? "not-allowed"
- : "pointer",
- }}
>
- {approvalStatus === "approved" ? "Track" : "Request Internship"}
+ {approvalStatus === "approved" ? "Approved" : approvalStatus === "pending" ? "Under Review" : "Request Internship"}
- {/* ------ FORM A2 Card ------ */}
+ {/* 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
-
- )}
+
Weekly Report (FORM A2)
+
+ {approvalStatus === "approved"
+ ? "You may now submit weekly reports"
+ : "Finish Form A1 approval first"}
+
-
{
- if (approvalStatus === "approved") {
- navigate("/weekly-report");
- }
- }}
+ onClick={() => navigate("/weekly-report")}
+ >
+ Request
+
+
+
+
+ {/* Submissions Section */}
+
+
My Submissions
+ {submissions.length === 0 ? (
+
No submissions yet.
+ ) : (
+
+
+
+ Form
+ Supervisor Status
+ Coordinator Status
+ Actions
+
+
+
+ {submissions.map((s) => (
+
+ {s.form_type}
+ {s.supervisor_status}
+ {s.coordinator_status}
+
+ {s.supervisor_status === "approved" &&
+ s.coordinator_status === "pending" ? (
+ <>
+ handleResend(s._id)}>
+ Resend
+
+ handleDelete(s._id)}>
+ Delete
+
+ >
+ ) : (
+ "โ"
+ )}
+
+
+ ))}
+
+
+ )}
+
+
- Request
+ {/* Trash SVG icon */}
+
+
+
+
+ Delete My Account
+
@@ -138,4 +310,4 @@ const StudentDashboard = () => {
);
};
-export default StudentDashboard;
+export default StudentDashboard;
\ No newline at end of file
diff --git a/client/src/router.js b/client/src/router.js
index 2dba3de27..a41675cf9 100644
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -19,6 +19,8 @@ import CoordinatorRequestDetailView from "./pages/CoordinatorRequestDetailView";
import TokenRenewal from "./pages/TokenRenewal";
import StudentDashboard from "./pages/StudentDashboard";
import ProtectedRouteStudent from "./pages/ProtectedRouteStudent";
+import About from "./pages/About";
+import Contact from "./pages/Contact";
import WeeklyFourWeekReportForm from "./pages/WeeklyFourWeekReportForm";
import SubmittedReports from "./pages/SubmittedReports";
import CumulativeReviewForm from "./pages/CumulativeReviewForm";
@@ -84,6 +86,14 @@ const router = createBrowserRouter([
path: "renew-token/:token",
element:
,
},
+ {
+ path: "about",
+ element:
,
+ },
+ {
+ path: "contact",
+ element:
,
+ },
{
path: "four-week-report",
element:
,
diff --git a/client/src/styles/About.css b/client/src/styles/About.css
new file mode 100644
index 000000000..4a6ba6b8c
--- /dev/null
+++ b/client/src/styles/About.css
@@ -0,0 +1,112 @@
+ .about-wrapper {
+ max-width: 1100px;
+ margin: auto;
+ padding: 2rem;
+ font-family: 'Segoe UI', sans-serif;
+ color: #333;
+ }
+
+ .about-heading {
+ text-align: center;
+ font-size: 2.8rem;
+ margin-bottom: 1rem;
+ color: #2a4d69;
+ }
+
+ .about-banner {
+ width: 100%;
+ height: auto;
+ border-radius: 8px;
+ margin-bottom: 1.5rem;
+ }
+
+ .about-summary {
+ font-size: 1.1rem;
+ text-align: center;
+ margin-bottom: 2rem;
+ line-height: 1.6;
+ }
+
+ .section {
+ margin-bottom: 3rem;
+ }
+
+ .feature-cards {
+ display: flex;
+ gap: 1.5rem;
+ justify-content: space-around;
+ flex-wrap: wrap;
+ }
+
+ .feature-card {
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 12px;
+ padding: 1.5rem;
+ width: 300px;
+ text-align: center;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.05);
+ transition: 0.3s ease;
+ }
+
+ .feature-card:hover {
+ box-shadow: 0 6px 15px rgba(0,0,0,0.1);
+ }
+
+ .feature-card .icon {
+ font-size: 3rem;
+ margin-bottom: 0.5rem;
+ }
+
+ .section-heading {
+ text-align: center;
+ font-size: 1.8rem;
+ margin-bottom: 1rem;
+ }
+
+ .user-role-cards {
+ display: flex;
+ justify-content: space-around;
+ gap: 1.5rem;
+ flex-wrap: wrap;
+ }
+
+ .user-card {
+ background: #fff;
+ padding: 1.2rem;
+ text-align: center;
+ border-radius: 12px;
+ border: 1px solid #ddd;
+ width: 250px;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.05);
+ }
+
+ .user-card img {
+ width: 200px;
+ margin-bottom: 0.8rem;
+ }
+
+ .cta-footer {
+ background-color: #8b0000;
+ color: white;
+ text-align: center;
+ padding: 2rem;
+ border-radius: 8px;
+ }
+
+ .cta-button {
+ display: inline-block;
+ margin-top: 1rem;
+ padding: 0.8rem 1.6rem;
+ background: white;
+ color: #8b0000;
+ text-decoration: none;
+ font-weight: bold;
+ border-radius: 6px;
+ transition: background 0.3s ease;
+ }
+
+ .cta-button:hover {
+ background: #f0f0f0;
+ }
+
\ No newline at end of file
diff --git a/client/src/styles/Contact.css b/client/src/styles/Contact.css
new file mode 100644
index 000000000..e1f1dc745
--- /dev/null
+++ b/client/src/styles/Contact.css
@@ -0,0 +1,80 @@
+.contact-container {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 2rem;
+ font-family: 'Segoe UI', sans-serif;
+ background-color: #f9f9fb;
+}
+
+.contact-heading {
+ text-align: center;
+ font-size: 2.5rem;
+ margin-bottom: 2rem;
+ color: #222;
+}
+
+.contact-card,
+.tech-support-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 1.5rem;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+ margin-bottom: 1.5rem;
+}
+
+.contact-card {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+}
+
+.contact-image {
+ width: 160px;
+ height: 160px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.contact-info {
+ flex: 1;
+}
+
+.contact-info h2 {
+ margin-bottom: 0.25rem;
+ color: #222;
+}
+
+.contact-info a {
+ color: #9d2235;
+ text-decoration: none;
+}
+
+.contact-info a:hover {
+ text-decoration: underline;
+}
+
+.team-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(375px, 1fr));
+ gap: 1.2rem;
+ margin-top: 1rem;
+}
+
+.team-member {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+}
+
+.team-name {
+ font-weight: 600;
+ color: #333;
+}
+
+.team-icon {
+ width: 114px;
+ height: 133px;
+ fill: #8a1c1c;
+ flex-shrink: 0;
+ display: block;
+}
\ No newline at end of file
diff --git a/server/models/TokenRequest.js b/server/models/TokenRequest.js
index d7b52670e..2b76e0a77 100644
--- a/server/models/TokenRequest.js
+++ b/server/models/TokenRequest.js
@@ -21,8 +21,7 @@ const mongoose = require('mongoose');
* - deletedAt: Marks soft deletion if the student cancels.
* - 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`.
@@ -79,10 +78,11 @@ const userTokenRequestSchema = new mongoose.Schema(
},
token: {
type: String,
- required: [true, 'Token is required'],
- unique: true,
+ required: function () {
+ return this.role === "student";
+ },
+ // Note: unique index will be handled separately below
},
-
isActivated: {
type: Boolean,
default: false,
@@ -105,7 +105,7 @@ const userTokenRequestSchema = new mongoose.Schema(
},
status: {
type: String,
- enum: ['pending', 'activated', 'expired', 'deleted','deactivated'],
+ enum: ['pending', 'activated', 'expired', 'deleted', 'deactivated'],
default: 'pending',
},
},
@@ -114,6 +114,20 @@ const userTokenRequestSchema = new mongoose.Schema(
}
);
+userTokenRequestSchema.index(
+ { token: 1 },
+ {
+ unique: true,
+ partialFilterExpression: { isStudent: true, token: { $exists: true, $ne: null } }
+ }
+);
+userTokenRequestSchema.index(
+ { soonerId: 1 },
+ {
+ unique: true,
+ partialFilterExpression: { isStudent: true, soonerId: { $exists: true, $ne: null } }
+ }
+);
// Automatically set expiresAt to 5 days after requestedAt
userTokenRequestSchema.pre('save', function (next) {
if (!this.expiresAt) {
@@ -124,12 +138,16 @@ userTokenRequestSchema.pre('save', function (next) {
next();
});
+// Auto-expire unactivated requests after 5 days
userTokenRequestSchema.index(
{ requestedAt: 1 },
{
- expireAfterSeconds: 432000,
+ expireAfterSeconds: 432000, // 5 days
partialFilterExpression: { isActivated: false },
}
);
-module.exports = mongoose.model('UserTokenRequest', userTokenRequestSchema);
\ No newline at end of file
+// โ
NEW: Make token unique only if token exists (Partial Unique Index)
+
+
+module.exports = mongoose.model('UserTokenRequest', userTokenRequestSchema);
diff --git a/server/models/User.js b/server/models/User.js
index 59aecfdd8..ee8181c4c 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -18,6 +18,9 @@ const userSchema = new mongoose.Schema({
type: String,
required: true,
},
+ soonerId: {
+ type: String,
+ },
createdAt: {
type: Date,
default: Date.now,
@@ -25,4 +28,3 @@ const userSchema = new mongoose.Schema({
});
module.exports = mongoose.model("User", userSchema);
-
diff --git a/server/routes/studentRoutes.js b/server/routes/studentRoutes.js
index 26716b7c2..70311336e 100644
--- a/server/routes/studentRoutes.js
+++ b/server/routes/studentRoutes.js
@@ -4,27 +4,31 @@ 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 });
+ const internshipData = await InternshipRequest.findOne({
+ "student.email": ouEmail,
+ });
+
if (!internshipData) {
// No record found, return a specific flag
- return res.status(200).json({ message: "No internship request found", approvalStatus: "not_submitted" });
+ return res.status(200).json({
+ message: "No internship request found",
+ approvalStatus: "not_submitted",
+ });
}
+ const { supervisor_status, coordinator_status } = internshipData;
+
- const approvalStatus = internshipData.status;
+ const approvalStatus =
+ (supervisor_status == "pending" || supervisor_status == "approved" ) ? supervisor_status : coordinator_status
+
+ console.log(supervisor_status, coordinator_status)
return res.status(200).json({ message: "Success", approvalStatus });
} catch (error) {
@@ -33,6 +37,54 @@ router.post("/", async (req, res) => {
}
});
-
+// Fetch interneeName, soonerId, interneeEmail by OU email passed as query string
+router.get("/me", async (req, res) => {
+ const { ouEmail } = req.query;
+ if (!ouEmail) {
+ return res
+ .status(400)
+ .json({ message: "Missing query parameter: ouEmail" });
+ }
+
+ try {
+ // look in the TokenRequest collection where your student doc lives
+ const student = await TokenRequest.findOne({ ouEmail }).select(
+ "fullName soonerId ouEmail"
+ );
+
+ if (!student) {
+ return res.status(404).json({ message: "Student not found" });
+ }
+
+ return res.json({
+ interneeName: student.fullName,
+ soonerId: student.soonerId,
+ interneeEmail: student.ouEmail,
+ });
+ } catch (err) {
+ console.error("Error in GET /api/student/me:", err);
+ return res.status(500).json({ message: "Server error" });
+ }
+});
+
+// DELETE /api/student/account/:id
+router.delete("/account/:id", async (req, res) => {
+ try {
+ const studentId = req.params.id;
+
+ await TokenRequest.findByIdAndDelete(studentId);
+
+ await InternshipRequest.deleteMany({ student: studentId });
+
+ return res
+ .status(200)
+ .json({ message: "Account and related data deleted" });
+ } catch (err) {
+ console.error("Error deleting account:", err);
+ return res
+ .status(500)
+ .json({ message: "Server error while deleting account" });
+ }
+});
-module.exports = router;
\ No newline at end of file
+module.exports = router;
diff --git a/server/routes/token.js b/server/routes/token.js
index 5b663911b..6c8387995 100644
--- a/server/routes/token.js
+++ b/server/routes/token.js
@@ -5,7 +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 User = require("../models/User");
const JWT_SECRET = process.env.JWT_SECRET;
const FRONTEND_URL = process.env.FRONTEND_URL;
@@ -15,10 +15,13 @@ const hashToken = (token) => {
return crypto.createHash("sha256").update(token).digest("hex");
};
+// ---------------------------------- TOKEN REQUEST ----------------------------------
+// ---------------------------------- TOKEN REQUEST ----------------------------------
router.post("/request", async (req, res) => {
try {
const { fullName, ouEmail, soonerId, password, semester, academicAdvisor, role } = req.body;
- if (!fullName || !ouEmail || !password || !semester) {
+
+ if (!fullName || !ouEmail || !password || !semester || !role) {
return res.status(400).json({ error: "All fields are required." });
}
@@ -27,71 +30,84 @@ router.post("/request", async (req, res) => {
return res.status(401).json({ error: "Token request already exists for this email." });
}
- if(role==="student"){
+ if (role.toLowerCase() === "student") {
+ if (!soonerId) {
+ return res.status(400).json({ error: "Sooner ID is required for students." });
+ }
+
const existingSoonerId = await TokenRequest.findOne({ soonerId });
- if(existingSoonerId){
+ 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" });
- const hashedToken = hashToken(plainToken);
- const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
+ let plainToken = null;
+ let hashedToken = null;
+ const isStudent = role.toLowerCase() === "student";
- const requestedAt = new Date();
- const expiresAt = new Date(requestedAt.getTime() + 5 * 24 * 60 * 60 * 1000);
+ if (role.toLowerCase() === "student") {
+ plainToken = jwt.sign({ ouEmail }, JWT_SECRET, { expiresIn: "180d" });
+ hashedToken = hashToken(plainToken);
+ }
+
+ const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const request = new TokenRequest({
fullName,
ouEmail,
- soonerId: role === "student" ? soonerId : "",
+ soonerId: isStudent ? soonerId : undefined,
password: hashedPassword,
semester,
role,
- academicAdvisor: role === "student" ? academicAdvisor : "",
- isStudent: role === "student",
- token: hashedToken,
- requestedAt,
- expiresAt,
- activationLinkSentAt: new Date(),
+ academicAdvisor: isStudent ? academicAdvisor : undefined,
+ isStudent,
+ ...(isStudent && {
+ token: hashedToken,
+ activationLinkSentAt: new Date(),
+ })
});
-
-
await request.save();
-
-
- const activationLink = `${FRONTEND_URL}/activate/${plainToken}`;
- const emailBody = `
-
Hi ${fullName},
-
Thank you for requesting access to the Internship Program Management System (IPMS).
-
Your activation link:
-
${activationLink}
-
Note: This token will expire in 5 days if not activated.
-
Regards, IPMS Team
- `;
-
- await emailService.sendEmail({
- to: ouEmail,
- subject: "Your IPMS Token Activation Link",
- html: emailBody,
- });
- res.status(201).json({
- message: "Token requested and email sent.",
- token: plainToken,
- expiresAt,
- });
+ if (role.toLowerCase() === "student") {
+ const activationLink = `${FRONTEND_URL}/activate/${plainToken}`;
+ const emailBody = `
+
Hi ${fullName},
+
Thank you for requesting access to the Internship Program Management System (IPMS).
+
Your activation link:
+
${activationLink}
+
Note: This token will expire in 5 days if not activated.
+
Regards, IPMS Team
+ `;
+
+ await emailService.sendEmail({
+ to: ouEmail,
+ subject: "Your IPMS Token Activation Link",
+ html: emailBody,
+ });
+
+ res.status(201).json({
+ message: "Token requested and email sent.",
+ token: plainToken,
+ });
+ } else {
+ console.log(`Email not sent - user is not a student: ${ouEmail}`);
+ res.status(201).json({ message: "Token is not required for supervisors or coordinators." });
+ }
} catch (err) {
console.error("Token Request Error:", err);
res.status(500).json({ error: err.message });
}
});
+// ---------------------------------- ACTIVATE TOKEN ----------------------------------
router.post("/activate", async (req, res) => {
try {
+ console.log(" Activation request received at backend");
+
const { token } = req.body;
if (!token) return res.status(400).json({ error: "Token is missing." });
+
const hashedToken = hashToken(token);
const user = await TokenRequest.findOne({ token: hashedToken });
@@ -109,13 +125,16 @@ router.post("/activate", async (req, res) => {
user.expiresAt = sixMonthsLater;
await user.save();
+ console.log("Token activated successfully");
res.json({ message: "Token activated successfully." });
} catch (err) {
+ console.error("Activation error:", err);
res.status(500).json({ error: err.message });
}
});
+// ---------------------------------- LOGIN BY TOKEN (Optional) ----------------------------------
router.post("/login", async (req, res) => {
try {
const { token } = req.body;
@@ -133,6 +152,7 @@ router.post("/login", async (req, res) => {
}
});
+// ---------------------------------- USER LOGIN (Email/Password) ----------------------------------
router.post("/user-login", async (req, res) => {
const { ouEmail, password, role } = req.body;
@@ -144,7 +164,7 @@ router.post("/user-login", async (req, res) => {
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 does not Exist. Try Signing up." });
}
const isMatch = await bcrypt.compare(password, user.password);
@@ -173,13 +193,13 @@ router.post("/user-login", async (req, res) => {
const tokenExpiry = new Date(user.expiresAt);
if (tokenExpiry < now || user.status === "deactivated") {
- if(!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}`
+ message: "Your account is deactivated due to token expiry.",
+ renewalLink: `${FRONTEND_URL}/renew-token/${user.token}`,
});
}
}
@@ -191,6 +211,28 @@ router.post("/user-login", async (req, res) => {
}
});
+router.delete("/delete", async (req, res) => {
+ try {
+ const { ouEmail } = req.body;
+
+ if (!ouEmail) {
+ return res.status(400).json({ error: "Email is not found for deletion." });
+ }
+
+ const user = await TokenRequest.findOneAndDelete({ouEmail});
+
+ if (!user) {
+ return res.status(404).json({ error: "User not found." });
+ }
+
+ res.status(200).json({ message: "User deleted successfully." });
+
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// ---------------------------------- DEACTIVATE TOKEN (SOFT DELETE) ----------------------------------
router.delete("/deactivate", async (req, res) => {
try {
const { token, ouEmail } = req.body;
@@ -201,9 +243,6 @@ router.delete("/deactivate", async (req, res) => {
let filter = {};
if (token) {
- if (typeof token !== "string") {
- return res.status(400).json({ error: "Token must be a string." });
- }
const hashedToken = hashToken(token);
filter = { token: hashedToken };
} else {
@@ -230,6 +269,8 @@ router.delete("/deactivate", async (req, res) => {
}
});
+// ---------------------------------- TOKEN RENEWAL ----------------------------------
+// ---------------------------------- TOKEN RENEWAL ----------------------------------
router.post("/renew", async (req, res) => {
try {
const { token } = req.body;
@@ -238,6 +279,7 @@ router.post("/renew", async (req, res) => {
return res.status(400).json({ message: "Token is required." });
}
+ // const hashedToken = hashToken(token);
const user = await TokenRequest.findOne({ token: token });
if (!user) {
@@ -272,4 +314,4 @@ router.post("/renew", async (req, res) => {
}
});
-module.exports = router;
+module.exports = router;
\ No newline at end of file
diff --git a/server/services/emailService.js b/server/services/emailService.js
index e2b1118e8..b784325e0 100644
--- a/server/services/emailService.js
+++ b/server/services/emailService.js
@@ -18,8 +18,8 @@ class EmailService {
});
this.defaultSender =
- process.env.EMAIL_DEFAULT_SENDER ||
- "Internship Program Management System
";
+ process.env.EMAIL_DEFAULT_SENDER ||
+ "Internship Program Management System ";
}
/**
@@ -33,6 +33,7 @@ class EmailService {
* @param {Array} [options.attachments] - Array of attachment objects
* @param {Array} [options.cc] - Carbon copy recipients
* @param {Array} [options.bcc] - Blind carbon copy recipients
+ * @param {string} [options.role] - User role (optional, to conditionally skip email)
* @returns {Promise} - Result of the email sending operation
*/
async sendEmail(options) {
@@ -44,6 +45,7 @@ class EmailService {
};
}
+
const mailOptions = {
from: options.from || this.defaultSender,
to: options.to,
@@ -52,7 +54,7 @@ class EmailService {
text: options.text || options.html.replace(/<[^>]*>/g, ""),
attachments: options.attachments || [],
};
-
+
// Add optional fields if provided
if (options.cc) mailOptions.cc = options.cc;
if (options.bcc) mailOptions.bcc = options.bcc;
@@ -66,6 +68,7 @@ class EmailService {
}
}
}
+
// Create and export a singleton instance
const emailService = new EmailService();
module.exports = emailService;