const bcrypt = require('bcrypt'); const crypto = require('crypto'); const dbConnection = require('../database/connection'); const { dbErrorHandler } = require('../middleware/errorHandler'); class User { constructor(userData = {}) { this.id = userData.id; this.email = userData.email; this.password_hash = userData.password_hash; this.is_verified = userData.is_verified || false; this.created_at = userData.created_at; this.updated_at = userData.updated_at; this.last_login = userData.last_login; this.verification_token = userData.verification_token; this.reset_token = userData.reset_token; this.reset_expires = userData.reset_expires; } /** * Hash a password using bcrypt * @param {string} password - Plain text password * @returns {Promise} - Hashed password */ static async hashPassword(password) { const saltRounds = 12; return await bcrypt.hash(password, saltRounds); } /** * Verify a password against a hash * @param {string} password - Plain text password * @param {string} hash - Hashed password * @returns {Promise} - True if password matches */ static async verifyPassword(password, hash) { return await bcrypt.compare(password, hash); } /** * Validate email format * @param {string} email - Email address to validate * @returns {boolean} - True if email format is valid */ static validateEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Validate password strength * @param {string} password - Password to validate * @returns {object} - Validation result with isValid and errors */ static validatePassword(password) { const errors = []; if (!password || password.length < 8) { errors.push('Password must be at least 8 characters long'); } if (!/[A-Z]/.test(password)) { errors.push('Password must contain at least one uppercase letter'); } if (!/[a-z]/.test(password)) { errors.push('Password must contain at least one lowercase letter'); } if (!/\d/.test(password)) { errors.push('Password must contain at least one number'); } if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) { errors.push('Password must contain at least one special character'); } return { isValid: errors.length === 0, errors }; } /** * Generate a secure random token * @returns {string} - Random token */ static generateToken() { return crypto.randomBytes(32).toString('hex'); } /** * Create a new user * @param {object} userData - User data * @returns {Promise} - Created user instance */ static async create(userData) { const { email, password } = userData; // Validate email format if (!User.validateEmail(email)) { throw new Error('Invalid email format'); } // Validate password strength const passwordValidation = User.validatePassword(password); if (!passwordValidation.isValid) { throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`); } // Check if user already exists const existingUser = await User.findByEmail(email); if (existingUser) { throw new Error('User with this email already exists'); } // Hash password const password_hash = await User.hashPassword(password); // Generate verification token const verification_token = User.generateToken(); const query = ` INSERT INTO users (email, password_hash, verification_token) VALUES ($1, $2, $3) RETURNING * `; return await dbErrorHandler(async () => { const result = await dbConnection.query(query, [email, password_hash, verification_token]); return new User(result.rows[0]); }); } /** * Find user by email * @param {string} email - Email address * @returns {Promise} - User instance or null */ static async findByEmail(email) { return await dbErrorHandler(async () => { const query = 'SELECT * FROM users WHERE email = $1'; const result = await dbConnection.query(query, [email]); if (result.rows.length === 0) { return null; } return new User(result.rows[0]); }); } /** * Find user by ID * @param {string} id - User ID * @returns {Promise} - User instance or null */ static async findById(id) { return await dbErrorHandler(async () => { const query = 'SELECT * FROM users WHERE id = $1'; const result = await dbConnection.query(query, [id]); if (result.rows.length === 0) { return null; } return new User(result.rows[0]); }); } /** * Find user by verification token * @param {string} token - Verification token * @returns {Promise} - User instance or null */ static async findByVerificationToken(token) { return await dbErrorHandler(async () => { const query = 'SELECT * FROM users WHERE verification_token = $1'; const result = await dbConnection.query(query, [token]); if (result.rows.length === 0) { return null; } return new User(result.rows[0]); }); } /** * Find user by reset token * @param {string} token - Reset token * @returns {Promise} - User instance or null */ static async findByResetToken(token) { return await dbErrorHandler(async () => { const query = ` SELECT * FROM users WHERE reset_token = $1 AND reset_expires > NOW() `; const result = await dbConnection.query(query, [token]); if (result.rows.length === 0) { return null; } return new User(result.rows[0]); }); } /** * Verify user email * @returns {Promise} - True if verification successful */ async verifyEmail() { return await dbErrorHandler(async () => { const query = ` UPDATE users SET is_verified = true, verification_token = NULL, updated_at = NOW() WHERE id = $1 `; const result = await dbConnection.query(query, [this.id]); if (result.rowCount > 0) { this.is_verified = true; this.verification_token = null; return true; } return false; }); } /** * Update last login timestamp * @returns {Promise} - True if update successful */ async updateLastLogin() { return await dbErrorHandler(async () => { const query = ` UPDATE users SET last_login = NOW(), updated_at = NOW() WHERE id = $1 `; const result = await dbConnection.query(query, [this.id]); if (result.rowCount > 0) { this.last_login = new Date(); return true; } return false; }); } /** * Set password reset token * @returns {Promise} - Reset token */ async setResetToken() { return await dbErrorHandler(async () => { const reset_token = User.generateToken(); const reset_expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now const query = ` UPDATE users SET reset_token = $1, reset_expires = $2, updated_at = NOW() WHERE id = $3 `; await dbConnection.query(query, [reset_token, reset_expires, this.id]); this.reset_token = reset_token; this.reset_expires = reset_expires; return reset_token; }); } /** * Update password * @param {string} newPassword - New password * @returns {Promise} - True if update successful */ async updatePassword(newPassword) { // Validate new password const passwordValidation = User.validatePassword(newPassword); if (!passwordValidation.isValid) { throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`); } return await dbErrorHandler(async () => { const password_hash = await User.hashPassword(newPassword); const query = ` UPDATE users SET password_hash = $1, reset_token = NULL, reset_expires = NULL, updated_at = NOW() WHERE id = $2 `; const result = await dbConnection.query(query, [password_hash, this.id]); if (result.rowCount > 0) { this.password_hash = password_hash; this.reset_token = null; this.reset_expires = null; return true; } return false; }); } /** * Update user profile * @param {object} updates - Fields to update * @returns {Promise} - True if update successful */ async update(updates) { const allowedFields = ['email']; const updateFields = []; const values = []; let paramIndex = 1; for (const [field, value] of Object.entries(updates)) { if (allowedFields.includes(field)) { if (field === 'email' && !User.validateEmail(value)) { throw new Error('Invalid email format'); } updateFields.push(`${field} = $${paramIndex}`); values.push(value); paramIndex++; } } if (updateFields.length === 0) { return false; } updateFields.push(`updated_at = NOW()`); values.push(this.id); const query = ` UPDATE users SET ${updateFields.join(', ')} WHERE id = $${paramIndex} `; return await dbErrorHandler(async () => { const result = await dbConnection.query(query, values); if (result.rowCount > 0) { // Update instance properties for (const [field, value] of Object.entries(updates)) { if (allowedFields.includes(field)) { this[field] = value; } } return true; } return false; }); } /** * Delete user account * @returns {Promise} - True if deletion successful */ async delete() { return await dbErrorHandler(async () => { const query = 'DELETE FROM users WHERE id = $1'; const result = await dbConnection.query(query, [this.id]); return result.rowCount > 0; }); } /** * Get user data without sensitive information * @returns {object} - Safe user data */ toSafeObject() { return { id: this.id, email: this.email, is_verified: this.is_verified, created_at: this.created_at, updated_at: this.updated_at, last_login: this.last_login }; } /** * Authenticate user with email and password * @param {string} email - Email address * @param {string} password - Password * @returns {Promise} - User instance if authentication successful */ static async authenticate(email, password) { const user = await User.findByEmail(email); if (!user) { return null; } const isValidPassword = await User.verifyPassword(password, user.password_hash); if (!isValidPassword) { return null; } // Update last login await user.updateLastLogin(); return user; } } module.exports = User;