From 2704a32e59c17bf91b3806938d74b66a0403e476 Mon Sep 17 00:00:00 2001
From: 100NikhilBro <100pranjalgupta@gmail.com>
Date: Fri, 10 Oct 2025 05:18:32 +0530
Subject: [PATCH 1/6] feat: Migrate email service from Nodemailer to Resend
#1247
---
server/package-lock.json | 73 ++---
server/package.json | 1 +
server/src/config/env.js | 16 +-
server/src/config/nodemailer.js | 15 --
server/src/config/resend.js | 10 +
.../controllers/collaborator/invite-collab.js | 255 ++++++++++++------
6 files changed, 242 insertions(+), 128 deletions(-)
delete mode 100644 server/src/config/nodemailer.js
create mode 100644 server/src/config/resend.js
diff --git a/server/package-lock.json b/server/package-lock.json
index 8b909dbc..76bf6ce7 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -26,6 +26,7 @@
"nanoid": "^5.1.2",
"nodemailer": "^7.0.5",
"rate-limiter-flexible": "^7.3.0",
+ "resend": "^6.1.2",
"sanitize-html": "^2.17.0"
},
"devDependencies": {
@@ -2279,6 +2280,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/hawk": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
+ "integrity": "sha512-X8xbmTc1cbPXcQV4WkLcRMALuyoxhfpFATmyuCxJPOAvrDS4DNnsTAOmKUxMTOWU6TzrTOkxPKwIx5ZOpJVSrg==",
+ "deprecated": "This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.",
+ "dependencies": {
+ "boom": "2.x.x",
+ "cryptiles": "2.x.x",
+ "hoek": "2.x.x",
+ "sntp": "1.x.x"
+ },
+ "engines": {
+ "node": ">=0.10.32"
+ }
+ },
"node_modules/helmet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
@@ -2287,6 +2303,15 @@
"node": ">=18.0.0"
}
},
+ "node_modules/hoek": {
+ "version": "2.16.3",
+ "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
+ "integrity": "sha512-V6Yw1rIcYV/4JsnggjBU0l4Kr+EXhpwqXRusENU1Xx6ro00IHPHYNynCuBTOZAPlr3AAmLvchH9I7N/VUdvOwQ==",
+ "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).",
+ "engines": {
+ "node": ">=0.10.40"
+ }
+ },
"node_modules/hpp": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz",
@@ -3239,38 +3264,6 @@
"url": "https://opencollective.com/mongoose"
}
},
- "node_modules/mongoose/node_modules/gaxios": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
- "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "extend": "^3.0.2",
- "https-proxy-agent": "^5.0.0",
- "is-stream": "^2.0.0",
- "node-fetch": "^2.6.9"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/mongoose/node_modules/gcp-metadata": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
- "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "gaxios": "^5.0.0",
- "json-bigint": "^1.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/mongoose/node_modules/mongodb": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz",
@@ -3989,6 +3982,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/resend": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/resend/-/resend-6.1.2.tgz",
+ "integrity": "sha512-C9Q+YkRe57P8MQlkHG3yatSR/B6sqBGA06Ri2DveJfkz9Vm16182FC/iHB0K6IAfmqZ4yRrFebFw1EPuktLtSg==",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@react-email/render": "*"
+ },
+ "peerDependenciesMeta": {
+ "@react-email/render": {
+ "optional": true
+ }
+ }
+ },
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
diff --git a/server/package.json b/server/package.json
index f71a1d6f..f2136339 100644
--- a/server/package.json
+++ b/server/package.json
@@ -43,6 +43,7 @@
"nanoid": "^5.1.2",
"nodemailer": "^7.0.5",
"rate-limiter-flexible": "^7.3.0",
+ "resend": "^6.1.2",
"sanitize-html": "^2.17.0"
},
"devDependencies": {
diff --git a/server/src/config/env.js b/server/src/config/env.js
index 20772585..959df2c7 100644
--- a/server/src/config/env.js
+++ b/server/src/config/env.js
@@ -7,24 +7,28 @@ dotenv.config();
export const PORT = process.env.PORT || 8000;
export const NODE_ENV = process.env.NODE_ENV || 'development';
export const VITE_SERVER_DOMAIN =
- process.env.VITE_SERVER_DOMAIN || 'https://code-a2z-server.vercel.app';
+ process.env.VITE_SERVER_DOMAIN || 'https://code-a2z-server.vercel.app';
// MongoDB Configuration
export const MONGODB_URL =
- process.env.MONGODB_URL || 'mongodb://localhost:27017/code-a2z';
+ process.env.MONGODB_URL || 'mongodb://localhost:27017/code-a2z';
// JWT Configuration
export const JWT_SECRET_ACCESS_KEY =
- process.env.JWT_SECRET_ACCESS_KEY || 'default_secret_key';
+ process.env.JWT_SECRET_ACCESS_KEY || 'default_secret_key';
export const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7D';
// Cloudinary Configuration (for media uploads)
export const CLOUDINARY_CLOUD_NAME =
- process.env.CLOUDINARY_CLOUD_NAME || 'admin';
+ process.env.CLOUDINARY_CLOUD_NAME || 'admin';
export const CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY || 'admin';
export const CLOUDINARY_API_SECRET =
- process.env.CLOUDINARY_API_SECRET || 'admin';
+ process.env.CLOUDINARY_API_SECRET || 'admin';
// Admin Credentials (for nodemailer & localtunnel)
+// export const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@example.com';
+// export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
+
+// Resend Configuration
export const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@example.com';
-export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
+export const RESEND_API_KEY = process.env.RESEND_API_KEY;
\ No newline at end of file
diff --git a/server/src/config/nodemailer.js b/server/src/config/nodemailer.js
deleted file mode 100644
index f3e27770..00000000
--- a/server/src/config/nodemailer.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import nodemailer from 'nodemailer';
-
-import { ADMIN_EMAIL, ADMIN_PASSWORD } from '../config/env.js';
-
-const transporter = nodemailer.createTransport({
- host: 'smtp.gmail.com',
- port: 587,
- secure: false, // true for 465, false for other ports
- auth: {
- user: ADMIN_EMAIL,
- pass: ADMIN_PASSWORD,
- },
-});
-
-export default transporter;
diff --git a/server/src/config/resend.js b/server/src/config/resend.js
new file mode 100644
index 00000000..5a05b803
--- /dev/null
+++ b/server/src/config/resend.js
@@ -0,0 +1,10 @@
+import { Resend } from 'resend';
+import { RESEND_API_KEY } from './env';
+
+if (!RESEND_API_KEY) {
+ throw new Error('Resend API key is not set in environment variables.');
+}
+
+const resend = new Resend(RESEND_API_KEY);
+
+export default resend;
\ No newline at end of file
diff --git a/server/src/controllers/collaborator/invite-collab.js b/server/src/controllers/collaborator/invite-collab.js
index abe03c73..799ee316 100644
--- a/server/src/controllers/collaborator/invite-collab.js
+++ b/server/src/controllers/collaborator/invite-collab.js
@@ -4,54 +4,166 @@ import Collaborator from '../../models/collaborator.model.js';
import Project from '../../models/project.model.js';
import User from '../../models/user.model.js';
-import transporter from '../../config/nodemailer.js';
+// import transporter from '../../config/nodemailer.js';
+import resend from '../../config/resend.js';
+
import { sendResponse } from '../../utils/response.js';
import { VITE_SERVER_DOMAIN } from '../../config/env.js';
-const invitationToCollaborate = async (req, res) => {
- const user_id = req.user;
- const { project_id } = req.body;
+// const invitationToCollaborate = async(req, res) => {
+// const user_id = req.user;
+// const { project_id } = req.body;
- try {
- const user = await User.findById(user_id);
- if (!user) {
- return sendResponse(res, 404, 'error', 'User not found!', null);
- }
+// try {
+// const user = await User.findById(user_id);
+// if (!user) {
+// return sendResponse(res, 404, 'error', 'User not found!', null);
+// }
- const projectToCollaborate = await Project.findOne({
- project_id: project_id,
- }).populate({ path: 'author', select: 'personal_info.email' });
+// const projectToCollaborate = await Project.findOne({
+// project_id: project_id,
+// }).populate({ path: 'author', select: 'personal_info.email' });
- if (!projectToCollaborate) {
- return sendResponse(res, 404, 'error', 'Project not found!', null);
- }
+// if (!projectToCollaborate) {
+// return sendResponse(res, 404, 'error', 'Project not found!', null);
+// }
- // Ensure author is populated and has _id and personal_info
- const author = projectToCollaborate.author;
- if (!author || !author._id) {
- return sendResponse(res, 404, 'error', 'Project author not found!', null);
- }
- if (user._id === author._id) {
- return sendResponse(
- res,
- 400,
- 'error',
- 'You cannot invite yourself to collaborate on your own project.',
- null
- );
- }
+// // Ensure author is populated and has _id and personal_info
+// const author = projectToCollaborate.author;
+// if (!author || !author._id) {
+// return sendResponse(res, 404, 'error', 'Project author not found!', null);
+// }
+// if (user._id === author._id) {
+// return sendResponse(
+// res,
+// 400,
+// 'error',
+// 'You cannot invite yourself to collaborate on your own project.',
+// null
+// );
+// }
+
+// const authorEmail = author.personal_info ? .email;
+
+// const token = crypto.randomBytes(16).toString('hex');
+// const acceptLink = `${VITE_SERVER_DOMAIN}/api/collaboration/accept/${token}`;
+// const rejectLink = `${VITE_SERVER_DOMAIN}/api/collaboration/reject/${token}`;
+
+
+
+// const mailOptions = {
+// from: process.env.ADMIN_EMAIL,
+// to: authorEmail,
+// subject: 'Collaboration Invitation',
+// html: `
+//
Hi,
+// ${user?.personal_info?.fullname} has requested to collaborate on your project "${projectToCollaborate.title}".
+// If you’d like to join, please click below:
+//
+// Accept Invitation |
+// Reject Invitation
+//
+// Your response will help us update the project collaboration status accordingly.
+// Thanks for being part of the community,
The Code A2Z Team
+// `,
+// };
+
+// transporter.sendMail(mailOptions, async(error, info) => {
+// if (error) {
+// console.error('Error sending email:', error);
+// return sendResponse(
+// res,
+// 500,
+// 'error',
+// 'Failed to send invitation email',
+// null
+// );
+// }
+// console.log('Email sent:', info.response);
+// const collaborationData = new Collaborator({
+// user_id: user_id,
+// project_id: project_id,
+// author_id: projectToCollaborate.author,
+// status: 'pending',
+// token: token,
+// });
+
+
+// await collaborationData.save();
+// return sendResponse(
+// res,
+// 200,
+// 'success',
+// 'Invitation sent successfully!',
+// null
+// );
+// });
+// } catch (error) {
+// return sendResponse(
+// res,
+// 500,
+// 'error',
+// error.message || 'Internal Server Error',
+// null
+// );
+// }
+// };
+
+// export default invitationToCollaborate;
- const authorEmail = author.personal_info?.email;
- const token = crypto.randomBytes(16).toString('hex');
- const acceptLink = `${VITE_SERVER_DOMAIN}/api/collaboration/accept/${token}`;
- const rejectLink = `${VITE_SERVER_DOMAIN}/api/collaboration/reject/${token}`;
- const mailOptions = {
- from: process.env.ADMIN_EMAIL,
- to: authorEmail,
- subject: 'Collaboration Invitation',
- html: `
+const invitationToCollaborate = async(req, res) => {
+ const user_id = req.user;
+ const { project_id } = req.body;
+
+ try {
+ const user = await User.findById(user_id);
+ if (!user) {
+ return sendResponse(res, 404, 'error', 'User not found!', null);
+ }
+
+ const projectToCollaborate = await Project.findOne({
+ project_id: project_id,
+ }).populate({ path: 'author', select: 'personal_info.email' });
+
+ if (!projectToCollaborate) {
+ return sendResponse(res, 404, 'error', 'Project not found!', null);
+ }
+
+ const author = projectToCollaborate.author;
+ if (!author || !author._id) {
+ return sendResponse(res, 404, 'error', 'Project author not found!', null);
+ }
+
+ // This logic correctly prevents self-invitation
+ if (String(user._id) === String(author._id)) {
+ return sendResponse(
+ res,
+ 400,
+ 'error',
+ 'You cannot invite yourself to collaborate on your own project.',
+ null
+ );
+ }
+
+ const authorEmail = author.personal_info ? .email;
+ const token = crypto.randomBytes(16).toString('hex');
+ const acceptLink = `${VITE_SERVER_DOMAIN}/api/collaboration/accept/${token}`;
+ const rejectLink = `${VITE_SERVER_DOMAIN}/api/collaboration/reject/${token}`;
+
+ if (!authorEmail) {
+ return sendResponse(res, 400, 'error', 'Project author does not have an email address.', null);
+ }
+
+ // 2. CHANGED: Swapped transporter.sendMail for resend.emails.send
+ await resend.emails.send({
+ // 3. UPDATED: Use an email from your verified domain
+ from: `The Code A2Z Team <${process.env.ADMIN_EMAIL}>`,
+ // 4. UPDATED: 'to' must be an array
+ to: [authorEmail],
+ subject: 'Collaboration Invitation',
+ html: `
Hi,
${user?.personal_info?.fullname} has requested to collaborate on your project "${projectToCollaborate.title}".
If you’d like to join, please click below:
@@ -62,45 +174,38 @@ const invitationToCollaborate = async (req, res) => {
Your response will help us update the project collaboration status accordingly.
Thanks for being part of the community,
The Code A2Z Team
`,
- };
+ });
+
+ // 5. CHANGED: Success logic now runs sequentially after the email is sent
+ console.log('Invitation email sent to:', authorEmail);
+
+ const collaborationData = new Collaborator({
+ user_id: user_id,
+ project_id: project_id,
+ author_id: projectToCollaborate.author,
+ status: 'pending',
+ token: token,
+ });
+ await collaborationData.save();
- transporter.sendMail(mailOptions, async (error, info) => {
- if (error) {
- console.error('Error sending email:', error);
return sendResponse(
- res,
- 500,
- 'error',
- 'Failed to send invitation email',
- null
+ res,
+ 200,
+ 'success',
+ 'Invitation sent successfully!',
+ null
);
- }
- console.log('Email sent:', info.response);
- const collaborationData = new Collaborator({
- user_id: user_id,
- project_id: project_id,
- author_id: projectToCollaborate.author,
- status: 'pending',
- token: token,
- });
- await collaborationData.save();
- return sendResponse(
- res,
- 200,
- 'success',
- 'Invitation sent successfully!',
- null
- );
- });
- } catch (error) {
- return sendResponse(
- res,
- 500,
- 'error',
- error.message || 'Internal Server Error',
- null
- );
- }
+ } catch (error) {
+ // This single catch block now handles errors from the database AND email sending
+ console.error('Error in invitation process:', error);
+ return sendResponse(
+ res,
+ 500,
+ 'error',
+ 'Failed to send invitation',
+ null
+ );
+ }
};
-export default invitationToCollaborate;
+export default invitationToCollaborate;
\ No newline at end of file
From a0dac5790f0e5d355d1e425e00698a015809faf1 Mon Sep 17 00:00:00 2001
From: Nikhil Gupta <100pranjalgupta@gmail.com>
Date: Fri, 10 Oct 2025 05:25:21 +0530
Subject: [PATCH 2/6] Update resend.js
---
server/src/config/resend.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/server/src/config/resend.js b/server/src/config/resend.js
index 5a05b803..d04df452 100644
--- a/server/src/config/resend.js
+++ b/server/src/config/resend.js
@@ -1,10 +1,10 @@
import { Resend } from 'resend';
-import { RESEND_API_KEY } from './env';
+import { RESEND_API_KEY } from './env.js';
if (!RESEND_API_KEY) {
- throw new Error('Resend API key is not set in environment variables.');
+ throw new Error('Resend API key is not set in environment variables.');
}
const resend = new Resend(RESEND_API_KEY);
-export default resend;
\ No newline at end of file
+export default resend;
From af105f97cec7601dc5fc60c38a23cfeaea2cdec8 Mon Sep 17 00:00:00 2001
From: Nikhil Gupta <100pranjalgupta@gmail.com>
Date: Fri, 10 Oct 2025 05:26:39 +0530
Subject: [PATCH 3/6] Update invite-collab.js
---
.../controllers/collaborator/invite-collab.js | 165 ++++++++----------
1 file changed, 77 insertions(+), 88 deletions(-)
diff --git a/server/src/controllers/collaborator/invite-collab.js b/server/src/controllers/collaborator/invite-collab.js
index 799ee316..2f15e50d 100644
--- a/server/src/controllers/collaborator/invite-collab.js
+++ b/server/src/controllers/collaborator/invite-collab.js
@@ -43,14 +43,12 @@ import { VITE_SERVER_DOMAIN } from '../../config/env.js';
// );
// }
-// const authorEmail = author.personal_info ? .email;
+// const authorEmail = author.personal_info?.email;
// const token = crypto.randomBytes(16).toString('hex');
// const acceptLink = `${VITE_SERVER_DOMAIN}/api/collaboration/accept/${token}`;
// const rejectLink = `${VITE_SERVER_DOMAIN}/api/collaboration/reject/${token}`;
-
-
// const mailOptions = {
// from: process.env.ADMIN_EMAIL,
// to: authorEmail,
@@ -88,7 +86,6 @@ import { VITE_SERVER_DOMAIN } from '../../config/env.js';
// token: token,
// });
-
// await collaborationData.save();
// return sendResponse(
// res,
@@ -111,59 +108,63 @@ import { VITE_SERVER_DOMAIN } from '../../config/env.js';
// export default invitationToCollaborate;
+const invitationToCollaborate = async (req, res) => {
+ const user_id = req.user;
+ const { project_id } = req.body;
+
+ try {
+ const user = await User.findById(user_id);
+ if (!user) {
+ return sendResponse(res, 404, 'error', 'User not found!', null);
+ }
+
+ const projectToCollaborate = await Project.findOne({
+ project_id: project_id,
+ }).populate({ path: 'author', select: 'personal_info.email' });
+
+ if (!projectToCollaborate) {
+ return sendResponse(res, 404, 'error', 'Project not found!', null);
+ }
+
+ const author = projectToCollaborate.author;
+ if (!author || !author._id) {
+ return sendResponse(res, 404, 'error', 'Project author not found!', null);
+ }
+ // This logic correctly prevents self-invitation
+ if (String(user._id) === String(author._id)) {
+ return sendResponse(
+ res,
+ 400,
+ 'error',
+ 'You cannot invite yourself to collaborate on your own project.',
+ null
+ );
+ }
-const invitationToCollaborate = async(req, res) => {
- const user_id = req.user;
- const { project_id } = req.body;
-
- try {
- const user = await User.findById(user_id);
- if (!user) {
- return sendResponse(res, 404, 'error', 'User not found!', null);
- }
-
- const projectToCollaborate = await Project.findOne({
- project_id: project_id,
- }).populate({ path: 'author', select: 'personal_info.email' });
-
- if (!projectToCollaborate) {
- return sendResponse(res, 404, 'error', 'Project not found!', null);
- }
-
- const author = projectToCollaborate.author;
- if (!author || !author._id) {
- return sendResponse(res, 404, 'error', 'Project author not found!', null);
- }
-
- // This logic correctly prevents self-invitation
- if (String(user._id) === String(author._id)) {
- return sendResponse(
- res,
- 400,
- 'error',
- 'You cannot invite yourself to collaborate on your own project.',
- null
- );
- }
-
- const authorEmail = author.personal_info ? .email;
- const token = crypto.randomBytes(16).toString('hex');
- const acceptLink = `${VITE_SERVER_DOMAIN}/api/collaboration/accept/${token}`;
- const rejectLink = `${VITE_SERVER_DOMAIN}/api/collaboration/reject/${token}`;
-
- if (!authorEmail) {
- return sendResponse(res, 400, 'error', 'Project author does not have an email address.', null);
- }
-
- // 2. CHANGED: Swapped transporter.sendMail for resend.emails.send
- await resend.emails.send({
- // 3. UPDATED: Use an email from your verified domain
- from: `The Code A2Z Team <${process.env.ADMIN_EMAIL}>`,
- // 4. UPDATED: 'to' must be an array
- to: [authorEmail],
- subject: 'Collaboration Invitation',
- html: `
+ const authorEmail = author.personal_info?.email;
+ const token = crypto.randomBytes(16).toString('hex');
+ const acceptLink = `${VITE_SERVER_DOMAIN}/api/collaboration/accept/${token}`;
+ const rejectLink = `${VITE_SERVER_DOMAIN}/api/collaboration/reject/${token}`;
+
+ if (!authorEmail) {
+ return sendResponse(
+ res,
+ 400,
+ 'error',
+ 'Project author does not have an email address.',
+ null
+ );
+ }
+
+ // 2. CHANGED: Swapped transporter.sendMail for resend.emails.send
+ await resend.emails.send({
+ // 3. UPDATED: Use an email from your verified domain
+ from: `The Code A2Z Team <${process.env.ADMIN_EMAIL}>`,
+ // 4. UPDATED: 'to' must be an array
+ to: [authorEmail],
+ subject: 'Collaboration Invitation',
+ html: `
Hi,
${user?.personal_info?.fullname} has requested to collaborate on your project "${projectToCollaborate.title}".
If you’d like to join, please click below:
@@ -174,38 +175,26 @@ const invitationToCollaborate = async(req, res) => {
Your response will help us update the project collaboration status accordingly.
Thanks for being part of the community,
The Code A2Z Team
`,
- });
-
- // 5. CHANGED: Success logic now runs sequentially after the email is sent
- console.log('Invitation email sent to:', authorEmail);
-
- const collaborationData = new Collaborator({
- user_id: user_id,
- project_id: project_id,
- author_id: projectToCollaborate.author,
- status: 'pending',
- token: token,
- });
- await collaborationData.save();
-
- return sendResponse(
- res,
- 200,
- 'success',
- 'Invitation sent successfully!',
- null
- );
- } catch (error) {
- // This single catch block now handles errors from the database AND email sending
- console.error('Error in invitation process:', error);
- return sendResponse(
- res,
- 500,
- 'error',
- 'Failed to send invitation',
- null
- );
- }
+ });
+
+ // 5. CHANGED: Success logic now runs sequentially after the email is sent
+ console.log('Invitation email sent to:', authorEmail);
+
+ const collaborationData = new Collaborator({
+ user_id: user_id,
+ project_id: project_id,
+ author_id: projectToCollaborate.author,
+ status: 'pending',
+ token: token,
+ });
+ await collaborationData.save();
+
+ return sendResponse(res, 200, 'success', 'Invitation sent successfully!', null);
+ } catch (error) {
+ // This single catch block now handles errors from the database AND email sending
+ console.error('Error in invitation process:', error);
+ return sendResponse(res, 500, 'error', 'Failed to send invitation', null);
+ }
};
-export default invitationToCollaborate;
\ No newline at end of file
+export default invitationToCollaborate;
From 45b99b6f798960a4b7624f2b955ac03f8eb0aac4 Mon Sep 17 00:00:00 2001
From: Nikhil Gupta <100pranjalgupta@gmail.com>
Date: Sat, 11 Oct 2025 13:29:54 +0530
Subject: [PATCH 4/6] Update env.js
---
server/src/config/env.js | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/server/src/config/env.js b/server/src/config/env.js
index 959df2c7..77c98e66 100644
--- a/server/src/config/env.js
+++ b/server/src/config/env.js
@@ -25,10 +25,8 @@ export const CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY || 'admin';
export const CLOUDINARY_API_SECRET =
process.env.CLOUDINARY_API_SECRET || 'admin';
-// Admin Credentials (for nodemailer & localtunnel)
-// export const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@example.com';
-// export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
-
-// Resend Configuration
-export const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@example.com';
-export const RESEND_API_KEY = process.env.RESEND_API_KEY;
\ No newline at end of file
+// Resend / Email Configuration
+export const ADMIN_EMAIL =
+ process.env.ADMIN_EMAIL || "dev.admin@example.com";
+export const RESEND_API_KEY =
+ process.env.RESEND_API_KEY || "dev_resend_key_abc123";
From d3df9ad5fdddaea242c24f22918bdb8f728c62f6 Mon Sep 17 00:00:00 2001
From: Nikhil Gupta <100pranjalgupta@gmail.com>
Date: Sat, 11 Oct 2025 13:31:04 +0530
Subject: [PATCH 5/6] Update resend.js
---
server/src/config/resend.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/server/src/config/resend.js b/server/src/config/resend.js
index d04df452..685c6c61 100644
--- a/server/src/config/resend.js
+++ b/server/src/config/resend.js
@@ -1,8 +1,8 @@
-import { Resend } from 'resend';
-import { RESEND_API_KEY } from './env.js';
+import { Resend } from "resend";
+import { RESEND_API_KEY } from "./env.js";
if (!RESEND_API_KEY) {
- throw new Error('Resend API key is not set in environment variables.');
+ throw new Error("Resend API key is not set in environment variables.");
}
const resend = new Resend(RESEND_API_KEY);
From c0cdfdee61f535f469aefd0f4c35ba34d8d4582a Mon Sep 17 00:00:00 2001
From: Nikhil Gupta <100pranjalgupta@gmail.com>
Date: Sat, 11 Oct 2025 13:32:27 +0530
Subject: [PATCH 6/6] Update invite-collab.js
---
.../controllers/collaborator/invite-collab.js | 152 +++---------------
1 file changed, 22 insertions(+), 130 deletions(-)
diff --git a/server/src/controllers/collaborator/invite-collab.js b/server/src/controllers/collaborator/invite-collab.js
index 2f15e50d..e4fe7a7d 100644
--- a/server/src/controllers/collaborator/invite-collab.js
+++ b/server/src/controllers/collaborator/invite-collab.js
@@ -1,112 +1,10 @@
-import crypto from 'crypto';
-
-import Collaborator from '../../models/collaborator.model.js';
-import Project from '../../models/project.model.js';
-import User from '../../models/user.model.js';
-
-// import transporter from '../../config/nodemailer.js';
-import resend from '../../config/resend.js';
-
-import { sendResponse } from '../../utils/response.js';
-import { VITE_SERVER_DOMAIN } from '../../config/env.js';
-
-// const invitationToCollaborate = async(req, res) => {
-// const user_id = req.user;
-// const { project_id } = req.body;
-
-// try {
-// const user = await User.findById(user_id);
-// if (!user) {
-// return sendResponse(res, 404, 'error', 'User not found!', null);
-// }
-
-// const projectToCollaborate = await Project.findOne({
-// project_id: project_id,
-// }).populate({ path: 'author', select: 'personal_info.email' });
-
-// if (!projectToCollaborate) {
-// return sendResponse(res, 404, 'error', 'Project not found!', null);
-// }
-
-// // Ensure author is populated and has _id and personal_info
-// const author = projectToCollaborate.author;
-// if (!author || !author._id) {
-// return sendResponse(res, 404, 'error', 'Project author not found!', null);
-// }
-// if (user._id === author._id) {
-// return sendResponse(
-// res,
-// 400,
-// 'error',
-// 'You cannot invite yourself to collaborate on your own project.',
-// null
-// );
-// }
-
-// const authorEmail = author.personal_info?.email;
-
-// const token = crypto.randomBytes(16).toString('hex');
-// const acceptLink = `${VITE_SERVER_DOMAIN}/api/collaboration/accept/${token}`;
-// const rejectLink = `${VITE_SERVER_DOMAIN}/api/collaboration/reject/${token}`;
-
-// const mailOptions = {
-// from: process.env.ADMIN_EMAIL,
-// to: authorEmail,
-// subject: 'Collaboration Invitation',
-// html: `
-// Hi,
-// ${user?.personal_info?.fullname} has requested to collaborate on your project "${projectToCollaborate.title}".
-// If you’d like to join, please click below:
-//
-// Accept Invitation |
-// Reject Invitation
-//
-// Your response will help us update the project collaboration status accordingly.
-// Thanks for being part of the community,
The Code A2Z Team
-// `,
-// };
-
-// transporter.sendMail(mailOptions, async(error, info) => {
-// if (error) {
-// console.error('Error sending email:', error);
-// return sendResponse(
-// res,
-// 500,
-// 'error',
-// 'Failed to send invitation email',
-// null
-// );
-// }
-// console.log('Email sent:', info.response);
-// const collaborationData = new Collaborator({
-// user_id: user_id,
-// project_id: project_id,
-// author_id: projectToCollaborate.author,
-// status: 'pending',
-// token: token,
-// });
-
-// await collaborationData.save();
-// return sendResponse(
-// res,
-// 200,
-// 'success',
-// 'Invitation sent successfully!',
-// null
-// );
-// });
-// } catch (error) {
-// return sendResponse(
-// res,
-// 500,
-// 'error',
-// error.message || 'Internal Server Error',
-// null
-// );
-// }
-// };
-
-// export default invitationToCollaborate;
+import crypto from "crypto";
+import Collaborator from "../../models/collaborator.model.js";
+import Project from "../../models/project.model.js";
+import User from "../../models/user.model.js";
+import resend from "../../config/resend.js";
+import { sendResponse } from "../../utils/response.js";
+import { VITE_SERVER_DOMAIN } from "../../config/env.js";
const invitationToCollaborate = async (req, res) => {
const user_id = req.user;
@@ -115,35 +13,34 @@ const invitationToCollaborate = async (req, res) => {
try {
const user = await User.findById(user_id);
if (!user) {
- return sendResponse(res, 404, 'error', 'User not found!', null);
+ return sendResponse(res, 404, "error", "User not found!", null);
}
const projectToCollaborate = await Project.findOne({
project_id: project_id,
- }).populate({ path: 'author', select: 'personal_info.email' });
+ }).populate({ path: "author", select: "personal_info.email" });
if (!projectToCollaborate) {
- return sendResponse(res, 404, 'error', 'Project not found!', null);
+ return sendResponse(res, 404, "error", "Project not found!", null);
}
const author = projectToCollaborate.author;
if (!author || !author._id) {
- return sendResponse(res, 404, 'error', 'Project author not found!', null);
+ return sendResponse(res, 404, "error", "Project author not found!", null);
}
- // This logic correctly prevents self-invitation
if (String(user._id) === String(author._id)) {
return sendResponse(
res,
400,
- 'error',
- 'You cannot invite yourself to collaborate on your own project.',
+ "error",
+ "You cannot invite yourself to collaborate on your own project.",
null
);
}
const authorEmail = author.personal_info?.email;
- const token = crypto.randomBytes(16).toString('hex');
+ const token = crypto.randomBytes(16).toString("hex");
const acceptLink = `${VITE_SERVER_DOMAIN}/api/collaboration/accept/${token}`;
const rejectLink = `${VITE_SERVER_DOMAIN}/api/collaboration/reject/${token}`;
@@ -151,19 +48,16 @@ const invitationToCollaborate = async (req, res) => {
return sendResponse(
res,
400,
- 'error',
- 'Project author does not have an email address.',
+ "error",
+ "Project author does not have an email address.",
null
);
}
- // 2. CHANGED: Swapped transporter.sendMail for resend.emails.send
await resend.emails.send({
- // 3. UPDATED: Use an email from your verified domain
from: `The Code A2Z Team <${process.env.ADMIN_EMAIL}>`,
- // 4. UPDATED: 'to' must be an array
to: [authorEmail],
- subject: 'Collaboration Invitation',
+ subject: "Collaboration Invitation",
html: `
Hi,
${user?.personal_info?.fullname} has requested to collaborate on your project "${projectToCollaborate.title}".
@@ -177,23 +71,21 @@ const invitationToCollaborate = async (req, res) => {
`,
});
- // 5. CHANGED: Success logic now runs sequentially after the email is sent
- console.log('Invitation email sent to:', authorEmail);
+ console.log("Invitation email sent to:", authorEmail);
const collaborationData = new Collaborator({
user_id: user_id,
project_id: project_id,
author_id: projectToCollaborate.author,
- status: 'pending',
+ status: "pending",
token: token,
});
await collaborationData.save();
- return sendResponse(res, 200, 'success', 'Invitation sent successfully!', null);
+ return sendResponse(res, 200, "success", "Invitation sent successfully!", null);
} catch (error) {
- // This single catch block now handles errors from the database AND email sending
- console.error('Error in invitation process:', error);
- return sendResponse(res, 500, 'error', 'Failed to send invitation', null);
+ console.error("Error in invitation process:", error);
+ return sendResponse(res, 500, "error", "Failed to send invitation", null);
}
};