420 lines
13 KiB
JavaScript
420 lines
13 KiB
JavaScript
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<string>} - 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<boolean>} - 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<User>} - 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|null>} - 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|null>} - 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|null>} - 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|null>} - 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<boolean>} - 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<boolean>} - 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<string>} - 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<boolean>} - 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<boolean>} - 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<boolean>} - 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|null>} - 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; |