const crypto = require('crypto'); const { pool } = require('../db'); const HASH_ALGORITHM = 'scrypt'; const KEY_LENGTH = 64; const normalizeEmail = (email) => String(email || '').trim().toLowerCase(); const generatePassword = () => { return crypto.randomBytes(9).toString('base64url'); }; const hashPassword = (password) => { const salt = crypto.randomBytes(16).toString('hex'); const hash = crypto.scryptSync(password, salt, KEY_LENGTH).toString('hex'); return `${HASH_ALGORITHM}:${salt}:${hash}`; }; const verifyPassword = (password, passwordHash) => { const [algorithm, salt, storedHash] = String(passwordHash || '').split(':'); if (algorithm !== HASH_ALGORITHM || !salt || !storedHash) return false; const hash = crypto.scryptSync(password, salt, KEY_LENGTH); const storedBuffer = Buffer.from(storedHash, 'hex'); if (storedBuffer.length !== hash.length) return false; return crypto.timingSafeEqual(hash, storedBuffer); }; const publicUserFields = (row) => ({ id: row.id, name: row.name, email: row.email, isActive: row.is_active, createdAt: row.created_at, updatedAt: row.updated_at }); const findUserByEmail = async (email) => { const normalizedEmail = normalizeEmail(email); const result = await pool.query( `SELECT id, name, email, password_hash, is_active, created_at, updated_at FROM app_users WHERE LOWER(email) = $1 LIMIT 1`, [normalizedEmail] ); return result.rows[0] || null; }; const listUsers = async () => { const result = await pool.query( `SELECT id, name, email, is_active, created_at, updated_at FROM app_users ORDER BY created_at DESC, id DESC` ); return result.rows.map(publicUserFields); }; const createUser = async ({ name, email, password }) => { const normalizedName = String(name || '').trim(); const normalizedEmail = normalizeEmail(email); const generatedPassword = !password; const plainPassword = String(password || generatePassword()).trim(); if (!normalizedName) { const error = new Error('Name is required'); error.statusCode = 400; throw error; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) { const error = new Error('Valid email is required'); error.statusCode = 400; throw error; } if (plainPassword.length < 6) { const error = new Error('Password must have at least 6 characters'); error.statusCode = 400; throw error; } const passwordHash = hashPassword(plainPassword); try { const result = await pool.query( `INSERT INTO app_users (name, email, password_hash) VALUES ($1, $2, $3) RETURNING id, name, email, is_active, created_at, updated_at`, [normalizedName, normalizedEmail, passwordHash] ); return { user: publicUserFields(result.rows[0]), password: plainPassword, generatedPassword }; } catch (error) { if (error.code === '23505') { const duplicateError = new Error('A user with this email already exists'); duplicateError.statusCode = 409; throw duplicateError; } throw error; } }; const findUserById = async (id) => { const result = await pool.query( `SELECT id, name, email, password_hash, is_active, created_at, updated_at FROM app_users WHERE id = $1 LIMIT 1`, [id] ); return result.rows[0] || null; }; const updateUser = async (id, { name, email, password, isActive }) => { const existingUser = await findUserById(id); if (!existingUser) { const error = new Error('User not found'); error.statusCode = 404; throw error; } const normalizedName = String(name ?? existingUser.name).trim(); const normalizedEmail = normalizeEmail(email ?? existingUser.email); const normalizedIsActive = typeof isActive === 'boolean' ? isActive : existingUser.is_active; const normalizedPassword = typeof password === 'string' ? password.trim() : ''; if (!normalizedName) { const error = new Error('Name is required'); error.statusCode = 400; throw error; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) { const error = new Error('Valid email is required'); error.statusCode = 400; throw error; } if (normalizedPassword && normalizedPassword.length < 6) { const error = new Error('Password must have at least 6 characters'); error.statusCode = 400; throw error; } const passwordHash = normalizedPassword ? hashPassword(normalizedPassword) : existingUser.password_hash; try { const result = await pool.query( `UPDATE app_users SET name = $1, email = $2, password_hash = $3, is_active = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5 RETURNING id, name, email, is_active, created_at, updated_at`, [normalizedName, normalizedEmail, passwordHash, normalizedIsActive, id] ); return publicUserFields(result.rows[0]); } catch (error) { if (error.code === '23505') { const duplicateError = new Error('A user with this email already exists'); duplicateError.statusCode = 409; throw duplicateError; } throw error; } }; const deleteUser = async (id) => { const result = await pool.query( `DELETE FROM app_users WHERE id = $1 RETURNING id`, [id] ); if (!result.rowCount) { const error = new Error('User not found'); error.statusCode = 404; throw error; } }; module.exports = { createUser, deleteUser, findUserById, findUserByEmail, generatePassword, listUsers, normalizeEmail, publicUserFields, updateUser, verifyPassword };