Skip to content

Commit 1cc73a1

Browse files
authored
feat: Migrate email service from Nodemailer to Resend (#1294)
* feat: Migrate email service from Nodemailer to Resend #1247 * Update resend.js * Update invite-collab.js * Update env.js * Update resend.js * Update invite-collab.js
1 parent a1b51c2 commit 1cc73a1

6 files changed

Lines changed: 106 additions & 113 deletions

File tree

server/package-lock.json

Lines changed: 41 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"nanoid": "^5.1.2",
4444
"nodemailer": "^7.0.5",
4545
"rate-limiter-flexible": "^7.3.0",
46+
"resend": "^6.1.2",
4647
"sanitize-html": "^2.17.0"
4748
},
4849
"devDependencies": {

server/src/config/env.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,26 @@ dotenv.config();
77
export const PORT = process.env.PORT || 8000;
88
export const NODE_ENV = process.env.NODE_ENV || 'development';
99
export const VITE_SERVER_DOMAIN =
10-
process.env.VITE_SERVER_DOMAIN || 'https://code-a2z-server.vercel.app';
10+
process.env.VITE_SERVER_DOMAIN || 'https://code-a2z-server.vercel.app';
1111

1212
// MongoDB Configuration
1313
export const MONGODB_URL =
14-
process.env.MONGODB_URL || 'mongodb://localhost:27017/code-a2z';
14+
process.env.MONGODB_URL || 'mongodb://localhost:27017/code-a2z';
1515

1616
// JWT Configuration
1717
export const JWT_SECRET_ACCESS_KEY =
18-
process.env.JWT_SECRET_ACCESS_KEY || 'default_secret_key';
18+
process.env.JWT_SECRET_ACCESS_KEY || 'default_secret_key';
1919
export const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7D';
2020

2121
// Cloudinary Configuration (for media uploads)
2222
export const CLOUDINARY_CLOUD_NAME =
23-
process.env.CLOUDINARY_CLOUD_NAME || 'admin';
23+
process.env.CLOUDINARY_CLOUD_NAME || 'admin';
2424
export const CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY || 'admin';
2525
export const CLOUDINARY_API_SECRET =
26-
process.env.CLOUDINARY_API_SECRET || 'admin';
26+
process.env.CLOUDINARY_API_SECRET || 'admin';
2727

28-
// Admin Credentials (for nodemailer & localtunnel)
29-
export const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@example.com';
30-
export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
28+
// Resend / Email Configuration
29+
export const ADMIN_EMAIL =
30+
process.env.ADMIN_EMAIL || "dev.admin@example.com";
31+
export const RESEND_API_KEY =
32+
process.env.RESEND_API_KEY || "dev_resend_key_abc123";

server/src/config/nodemailer.js

Lines changed: 0 additions & 15 deletions
This file was deleted.

server/src/config/resend.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Resend } from "resend";
2+
import { RESEND_API_KEY } from "./env.js";
3+
4+
if (!RESEND_API_KEY) {
5+
throw new Error("Resend API key is not set in environment variables.");
6+
}
7+
8+
const resend = new Resend(RESEND_API_KEY);
9+
10+
export default resend;

server/src/controllers/collaborator/invite-collab.js

Lines changed: 44 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import crypto from 'crypto';
2-
3-
import Collaborator from '../../models/collaborator.model.js';
4-
import Project from '../../models/project.model.js';
5-
import User from '../../models/user.model.js';
6-
7-
import transporter from '../../config/nodemailer.js';
8-
import { sendResponse } from '../../utils/response.js';
9-
import { VITE_SERVER_DOMAIN } from '../../config/env.js';
1+
import crypto from "crypto";
2+
import Collaborator from "../../models/collaborator.model.js";
3+
import Project from "../../models/project.model.js";
4+
import User from "../../models/user.model.js";
5+
import resend from "../../config/resend.js";
6+
import { sendResponse } from "../../utils/response.js";
7+
import { VITE_SERVER_DOMAIN } from "../../config/env.js";
108

119
const invitationToCollaborate = async (req, res) => {
1210
const user_id = req.user;
@@ -15,42 +13,51 @@ const invitationToCollaborate = async (req, res) => {
1513
try {
1614
const user = await User.findById(user_id);
1715
if (!user) {
18-
return sendResponse(res, 404, 'error', 'User not found!', null);
16+
return sendResponse(res, 404, "error", "User not found!", null);
1917
}
2018

2119
const projectToCollaborate = await Project.findOne({
2220
project_id: project_id,
23-
}).populate({ path: 'author', select: 'personal_info.email' });
21+
}).populate({ path: "author", select: "personal_info.email" });
2422

2523
if (!projectToCollaborate) {
26-
return sendResponse(res, 404, 'error', 'Project not found!', null);
24+
return sendResponse(res, 404, "error", "Project not found!", null);
2725
}
2826

29-
// Ensure author is populated and has _id and personal_info
3027
const author = projectToCollaborate.author;
3128
if (!author || !author._id) {
32-
return sendResponse(res, 404, 'error', 'Project author not found!', null);
29+
return sendResponse(res, 404, "error", "Project author not found!", null);
3330
}
34-
if (user._id === author._id) {
31+
32+
if (String(user._id) === String(author._id)) {
3533
return sendResponse(
3634
res,
3735
400,
38-
'error',
39-
'You cannot invite yourself to collaborate on your own project.',
36+
"error",
37+
"You cannot invite yourself to collaborate on your own project.",
4038
null
4139
);
4240
}
4341

4442
const authorEmail = author.personal_info?.email;
45-
46-
const token = crypto.randomBytes(16).toString('hex');
43+
const token = crypto.randomBytes(16).toString("hex");
4744
const acceptLink = `${VITE_SERVER_DOMAIN}/api/collaboration/accept/${token}`;
4845
const rejectLink = `${VITE_SERVER_DOMAIN}/api/collaboration/reject/${token}`;
4946

50-
const mailOptions = {
51-
from: process.env.ADMIN_EMAIL,
52-
to: authorEmail,
53-
subject: 'Collaboration Invitation',
47+
if (!authorEmail) {
48+
return sendResponse(
49+
res,
50+
400,
51+
"error",
52+
"Project author does not have an email address.",
53+
null
54+
);
55+
}
56+
57+
await resend.emails.send({
58+
from: `The Code A2Z Team <${process.env.ADMIN_EMAIL}>`,
59+
to: [authorEmail],
60+
subject: "Collaboration Invitation",
5461
html: `
5562
<p>Hi,</p>
5663
<p><strong>${user?.personal_info?.fullname}</strong> has requested to collaborate on your project "${projectToCollaborate.title}".</p>
@@ -62,44 +69,23 @@ const invitationToCollaborate = async (req, res) => {
6269
<p>Your response will help us update the project collaboration status accordingly.</p>
6370
<p>Thanks for being part of the community,<br/>The Code A2Z Team</p>
6471
`,
65-
};
72+
});
6673

67-
transporter.sendMail(mailOptions, async (error, info) => {
68-
if (error) {
69-
console.error('Error sending email:', error);
70-
return sendResponse(
71-
res,
72-
500,
73-
'error',
74-
'Failed to send invitation email',
75-
null
76-
);
77-
}
78-
console.log('Email sent:', info.response);
79-
const collaborationData = new Collaborator({
80-
user_id: user_id,
81-
project_id: project_id,
82-
author_id: projectToCollaborate.author,
83-
status: 'pending',
84-
token: token,
85-
});
86-
await collaborationData.save();
87-
return sendResponse(
88-
res,
89-
200,
90-
'success',
91-
'Invitation sent successfully!',
92-
null
93-
);
74+
console.log("Invitation email sent to:", authorEmail);
75+
76+
const collaborationData = new Collaborator({
77+
user_id: user_id,
78+
project_id: project_id,
79+
author_id: projectToCollaborate.author,
80+
status: "pending",
81+
token: token,
9482
});
83+
await collaborationData.save();
84+
85+
return sendResponse(res, 200, "success", "Invitation sent successfully!", null);
9586
} catch (error) {
96-
return sendResponse(
97-
res,
98-
500,
99-
'error',
100-
error.message || 'Internal Server Error',
101-
null
102-
);
87+
console.error("Error in invitation process:", error);
88+
return sendResponse(res, 500, "error", "Failed to send invitation", null);
10389
}
10490
};
10591

0 commit comments

Comments
 (0)