/**
* @fileoverview MongoDB database operations and user authentication
* @description This module handles all database operations including user management,
* post management, and email verification using external APIs.
* @author Your Name
* @version 1.0.0
* @requires mongodb
* @requires mongoose
* @requires bcryptjs
* @requires jsonwebtoken
* @requires axios
*/
const { MongoClient, ServerApiVersion } = require('mongodb');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const axios = require('axios');
// Load environment variables
require('dotenv').config();
/**
* JWT secret key from environment variables
* @type {string}
*/
const JWT_SECRET = process.env.JWT_SECRET;
/**
* JWT token expiration time from environment variables
* @type {string}
*/
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN;
/**
* MongoDB connection URI from environment variables
* @type {string}
*/
const uri = process.env.MONGODB_URI;
/**
* Mailboxlayer API key for email verification
* @type {string}
*/
const MAILBOXLAYER_API_KEY = process.env.MAILBOXLAYER_API_KEY;
/**
* Verifies if an email address is valid and exists using Mailboxlayer API
* @async
* @function verifyEmail
* @param {string} email - The email address to verify
* @returns {Promise<Object>} Object containing validation status and reason
* @returns {boolean} returns.valid - Whether the email is valid
* @returns {string} returns.reason - Explanation of the validation result
* @example
* const result = await verifyEmail('user@example.com');
* if (result.valid) {
* console.log('Email is valid:', result.reason);
* } else {
* console.log('Email is invalid:', result.reason);
* }
* @throws {Error} If API request fails, falls back to allowing the email
*/
async function verifyEmail(email) {
try {
console.log(`Verifying email: ${email}`);
const response = await axios.get('http://apilayer.net/api/check', {
params: {
access_key: MAILBOXLAYER_API_KEY,
email: email,
smtp: 1, // Enables SMTP check (only works with paid plans)
format: 1 // Returns formatted JSON
},
timeout: 10000 // 10 second timeout
});
const data = response.data;
console.log('Mailboxlayer response:', data);
// Check if API call was successful
if (data.error) {
console.error('Mailboxlayer API error:', data.error);
return { valid: false, reason: `API error: ${data.error.info}` };
}
if (!data.format_valid) {
return { valid: false, reason: 'Invalid email format' };
}
if (!data.mx_found) {
return { valid: false, reason: 'Domain has no MX records' };
}
// Note: SMTP check might not work with free plan
if (data.smtp_check === false) {
return { valid: false, reason: 'SMTP check failed — mailbox may not exist' };
}
return { valid: true, reason: 'Email is valid and exists' };
} catch (error) {
console.error('Email verification error:', error.message);
// If API is down or network issue, allow registration but log warning
console.warn('Email verification service unavailable, allowing registration');
return { valid: true, reason: 'Email verification service unavailable, but proceeding' };
}
}
/**
* @typedef {Object} User
* @property {string} username - User's unique username (min 3 characters)
* @property {string} email - User's email address (must be valid format)
* @property {string} password - User's hashed password (min 6 characters)
* @property {Date} createdAt - Account creation timestamp
*/
/**
* Mongoose schema for User model
* @type {mongoose.Schema}
* @description Defines the structure for user documents in MongoDB
*/
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: [true, 'Please add a username'],
unique: true,
trim: true,
minlength: [3, 'Username must be at least 3 characters long']
},
email: {
type: String,
required: [true, 'Please add an email'],
unique: true,
match: [
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
'Please add a valid email'
]
},
password: {
type: String,
required: [true, 'Please add a password'],
minlength: [6, 'Password must be at least 6 characters long'],
select: false
},
createdAt: {
type: Date,
default: Date.now
}
});
UserSchema.pre('save', async function (next) {
if (!this.isModified('password')) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
UserSchema.methods.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
const User = mongoose.model('User', UserSchema);
// Generate JWT Token
const generateToken = (userId) => {
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
};
// Verify JWT Token
const verifyToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
throw new Error('Invalid or expired token');
}
};
// Get user by ID (for protected routes)
const getUserById = async (userId) => {
try {
const user = await User.findById(userId).select('-password');
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
throw new Error('Error fetching user');
}
};
// MongoDB connection functions
const connectToMongoDB = async () => {
try {
const options = {
serverSelectionTimeoutMS: parseInt(process.env.ServerSelectTimer) || 30000,
socketTimeoutMS: parseInt(process.env.SocketTimeout) || 45000,
};
await mongoose.connect(uri, options);
console.log('Connected to MongoDB Atlas successfully');
// Connection event listeners
mongoose.connection.on('error', (err) => {
console.error('MongoDB connection error:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB disconnected');
});
return true;
} catch (error) {
console.error('MongoDB connection error:', error);
throw error;
}
};
const closeConnection = async () => {
try {
await mongoose.connection.close();
console.log('MongoDB connection closed');
} catch (error) {
console.error('Error closing MongoDB connection:', error);
}
};
// Delete a user based on email and password verification
const deleteaUser = async (email, password) => {
try {
// Find the user with password included
const user = await User.findOne({ email }).select('+password');
if (!user) {
throw new Error('User not found with this email');
}
// Verify the password
const isPasswordMatch = await user.matchPassword(password);
if (!isPasswordMatch) {
throw new Error('Incorrect password');
}
// If password verification passes, delete the user
await User.findOneAndDelete({ email });
console.log('User deleted successfully:', email);
return {
success: true,
message: 'User account deleted successfully'
};
} catch (error) {
console.error('Error deleting user:', error.message);
throw error;
}
}
// Check if the user exists in the database and if it doesn't will create new user
const registerUser = async ({ name, email, password }) => {
try {
// Skip email verification in test environment
if (process.env.NODE_ENV !== 'test' && !process.env.SKIP_EMAIL_VERIFICATION) {
console.log('Verifying email with mailboxlayer...');
const emailVerification = await verifyEmail(email);
if (!emailVerification.valid) {
throw new Error(`Email verification failed: ${emailVerification.reason}`);
}
console.log('Email verification passed:', emailVerification.reason);
} else {
console.log('Skipping email verification for test environment');
}
const userExists = await User.findOne({ email });
if (userExists) {
return {
success: false,
message: 'User already exists'
};
}
const newUser = await User.create({
username: name,
email,
password
});
// Generate token
const token = generateToken(newUser._id);
console.log('User registered successfully:', newUser.username);
return {
success: true,
message: 'User registered successfully',
token: token,
user: {
_id: newUser._id,
name: newUser.username,
email: newUser.email
}
};
} catch (error) {
console.error('Error registering user:', error.message);
return {
success: false,
message: error.message
};
}
};
// Check if the user exists if it does do login
const LoginUser = async ({ email, password }) => {
try {
const FindUser = await User.findOne({ email: email }).select('+password');
if (!FindUser) {
return {
success: false,
message: 'Invalid credentials'
};
}
const isPasswordMatch = await FindUser.matchPassword(password);
if (!isPasswordMatch) {
return {
success: false,
message: 'Invalid credentials'
};
}
// Generate token
const token = generateToken(FindUser._id);
console.log('User found with credentials correctly');
return {
success: true,
message: 'Login successful',
token: token,
user: {
_id: FindUser._id,
name: FindUser.username,
email: FindUser.email
}
};
} catch (error) {
console.error('Error with the Login:', error.message);
return {
success: false,
message: 'Login error'
};
}
}
module.exports = {
User,
connectToMongoDB,
closeConnection,
registerUser,
LoginUser,
generateToken,
verifyToken,
getUserById,
verifyEmail,
deleteaUser
};