const bcrypt = require('bcrypt'); const fs = require('fs').promises; const path = require('path'); const sqlite3 = require('sqlite3'); const { promisify } = require('util'); class AuthManager { constructor() { this.dbPath = path.join(process.cwd(), 'users.db'); this.db = null; this.saltRounds = 12; } async initialize() { return new Promise((resolve, reject) => { this.db = new sqlite3.Database(this.dbPath, (err) => { if (err) { reject(err); return; } // Create users table if it doesn't exist this.db.run(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_by INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME, is_active BOOLEAN DEFAULT 1, FOREIGN KEY (created_by) REFERENCES users (id) ) `, (err) => { if (err) { reject(err); return; } resolve(); }); }); }); } async createUser(username, password, createdByUserId = null) { if (!username || !password) { throw new Error('Username and password are required'); } // Check if this is not the first user and no creator is specified const isFirstUser = await this.isFirstUser(); if (!isFirstUser && !createdByUserId) { throw new Error('Only existing administrators can create new accounts'); } // Validate username format if (!/^[a-zA-Z0-9_-]{3,20}$/.test(username)) { throw new Error('Username must be 3-20 characters, letters, numbers, underscore, or hyphen only'); } // Validate password strength if (password.length < 8) { throw new Error('Password must be at least 8 characters long'); } try { const passwordHash = await bcrypt.hash(password, this.saltRounds); return new Promise((resolve, reject) => { const stmt = this.db.prepare(` INSERT INTO users (username, password_hash, created_by) VALUES (?, ?, ?) `); stmt.run([username, passwordHash, createdByUserId], function(err) { if (err) { if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') { reject(new Error('Username already exists')); } else { reject(err); } return; } resolve({ id: this.lastID, username: username, created_at: new Date().toISOString() }); }); stmt.finalize(); }); } catch (error) { throw new Error('Failed to create user: ' + error.message); } } async authenticateUser(username, password) { if (!username || !password) { throw new Error('Username and password are required'); } return new Promise((resolve, reject) => { this.db.get( 'SELECT * FROM users WHERE username = ? AND is_active = 1', [username], async (err, user) => { if (err) { reject(err); return; } if (!user) { reject(new Error('Invalid username or password')); return; } try { const passwordMatch = await bcrypt.compare(password, user.password_hash); if (!passwordMatch) { reject(new Error('Invalid username or password')); return; } // Update last login this.db.run( 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', [user.id] ); // Return user info (without password hash) resolve({ id: user.id, username: user.username, created_at: user.created_at, last_login: user.last_login }); } catch (bcryptError) { reject(bcryptError); } } ); }); } async getUserById(id) { return new Promise((resolve, reject) => { this.db.get( 'SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1', [id], (err, user) => { if (err) { reject(err); return; } resolve(user || null); } ); }); } async getUserByUsername(username) { return new Promise((resolve, reject) => { this.db.get( 'SELECT id, username, created_at, last_login FROM users WHERE username = ? AND is_active = 1', [username], (err, user) => { if (err) { reject(err); return; } resolve(user || null); } ); }); } async getAllUsers() { return new Promise((resolve, reject) => { this.db.all( 'SELECT id, username, created_at, last_login FROM users WHERE is_active = 1 ORDER BY created_at DESC', [], (err, users) => { if (err) { reject(err); return; } resolve(users || []); } ); }); } async deleteUser(id) { return new Promise((resolve, reject) => { this.db.run( 'UPDATE users SET is_active = 0 WHERE id = ?', [id], function(err) { if (err) { reject(err); return; } resolve(this.changes > 0); } ); }); } async changePassword(id, currentPassword, newPassword) { if (!currentPassword || !newPassword) { throw new Error('Current password and new password are required'); } if (newPassword.length < 8) { throw new Error('New password must be at least 8 characters long'); } return new Promise((resolve, reject) => { this.db.get( 'SELECT password_hash FROM users WHERE id = ? AND is_active = 1', [id], async (err, user) => { if (err) { reject(err); return; } if (!user) { reject(new Error('User not found')); return; } try { const passwordMatch = await bcrypt.compare(currentPassword, user.password_hash); if (!passwordMatch) { reject(new Error('Current password is incorrect')); return; } const newPasswordHash = await bcrypt.hash(newPassword, this.saltRounds); this.db.run( 'UPDATE users SET password_hash = ? WHERE id = ?', [newPasswordHash, id], function(err) { if (err) { reject(err); return; } resolve(this.changes > 0); } ); } catch (bcryptError) { reject(bcryptError); } } ); }); } async isFirstUser() { return new Promise((resolve, reject) => { this.db.get( 'SELECT COUNT(*) as count FROM users WHERE is_active = 1', [], (err, result) => { if (err) { reject(err); return; } resolve(result.count === 0); } ); }); } close() { if (this.db) { this.db.close(); } } } module.exports = AuthManager;