Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <no-reply@paisable.com>"

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
Expand Down
8 changes: 8 additions & 0 deletions backend/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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' });
}
Expand Down
76 changes: 66 additions & 10 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
108 changes: 108 additions & 0 deletions backend/utils/sendEmail.js
Original file line number Diff line number Diff line change
@@ -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 });
}
Comment on lines +30 to +32
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sendRaw function for SendGrid catches errors but doesn't re-throw them, causing the function to return undefined instead of propagating the error. This makes error handling inconsistent with the caller's expectations in sendWelcomeEmail.

Copilot uses AI. Check for mistakes.
};
} 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 {
Comment on lines +51 to +56
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SMTP transporter verification is executed asynchronously at module load time without awaiting completion. This could lead to race conditions where sendRaw attempts to use an unverified transporter. Consider moving this verification inside sendRaw or making it a blocking operation during initialization.

Suggested change
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 {
// Ensure transporter is verified before sending emails
let smtpVerified = false;
let smtpVerifyPromise = null;
sendRaw = async ({ to, from, subject, text, html }) => {
try {
// Verify transporter once before first send
if (!smtpVerified) {
if (!smtpVerifyPromise) {
smtpVerifyPromise = transport.verify()
.then(() => {
smtpVerified = true;
console.info("SMTP transporter verified");
})
.catch(err => {
console.warn("SMTP transporter verification failed", { message: err.message });
throw err;
});
}
await smtpVerifyPromise;
}

Copilot uses AI. Check for mistakes.
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 });
}
Comment on lines +65 to +67
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sendRaw function for SMTP catches errors but doesn't re-throw them, causing the function to return undefined instead of propagating the error. This makes error handling inconsistent with the caller's expectations in sendWelcomeEmail.

Copilot uses AI. Check for mistakes.
};
}

// Inline welcome email template
function renderWelcomeTemplate(name = "") {
const html = `
<div style="font-family: Arial, sans-serif; line-height:1.4; color:#333;">
<h2>Welcome to Paisable${name ? ", " + name : ""}!</h2>
<p>Thank you for creating an account. We're excited to have you on board.</p>
<p>Here are a few things to get started:</p>
<ul>
<li>Log in and complete your profile</li>
<li>Explore transactions and receipts</li>
<li>Contact support if you need help</li>
</ul>
<p>Cheers,<br/>The Paisable Team</p>
</div>
`;

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,
};