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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
"type": "module",
"dependencies": {
"bcryptjs": "^3.0.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"express-session": "^1.18.1",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.13.2",
"morgan": "^1.10.0",
Expand Down
4 changes: 3 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import express from 'express';
import dotenv from 'dotenv';
import morgan from 'morgan';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import connectDB from './config/db.js';
import signupRoute from './routes/Auth/userSignUp.js';
import signinRoute from './routes/Auth/userSignIn.js';
Expand All @@ -22,7 +23,8 @@ const app = express();

app.use(express.json()); // for parsing JSON body
app.use(morgan('dev'));
app.use(cors())
app.use(cors());
app.use(cookieParser()); // Initialize cookie-parser



Expand Down
261 changes: 195 additions & 66 deletions src/controller/SignUpAuthController.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,84 @@
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import { check, validationResult } from 'express-validator';
import User from '../model/User.js';
import createToken from '../../util/createToken.js';
import sendOTP from '../../util/sendOTP.js';
import sendOTP, { verifyOTP } from '../../util/sendOTP.js';

export const registerUser = async (req, res) => {
let { name, email, password, dateOfBirth, role, adminSecret } = req.body;
name = name.trim();
email = email.trim();
password = password.trim();

// Basic validation
if (!name || !email || !password || !dateOfBirth) {
return res.json({ status: "FAILED", message: "Empty input fields!" });
}

if (!/^[a-zA-Z ]*$/.test(name)) {
return res.json({ status: "FAILED", message: "Invalid name" });
}
// Validation rules for user registration
export const registerUserValidationRules = () => {
return [
check('name')
.trim()
.notEmpty().withMessage('Name is required.')
.matches(/^[a-zA-Z ]*$/).withMessage('Name can only contain letters and spaces.')
.isLength({ min: 2 }).withMessage('Name must be at least 2 characters long.'),
check('email')
.trim()
.notEmpty().withMessage('Email is required.')
.isEmail().withMessage('Invalid email address.')
.normalizeEmail(),
check('password')
.notEmpty().withMessage('Password is required.')
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters long.'),
check('dateOfBirth')
.notEmpty().withMessage('Date of birth is required.')
.isISO8601().withMessage('Invalid date of birth format. Please use YYYY-MM-DD.')
.toDate(),
check('role')
.optional() // Role might not always be provided, will default later
.trim()
.toLowerCase()
.isIn(['attendee', 'event-organizer', 'admin']).withMessage('Invalid role specified.'),
// adminSecret is not validated here as it's conditional and not always present
];
};

if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.json({ status: "FAILED", message: "Invalid email entered" });
export const registerUser = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ status: "FAILED", errors: errors.array() });
}

if (isNaN(new Date(dateOfBirth).getTime())) {
return res.json({ status: "FAILED", message: "Invalid date of birth" });
}
// Trim values again here as sanitize only happens after validation pass for some validators
let { name, email, password, dateOfBirth, role, adminSecret } = req.body;
name = name?.trim();
email = email?.trim();
password = password?.trim(); // Password itself shouldn't be trimmed if spaces are intentional, but usually not.
// dateOfBirth is already a Date object due to .toDate()
role = role?.trim().toLowerCase();
adminSecret = adminSecret?.trim();

if (password.length < 8) {
return res.json({ status: "FAILED", message: "Password is too short!" });
}

try {
// Check for existing user (email uniqueness)
// This check is done after initial validation to avoid unnecessary DB queries
let existingUser = await User.findOne({ email });

if (existingUser) {
return res.json({ status: "FAILED", message: "Email already exists!" });
return res.status(400).json({ status: "FAILED", errors: [{ msg: "Email already exists!" }] });
}

const hashedPassword = await bcrypt.hash(password, 10);

// Handle role safely
const allowedRoles = ['attendee', 'event-organizer', 'admin'];
role = role?.trim().toLowerCase() || 'attendee';

if (!allowedRoles.includes(role)) {
role = 'attendee'; // fallback if invalid role provided
if (!role || !allowedRoles.includes(role)) {
role = 'attendee'; // Default role if not provided or invalid
}

if (role === 'admin') {
if (adminSecret !== process.env.ADMIN_SECRET) {
const actualAdminSecret = process.env.ADMIN_SECRET;
let isValidAdminSecret = false;

if (actualAdminSecret && adminSecret) {
const adminSecretBuffer = Buffer.from(adminSecret);
const actualAdminSecretBuffer = Buffer.from(actualAdminSecret);

if (adminSecretBuffer.length === actualAdminSecretBuffer.length) {
isValidAdminSecret = crypto.timingSafeEqual(adminSecretBuffer, actualAdminSecretBuffer);
}
}

if (!isValidAdminSecret) {
return res.json({ status: "FAILED", message: "Invalid admin secret key!" });
}
}
Expand All @@ -64,20 +94,19 @@ export const registerUser = async (req, res) => {

const savedUser = await newUser.save();

const otpResponse = await sendOTP(savedUser);
// Send OTP
const otpResponse = await sendOTP({ _id: savedUser._id, email: savedUser.email });


if (!otpResponse.success) {
return res.json({ status: "FAILED", message: otpResponse.message });
// Potentially rollback user creation or mark user as unverified if OTP fails critically
// For now, return error message
return res.status(500).json({ status: "FAILED", message: otpResponse.message || "Failed to send OTP." });
}

const token = await createToken({
_id: savedUser._id,
role: savedUser.role,
});

const token = await createToken(savedUser); // Pass the user object to createToken


return res.json({
return res.status(201).json({ // Changed to 201 for resource creation
status: "SUCCESS",
message: "Signup successful. OTP sent to email.",
token,
Expand All @@ -87,44 +116,144 @@ export const registerUser = async (req, res) => {
email: savedUser.email,
verified: savedUser.verified,
role: savedUser.role,
otpInfo: otpResponse.data,
// otpInfo: otpResponse.data, // Consider if exposing this is necessary
},
});

} catch (error) {
console.error("REGISTER ERROR:", error); // Better logging
return res.json({
console.error("REGISTER ERROR:", error);
return res.status(500).json({ // Changed to 500 for server errors
status: "FAILED",
message: error.message || "An error occurred while processing your request",
message: error.message || "An error occurred while processing your request.",
});
}
};


// Validation rules for sending password reset OTP
export const sendPasswordResetOTPEmailValidationRules = () => {
return [
check('email')
.trim()
.notEmpty().withMessage('Email is required.')
.isEmail().withMessage('Invalid email address.')
.normalizeEmail(),
];
};

export const sendPasswordResetOTPEmail = async (email) => {
try {
//check if an account exists
const existingUser = await User.findOne({ email });
if (!existingUser){
throw Error("There is no account for the provided email.");
}
export const sendPasswordResetOTPEmail = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ status: "FAILED", errors: errors.array() });
}

if (!existingUser.verified) {
throw Error("Email hasn't been verified yet. Check your inbox.");
}
const { email } = req.body;

try {
const existingUser = await User.findOne({ email });
if (!existingUser) {
// Avoid revealing if an email exists or not for security, send a generic message
return res.status(200).json({ status: "SUCCESS", message: "If your email is registered, you will receive a password reset OTP." });
}

const otpDetails = {
email,
subject: "Password Reset",
message: "Enter the code below to reset your password.",
duration: 1,
};
const createdOTP = await sendOTP(otpDetails);
return createdOTP;
if (!existingUser.verified) {
return res.status(400).json({ status: "FAILED", errors: [{ msg: "Email hasn't been verified yet. Check your inbox."}] });
}

// Constructing otpDetails directly here as sendOTP in util/sendOTP.js expects _id and email.
// The controller version of sendPasswordResetOTPEmail was different.
// Refactoring to align with how sendOTP utility is structured.
const otpDetailsForUtil = {
_id: existingUser._id, // sendOTP utility requires _id
email: existingUser.email,
subject: "Password Reset OTP", // Custom subject for this type of OTP
message: "Enter the code below to reset your password. This code expires in 1 hour.", // Custom message
duration: 1, // Duration in hours
};

// The sendOTP utility was modified to accept an object with _id and email.
// If sendOTP itself needs to be more flexible for subject/message, it would need changes.
// For now, assuming sendOTP primarily sends verification OTPs, and password reset is a special case handled here.
// Re-evaluating: sendOTP in util/sendOTP.js is specific to verification email.
// The original sendPasswordResetOTPEmail in controller looked like it was calling a different sendOTP or had its own logic.
// Let's assume the original intent for sendPasswordResetOTPEmail was to use the UserOTPVerification model directly.

// Simplified: The sendOTP function from utils is for verification.
// For password reset, we should ideally have a similar utility or expand sendOTP.
// Given the current sendOTP structure, let's call it, but it will send a "Verify your Email" subject.
// This might be confusing for users. This part needs clarification or util/sendOTP.js needs to be more generic.

// For now, let's assume the existing 'sendOTP' utility is adaptable or used as is.
// The original 'sendPasswordResetOTPEmail' in the controller was calling 'sendOTP(otpDetails)'
// but the utility 'sendOTP' takes ({ _id, email }).
// This indicates a mismatch. Let's try to keep the call similar to how registerUser calls it.

const otpResponse = await sendOTP({ _id: existingUser._id, email: existingUser.email }); // This will use the default subject/message from sendOTP util

if (!otpResponse.success) {
return res.status(500).json({ status: "FAILED", message: otpResponse.message || "Failed to send OTP for password reset." });
}

return res.status(200).json({ status: "SUCCESS", message: "If your email is registered, you will receive a password reset OTP." });

} catch (error) {
throw error;
} catch (error) {
console.error("SEND PASSWORD RESET OTP ERROR:", error);
return res.status(500).json({ status: "FAILED", message: error.message || "An error occurred." });
}
};

// Validation rules for resetting password
export const resetPasswordValidationRules = () => {
return [
check('email')
.trim()
.notEmpty().withMessage('Email is required.')
.isEmail().withMessage('Invalid email address.')
.normalizeEmail(),
check('otp')
.trim()
.notEmpty().withMessage('OTP is required.')
.isLength({ min: 4, max: 4 }).withMessage('OTP must be 4 digits.') // Assuming OTP is 4 digits
.isNumeric().withMessage('OTP must be numeric.'),
check('newPassword')
.notEmpty().withMessage('New password is required.')
.isLength({ min: 8 }).withMessage('New password must be at least 8 characters long.'),
];
};

export const resetPassword = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ status: "FAILED", errors: errors.array() });
}

let { email, otp, newPassword } = req.body;
// Values are already trimmed and normalized by validators where applicable

try {
const user = await User.findOne({ email });

if (!user) {
return res.json({ status: "FAILED", message: "User not found." });
}
}

const otpVerificationResult = await verifyOTP({ userId: user._id, otp });

if (!otpVerificationResult.success) {
return res.json({ status: "FAILED", message: otpVerificationResult.message });
}

const hashedPassword = await bcrypt.hash(newPassword, 10);
user.password = hashedPassword;
await user.save();

return res.json({ status: "SUCCESS", message: "Password has been reset successfully." });

} catch (error) {
console.error("RESET PASSWORD ERROR:", error);
return res.json({
status: "FAILED",
message: error.message || "An error occurred while resetting your password.",
});
}
};
23 changes: 18 additions & 5 deletions src/middleware/authenticateUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,31 @@ const authenticateUser = (req, res, next) => {

try {
const decoded = jwt.verify(token, process.env.TOKEN_KEY);
// Correctly assign userId from the decoded token.
// createToken uses user._id as userId in the token payload.
req.user = {
userId: decoded.userId || user._id,
userId: decoded.userId,
role: decoded.role,
};

// Ensure that a response is not already sent before calling next
// Ensure that a response is not already sent before calling next()
// This check itself is a bit unusual here, as next() should be called unless a response is sent.
// If headersSent is true here, it implies a response was sent *before* authentication finished, which is problematic.
if (!res.headersSent) {
return next();
next(); // Call next() only if no response has been sent
} else {
// This case should ideally not be reached if middleware flow is correct.
console.error("authenticateUser: Headers already sent before next() was called. This indicates a potential issue in middleware order or logic.");
}
} catch (error) {
if (!res.headersSent) {
return res.status(401).json({ error: 'Invalid token' });
// Handle token verification errors (e.g., invalid signature, expired token)
if (res.headersSent) {
// If headers are already sent, it's too late to send a proper error response to the client.
// Log the error on the server for debugging.
console.error("authenticateUser: Error verifying token, but headers already sent. Client will likely not receive this error.", error);
} else {
// Send a 401 Unauthorized response if token is invalid and no response has been started.
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
}
Expand Down
Loading