diff --git a/Dockerfile b/Dockerfile index fad2978..88995f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,8 +33,7 @@ RUN apt-get update && apt-get install -y \ xdg-utils \ wget \ --no-install-recommends && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* + apt-get clean && rm -rf /var/lib/apt/lists/* # Set working directory WORKDIR /app @@ -43,17 +42,20 @@ WORKDIR /app ARG DATABASE_URL ENV DATABASE_URL=${DATABASE_URL} -# Copy package files and install deps +# Skip Puppeteer Chromium download +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true + +# Copy package files and install dependencies COPY package*.json ./ -RUN npm install --omit=dev +RUN npm ci --omit=dev -# Copy source +# Copy source code COPY . . # Make build.sh executable RUN chmod +x ./build.sh -# Expose app port +# Expose port EXPOSE 5000 # Start diff --git a/config/nodeMailer.js b/config/nodeMailer.js index effadfd..bc74138 100644 --- a/config/nodeMailer.js +++ b/config/nodeMailer.js @@ -1,4 +1,12 @@ // config/nodeMailer.js +// DEPRECATED: This file has been replaced by Resend API +// See: config/resend.js and utils/email/nodeMailer.js for new implementation +// Date: January 2026 + +/* ============================================================ + ORIGINAL NODEMAILER CONFIGURATION (COMMENTED OUT) + ============================================================ + const nodemailer = require("nodemailer"); // Primary transporter (e.g., Gmail) @@ -43,3 +51,14 @@ module.exports = { tertiary: mailTransporterTertiary, mailerSend: mailTransporterMailerSend, }; + +============================================================ */ + +// Export empty object to prevent import errors during transition +// This file is no longer used - Resend API is used instead +module.exports = { + primary: null, + secondary: null, + tertiary: null, + mailerSend: null, +}; diff --git a/config/resend.js b/config/resend.js new file mode 100644 index 0000000..c85da16 --- /dev/null +++ b/config/resend.js @@ -0,0 +1,12 @@ +/** + * RESEND EMAIL CONFIGURATION + * This file is kept for backward compatibility but is no longer used. + * Email sending is now handled directly in utils/email/nodeMailer.js + * + * The Resend clients are initialized directly in nodeMailer.js using: + * - RESEND_API_KEY + EMAIL_FROM (primary) + * - RESEND_API_KEY_2 + EMAIL_FROM_2 (fallback) + */ + +// This file is deprecated - Resend clients are created in nodeMailer.js +module.exports = {}; diff --git a/controllers/chatbot/promptBuilder.js b/controllers/chatbot/promptBuilder.js index 2a61d5e..b27a120 100644 --- a/controllers/chatbot/promptBuilder.js +++ b/controllers/chatbot/promptBuilder.js @@ -21,7 +21,11 @@ The user query will be prepended with the current list of FED Team Members in JS 1. Use this injected team data for all questions about roles, current members, and team structure 2. The key properties in the JSON are: 'name', 'access' (role code), 'year', and 'extra' (with 'linkedin', 'github', etc.) 3. Translate the 'access' codes into friendly titles (e.g., DIRECTOR_TECHNICAL -> Director of Technical Team) -4. Founder of FED is 'Niket Raj Dwivedi', The CEO of Medial, mention only when user asks specifically *don't mention yourself in every response*. + +**NAMES TO REMEMBER:** +Mention these when asked by user (don't mention in every response) +1. Founder of FED is 'Niket Raj Dwivedi', The CEO of Medial +2. Our Faculty in Charge is 'Dr. Vishal Pradhan' **PROFESSIONAL LINK FORMATTING (CRITICAL - READ CAREFULLY):** **NEVER OUTPUT HTML TAGS!** You must ONLY use markdown syntax. @@ -111,7 +115,7 @@ Use this to personalize responses about their participation and achievements. 7. **NO RAW URLS:** NEVER show raw URLs like 'https://...' - ALWAYS use markdown links with clean text 8. **CONCISE BLOG LISTS:** When listing multiple blogs, be BRIEF - title + author + link only. No summaries for lists! 9. **MARKDOWN ONLY:** Always use proper markdown [text](url) format for links. Never output HTML tags. -10. **ALUMINI:** WHEN MENTIONS 'ALUMNI' OR A DIFFERENT NAME OTHER THAN TEAM DATA IS GIVEN JUST REPLY ONE WORD: 'ALUMINI'. +10. **ALUMINI:** WHENEVER USER MENTIONS 'ALUMNI' OR A DIFFERENT NAME NOT PRESENT IN TEAM DATA, JUST REPLY WITH ONE WORD: 'ALUMINI'. **EMAIL ESCALATION SYSTEM - CRITICAL:** You have the ability to trigger email sending by outputting a special tag. The user's NEXT message after you trigger email will be sent DIRECTLY to FED without you modifying it. diff --git a/index.js b/index.js index 8aa91cd..60d3e4f 100644 --- a/index.js +++ b/index.js @@ -107,33 +107,38 @@ const PORT = process.env.PORT || 3000; const frontendUrl = process.env.DOMAIN; console.log("Frontend URL:", frontendUrl); + +const allowedOrigins = [ + "https://www.fedkiit.com", + "https://fedkiit.com", + "http://localhost:5173", + "http://localhost:3000" +]; + +// Shared CORS options for both normal and preflight requests +const corsOptions = { + origin: (origin, callback) => { + if (!origin) return callback(null, true); + if (allowedOrigins.includes(origin)) { + return callback(null, true); // reflect request origin + } + callback(null, false); + }, + credentials: true, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"], + exposedHeaders: ["Content-Length"] +}; + // Middlewares app.use(express.json({ limit: '16kb' })); app.use(express.urlencoded({ extended: true, limit: "16kb" })); app.use(cookieParser()); -app.use(cors({ - origin: function (origin, callback) { - // Allow requests with no origin (like mobile apps or curl) - if (!origin) return callback(null, true); - - const allowedOrigins = [ - "http://localhost:5173", - "http://localhost:3000", - "https://fedkiit.com", - "https://www.fedkiit.com" - ]; - - if (allowedOrigins.includes(origin)) { - callback(null, true); - } else { - callback(null, true); // Allow all in dev, restrict in production - } - }, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - credentials: true, -})); -app.options('*', cors()); // handle preflight requests +// Apply CORS for normal requests +app.use(cors(corsOptions)); +// Ensure preflight responses use the same options (no wildcard when credentials) +app.options('*', cors(corsOptions)); // const allowedOrigins = [ // "https://fedkiit.com", diff --git a/package-lock.json b/package-lock.json index 06b43cd..a8b2efa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "puppeteer-extra-plugin-stealth": "^2.11.2", "qrcode": "^1.5.4", "react-otp-input": "^3.1.1", + "resend": "^6.7.0", "sharp": "^0.33.4", "uuid": "^10.0.0", "xlsx": "^0.18.5" @@ -745,6 +746,12 @@ "node": ">=12" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -2379,6 +2386,12 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -4870,6 +4883,26 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, + "node_modules/resend": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.7.0.tgz", + "integrity": "sha512-2ZV0NDZsh4Gh+Nd1hvluZIitmGJ59O4+OxMufymG6Y8uz1Jgt2uS1seSENnkIUlmwg7/dwmfIJC9rAufByz7wA==", + "license": "MIT", + "dependencies": { + "svix": "1.84.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5309,6 +5342,16 @@ "node": ">=0.8" } }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -5392,6 +5435,16 @@ "node": ">=4" } }, + "node_modules/svix": { + "version": "1.84.1", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz", + "integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, "node_modules/tar-fs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", diff --git a/package.json b/package.json index 5703c01..05733d2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "puppeteer-extra-plugin-stealth": "^2.11.2", "qrcode": "^1.5.4", "react-otp-input": "^3.1.1", + "resend": "^6.7.0", "sharp": "^0.33.4", "uuid": "^10.0.0", "xlsx": "^0.18.5" diff --git a/utils/email/nodeMailer.js b/utils/email/nodeMailer.js index f0ed3fe..ca30675 100644 --- a/utils/email/nodeMailer.js +++ b/utils/email/nodeMailer.js @@ -1,60 +1,106 @@ -const { primary, secondary, tertiary, mailerSend } = require("../../config/nodeMailer"); - -function sendMail(to, subject, htmlContent, textContent, attachments = []) { - const mailDetails = { - from: `"FED KIIT Compliance" <${process.env.MAIL_USER}>`, - to, - subject, - replyTo: "fedkiit@gmail.com", +/** + * RESEND EMAIL SERVICE + * Production-ready email sending with 2-domain fallback support + * + * Environment Variables Required: + * - RESEND_API_KEY: API key for primary domain + * - EMAIL_FROM: Primary sender (e.g., "FED KIIT ") + * - RESEND_API_KEY_2: API key for fallback domain + * - EMAIL_FROM_2: Fallback sender (e.g., "FED KIIT ") + */ + +const { Resend } = require("resend"); + +// Initialize Resend clients for both domains +const resendPrimary = process.env.RESEND_API_KEY + ? new Resend(process.env.RESEND_API_KEY) + : null; + +const resendSecondary = process.env.RESEND_API_KEY_2 + ? new Resend(process.env.RESEND_API_KEY_2) + : null; + +/** + * Send email using Resend API with automatic fallback + * Tries primary domain first, falls back to secondary if primary fails + * + * @param {string} to - Recipient email address + * @param {string} subject - Email subject + * @param {string} htmlContent - HTML content of the email + * @param {string} textContent - Plain text content (optional, auto-generated from HTML) + * @param {Array} attachments - Array of attachments [{filename, content}] + * @returns {Promise} - Resend response data + * @throws {Error} - If both primary and secondary fail + */ +async function sendMail(to, subject, htmlContent, textContent, attachments = []) { + // Validate environment + if (!resendPrimary && !resendSecondary) { + throw new Error("[Email] No Resend API keys configured. Set RESEND_API_KEY in .env"); + } + + // Build email options + const emailOptions = { + to: to, + subject: subject, html: htmlContent, text: textContent || htmlContent.replace(/<[^>]+>/g, ""), - ...(attachments.length > 0 && { attachments }), + reply_to: "fedkiit@gmail.com", }; - // Try sending with primary - primary.sendMail(mailDetails, (err, info) => { - if (err) { - console.error("Primary email failed:", err); - - // Try fallback sender - const fallbackDetails = { - ...mailDetails, - from: process.env.MAIL_USER_SECONDARY, - }; - - secondary.sendMail(fallbackDetails, (err2, info2) => { - if (err2) { - console.error("Secondary email also failed:", err2); - - // Try tertiary sender - const tertiaryDetails = { - ...mailDetails, - from: process.env.MAIL_USER_TERTIARY, - }; - tertiary.sendMail(tertiaryDetails, (err3, info3) => { - if (err3) { - mailerSend.sendMail({ ...mailDetails, from: '"FED KIIT Compliance" ' }, (err4, info4) => { - if (err4) { - console.error("MailerSend email also failed:", err4); - } else { - console.log("MailerSend email sent successfully:", info4); - } - }); - console.error("Tertiary email also failed:", err3); - } - else { - console.log("Tertiary email sent successfully:", info3); - } - }); - } else { - console.log("Fallback email sent successfully:", info2); - } + // Add attachments if present + if (attachments && attachments.length > 0) { + emailOptions.attachments = attachments.map(att => ({ + filename: att.filename, + content: att.content, + })); + } + + // ============ TRY PRIMARY SENDER ============ + if (resendPrimary && process.env.EMAIL_FROM) { + try { + console.log(`[Email] Sending via PRIMARY: ${process.env.EMAIL_FROM}`); + + const { data, error } = await resendPrimary.emails.send({ + ...emailOptions, + from: process.env.EMAIL_FROM, }); - } else { - console.log("Primary email sent successfully:", info); + if (!error && data) { + console.log(`[Email] SUCCESS via PRIMARY:`, data.id); + return data; + } + + console.error(`[Email] PRIMARY failed:`, error?.message || "Unknown error"); + } catch (err) { + console.error(`[Email] PRIMARY exception:`, err.message); } - }); + } + + // ============ TRY SECONDARY SENDER (FALLBACK) ============ + if (resendSecondary && process.env.EMAIL_FROM_2) { + try { + console.log(`[Email] Sending via SECONDARY: ${process.env.EMAIL_FROM_2}`); + + const { data, error } = await resendSecondary.emails.send({ + ...emailOptions, + from: process.env.EMAIL_FROM_2, + }); + + if (!error && data) { + console.log(`[Email] SUCCESS via SECONDARY:`, data.id); + return data; + } + + console.error(`[Email] SECONDARY failed:`, error?.message || "Unknown error"); + throw new Error(`Email failed: ${error?.message || "Secondary sender failed"}`); + } catch (err) { + console.error(`[Email] SECONDARY exception:`, err.message); + throw err; + } + } + + // Both failed or not configured + throw new Error("[Email] All email senders failed or not configured"); } module.exports = { sendMail };