const bcrypt = require('bcryptjs'); const crypto = require('crypto'); const AdminUserRepository = require('../repositories/AdminUserRepository'); const DEFAULT_SALT_ROUNDS = parseInt(process.env.ADMIN_PASSWORD_SALT_ROUNDS || '12', 10); class AdminAuthService { async needsInitialSetup() { const count = await AdminUserRepository.countActiveAdmins(); return count === 0; } async createInitialAdmin({ username, password }) { const trimmedUsername = (username || '').trim().toLowerCase(); if (!trimmedUsername) { throw new Error('USERNAME_REQUIRED'); } if (!password || password.length < 10) { throw new Error('PASSWORD_TOO_WEAK'); } const needsSetup = await this.needsInitialSetup(); if (!needsSetup) { throw new Error('SETUP_ALREADY_COMPLETED'); } const passwordHash = await this.hashPassword(password); const id = await AdminUserRepository.createAdminUser({ username: trimmedUsername, passwordHash, role: 'admin', requiresPasswordChange: false }); return { id, username: trimmedUsername, role: 'admin' }; } async createAdminUser({ username, password, role = 'admin', requiresPasswordChange = false }) { const trimmedUsername = (username || '').trim().toLowerCase(); if (!trimmedUsername) { throw new Error('USERNAME_REQUIRED'); } if (!password || password.length < 10) { throw new Error('PASSWORD_TOO_WEAK'); } const normalizedRole = (role || 'admin').trim().toLowerCase(); const targetRole = normalizedRole || 'admin'; const existing = await AdminUserRepository.getByUsername(trimmedUsername); if (existing) { throw new Error('USERNAME_IN_USE'); } const passwordHash = await this.hashPassword(password); const id = await AdminUserRepository.createAdminUser({ username: trimmedUsername, passwordHash, role: targetRole, requiresPasswordChange }); return { id, username: trimmedUsername, role: targetRole, requiresPasswordChange: Boolean(requiresPasswordChange) }; } async changePassword({ userId, currentPassword, newPassword }) { if (!userId) { throw new Error('USER_NOT_FOUND'); } if (!currentPassword) { throw new Error('CURRENT_PASSWORD_REQUIRED'); } if (!newPassword || newPassword.length < 10) { throw new Error('PASSWORD_TOO_WEAK'); } const userRecord = await AdminUserRepository.getById(userId); if (!userRecord || !userRecord.is_active) { throw new Error('USER_NOT_FOUND'); } const matches = await bcrypt.compare(currentPassword || '', userRecord.password_hash); if (!matches) { throw new Error('INVALID_CURRENT_PASSWORD'); } const passwordHash = await this.hashPassword(newPassword); await AdminUserRepository.updatePassword(userRecord.id, passwordHash, false); return { id: userRecord.id, username: userRecord.username, role: userRecord.role, requiresPasswordChange: false }; } async hashPassword(password) { return bcrypt.hash(password, DEFAULT_SALT_ROUNDS); } async verifyCredentials(username, password) { const normalizedUsername = (username || '').trim().toLowerCase(); const user = await AdminUserRepository.getByUsername(normalizedUsername); if (!user || !user.is_active) { return null; } const matches = await bcrypt.compare(password || '', user.password_hash); if (!matches) { return null; } await AdminUserRepository.recordSuccessfulLogin(user.id); return { id: user.id, username: user.username, role: user.role, requiresPasswordChange: Boolean(user.requires_password_change) }; } generateCsrfToken() { return crypto.randomBytes(32).toString('hex'); } startSession(req, user) { const csrfToken = this.generateCsrfToken(); req.session.user = { id: user.id, username: user.username, role: user.role, requiresPasswordChange: user.requiresPasswordChange || false }; req.session.csrfToken = csrfToken; return csrfToken; } async destroySession(req) { return new Promise((resolve, reject) => { if (!req.session) { return resolve(); } req.session.destroy((err) => { if (err) { reject(err); } else { resolve(); } }); }); } } module.exports = new AdminAuthService();