Skip to content
Merged
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
73 changes: 41 additions & 32 deletions server/package-lock.json

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

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
18 changes: 10 additions & 8 deletions server/src/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,26 @@ 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 / 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";
15 changes: 0 additions & 15 deletions server/src/config/nodemailer.js

This file was deleted.

10 changes: 10 additions & 0 deletions server/src/config/resend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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.");
}

const resend = new Resend(RESEND_API_KEY);

export default resend;
102 changes: 44 additions & 58 deletions server/src/controllers/collaborator/invite-collab.js
Original file line number Diff line number Diff line change
@@ -1,12 +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 { sendResponse } from '../../utils/response.js';
import { VITE_SERVER_DOMAIN } from '../../config/env.js';
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;
Expand All @@ -15,42 +13,51 @@ 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);
}

// 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);
return sendResponse(res, 404, "error", "Project author not found!", null);
}
if (user._id === author._id) {

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

const mailOptions = {
from: process.env.ADMIN_EMAIL,
to: authorEmail,
subject: 'Collaboration Invitation',
if (!authorEmail) {
return sendResponse(
res,
400,
"error",
"Project author does not have an email address.",
null
);
}

await resend.emails.send({
from: `The Code A2Z Team <${process.env.ADMIN_EMAIL}>`,
to: [authorEmail],
subject: "Collaboration Invitation",
html: `
<p>Hi,</p>
<p><strong>${user?.personal_info?.fullname}</strong> has requested to collaborate on your project "${projectToCollaborate.title}".</p>
Expand All @@ -62,44 +69,23 @@ const invitationToCollaborate = async (req, res) => {
<p>Your response will help us update the project collaboration status accordingly.</p>
<p>Thanks for being part of the community,<br/>The Code A2Z Team</p>
`,
};
});

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
);
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) {
return sendResponse(
res,
500,
'error',
error.message || 'Internal Server Error',
null
);
console.error("Error in invitation process:", error);
return sendResponse(res, 500, "error", "Failed to send invitation", null);
}
};

Expand Down
Loading