diff --git a/README.md b/README.md index 3c02992..7cbffbd 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,18 @@ JWT_SECRET=your-secret-key GEMINI_API_KEY=your-gemini-api-key ``` +For welcome email setup: +```env +SENDGRID_API_KEY=SG.your-sendgrid-api-key +EMAIL_FROM="Paisable " + +EMAIL_USER=your@gmail.com +EMAIL_PASS=your-app-password +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_SECURE=false +``` + Start the backend: ```bash diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index f554875..00c2fc9 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -1,6 +1,7 @@ const User = require('../models/User'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); +const { sendWelcomeEmail } = require('../utils/sendEmail'); // Function to generate JWT const generateToken = (id) => { @@ -37,6 +38,13 @@ const signup = async (req, res) => { email: user.email, token: generateToken(user._id), }); + + // sends welcome email + sendWelcomeEmail(user.email, user.name || '') + .then(() => console.log('Welcome email sent to', user.email)) + .catch(err => console.error('Welcome email failed for', user.email, err && err.message)); + + } else { res.status(400).json({ message: 'Invalid user data' }); } diff --git a/backend/package-lock.json b/backend/package-lock.json index cd54df5..e8a7108 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@google/generative-ai": "^0.24.1", + "@sendgrid/mail": "^8.1.6", "bcryptjs": "^3.0.2", "cors": "^2.8.5", "dotenv": "^17.2.2", @@ -19,6 +20,7 @@ "mongodb": "^6.19.0", "mongoose": "^8.18.1", "multer": "^2.0.2", + "nodemailer": "^7.0.6", "papaparse": "^5.5.3" }, "devDependencies": { @@ -954,6 +956,44 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@sendgrid/client": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz", + "integrity": "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==", + "license": "MIT", + "dependencies": { + "@sendgrid/helpers": "^8.0.0", + "axios": "^1.12.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.6.tgz", + "integrity": "sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==", + "license": "MIT", + "dependencies": { + "@sendgrid/client": "^8.1.5", + "@sendgrid/helpers": "^8.0.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1228,9 +1268,19 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/b4a": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.1.tgz", @@ -1783,7 +1833,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -1970,7 +2019,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1980,7 +2028,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -2147,7 +2194,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2436,7 +2482,6 @@ "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, "funding": [ { "type": "individual", @@ -2457,7 +2502,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -2474,7 +2518,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2484,7 +2527,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -2720,7 +2762,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4360,6 +4401,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz", + "integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -4711,6 +4761,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", diff --git a/backend/package.json b/backend/package.json index b92d83b..1adf516 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@google/generative-ai": "^0.24.1", + "@sendgrid/mail": "^8.1.6", "bcryptjs": "^3.0.2", "cors": "^2.8.5", "dotenv": "^17.2.2", @@ -24,6 +25,7 @@ "mongodb": "^6.19.0", "mongoose": "^8.18.1", "multer": "^2.0.2", + "nodemailer": "^7.0.6", "papaparse": "^5.5.3" }, "devDependencies": { diff --git a/backend/utils/sendEmail.js b/backend/utils/sendEmail.js new file mode 100644 index 0000000..8c5a1cc --- /dev/null +++ b/backend/utils/sendEmail.js @@ -0,0 +1,108 @@ +require("dotenv").config(); + +let sendRaw; + +// Validate critical env vars +if (!process.env.SENDGRID_API_KEY && (!process.env.EMAIL_USER || !process.env.EMAIL_PASS)) { + console.warn( + "Warning: No email provider fully configured. Emails may fail to send. " + + "Set SENDGRID_API_KEY for SendGrid or EMAIL_USER/EMAIL_PASS for SMTP." + ); +} + +const emailFrom = process.env.EMAIL_FROM || process.env.EMAIL_USER || "no-reply@paisable.com"; + +// Use SendGrid if API key is available +if (process.env.SENDGRID_API_KEY) { + const sgMail = require("@sendgrid/mail"); + sgMail.setApiKey(process.env.SENDGRID_API_KEY); + + sendRaw = async ({ to, from, subject, text, html }) => { + try { + const msg = { + to, + from: from || emailFrom, + subject, + text, + html, + }; + return await sgMail.send(msg); + } catch (err) { + console.error("SendGrid email failed", { to, subject, message: err.message }); + } + }; +} else { + // Fallback to SMTP via Nodemailer + const nodemailer = require("nodemailer"); + + const transport = nodemailer.createTransport({ + host: process.env.SMTP_HOST || "smtp.gmail.com", + port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 587, + secure: process.env.SMTP_SECURE === "true", + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + pool: true, + maxConnections: 5, + maxMessages: 100, + }); + + transport.verify() + .then(() => console.info("SMTP transporter verified")) + .catch(err => console.warn("SMTP transporter verification failed", { message: err.message })); + + sendRaw = async ({ to, from, subject, text, html }) => { + try { + const mailOptions = { + from: from || emailFrom, + to, + subject, + text, + html, + }; + return await transport.sendMail(mailOptions); + } catch (err) { + console.error("SMTP email failed", { to, subject, message: err.message }); + } + }; +} + +// Inline welcome email template +function renderWelcomeTemplate(name = "") { + const html = ` +
+

Welcome to Paisable${name ? ", " + name : ""}!

+

Thank you for creating an account. We're excited to have you on board.

+

Here are a few things to get started:

+ +

Cheers,
The Paisable Team

+
+ `; + + const text = `Hi ${name}\n\nWelcome to Paisable! Your account has been created.\n\n- Log in and complete your profile\n- Explore transactions and receipts\n- Contact support if you need help\n\nCheers,\nThe Paisable Team`; + + return { html, text }; +} + +// Fire-and-forget welcome email +async function sendWelcomeEmail(to, name = "") { + if (!to) return; + const { html, text } = renderWelcomeTemplate(name); + const subject = "Welcome to Paisable!"; + try { + await sendRaw({ to, subject, text, html }); + console.info("Welcome email sent", { to }); + } catch (err) { + console.error("Failed to send welcome email", { to, message: err.message }); + } +} + +module.exports = { + sendWelcomeEmail, + sendRaw, +};