using AuthorBuddy.Web.Data; using LiteDB; namespace AuthorBuddy.Web.Services; public class AuthService : IAuthService { private readonly ILiteDatabase _db; private readonly ISettingsService _settingsService; private readonly ILocalizationService _loc; private UserEntity? _currentUser; private string? _pending2FASecret; private bool _requires2FA; public AuthService(ILiteDatabase db, ISettingsService settingsService, ILocalizationService loc) { _db = db; _settingsService = settingsService; _loc = loc; } public bool IsLoggedIn => _currentUser != null; public bool Is2FARequired => _requires2FA; public UserEntity? CurrentUser => _currentUser; public async Task LoginAsync(string username, string password) { var col = _db.GetCollection("users"); var user = col.FindOne(u => u.Name == username); if (user == null || string.IsNullOrEmpty(user.PasswordHash)) return false; if (!user.IsActive) return false; if (!TotpHelper.VerifyPassword(password, user.PasswordHash)) return false; if (user.TwoFactorEnabled) { _pending2FASecret = user.TwoFactorSecret; _requires2FA = true; return true; } await SetAuthenticatedUser(user); return true; } public async Task LogoutAsync() { _currentUser = null; _requires2FA = false; _pending2FASecret = null; await Task.CompletedTask; } public async Task AdminBypassLoginAsync(int userId) { var col = _db.GetCollection("users"); var user = col.FindById(userId); if (user != null) await SetAuthenticatedUser(user); } public async Task RegisterAsync(string username, string password) { var col = _db.GetCollection("users"); var existing = col.FindOne(u => u.Name == username); if (existing != null) return false; var hasUsers = col.FindAll().Any(); var user = new UserEntity { Name = username, PasswordHash = TotpHelper.HashPassword(password), TwoFactorEnabled = false, TwoFactorSecret = string.Empty, Roles = hasUsers ? new List { UserRole.Author } : new List { UserRole.Administrator } }; col.Insert(user); await SetAuthenticatedUser(user); return true; } public Task GetCurrentUserAsync() { return Task.FromResult(_currentUser); } public async Task Verify2FAAsync(string code) { if (string.IsNullOrEmpty(_pending2FASecret)) return false; if (!TotpHelper.VerifyCode(_pending2FASecret, code)) return false; var col = _db.GetCollection("users"); var user = col.FindOne(u => u.TwoFactorSecret == _pending2FASecret); if (user == null) return false; await SetAuthenticatedUser(user); return true; } public Task Generate2FASecretAsync() { return Task.FromResult(TotpHelper.GenerateSecret()); } public string Get2FAProvisioningUri(string secret, string username) { return TotpHelper.GetProvisioningUri(secret, username); } public async Task Enable2FAAsync(string secret) { if (_currentUser == null) return false; var col = _db.GetCollection("users"); var user = col.FindById(_currentUser.Id); if (user == null) return false; user.TwoFactorSecret = secret; user.TwoFactorEnabled = true; col.Update(user); _currentUser.TwoFactorSecret = secret; _currentUser.TwoFactorEnabled = true; return true; } public async Task Disable2FAAsync() { if (_currentUser == null) return false; var col = _db.GetCollection("users"); var user = col.FindById(_currentUser.Id); if (user == null) return false; user.TwoFactorSecret = string.Empty; user.TwoFactorEnabled = false; col.Update(user); _currentUser.TwoFactorSecret = string.Empty; _currentUser.TwoFactorEnabled = false; return true; } public async Task Is2FAEnabledAsync() { if (_currentUser == null) return false; var col = _db.GetCollection("users"); var user = col.FindById(_currentUser.Id); return user?.TwoFactorEnabled ?? false; } public async Task GetCurrent2FASecretAsync() { if (_currentUser == null) return null; var col = _db.GetCollection("users"); var user = col.FindById(_currentUser.Id); return user?.TwoFactorSecret; } private async Task SetAuthenticatedUser(UserEntity user) { _currentUser = user; _requires2FA = false; _pending2FASecret = null; _settingsService.CurrentProjectId = null; _settingsService.CurrentFilePath = null; _settingsService.SetCurrentUser(user.Id); await _settingsService.InitializeAsync(); await _loc.InitAsync(); } public Task> GetAllUsersAsync() { var col = _db.GetCollection("users"); return Task.FromResult(col.FindAll().OrderBy(u => u.Name).ToList()); } public async Task SetUserActiveAsync(int userId, bool active) { if (_currentUser != null && userId == _currentUser.Id) return false; var col = _db.GetCollection("users"); var user = col.FindById(userId); if (user == null) return false; user.IsActive = active; col.Update(user); return true; } public async Task DeleteUserAsync(int userId) { if (_currentUser != null && userId == _currentUser.Id) return false; var col = _db.GetCollection("users"); var user = col.FindById(userId); if (user == null) return false; col.Delete(userId); return true; } public async Task CreateUserAsync(string username, string password, string email, List roles) { var col = _db.GetCollection("users"); var existing = col.FindOne(u => u.Name == username); if (existing != null) return false; if (roles.Contains(UserRole.Administrator) && AdminCount() >= 1) return false; var user = new UserEntity { Name = username, Email = email, PasswordHash = TotpHelper.HashPassword(password), Roles = roles, TwoFactorEnabled = false, TwoFactorSecret = string.Empty, IsActive = true }; col.Insert(user); await Task.CompletedTask; return true; } public async Task UpdateUserAsync(int userId, string? email, List? roles) { var col = _db.GetCollection("users"); var user = col.FindById(userId); if (user == null) return false; if (email != null) user.Email = email; if (roles != null) { var currentlyAdmin = user.Roles.Contains(UserRole.Administrator); var willBeAdmin = roles.Contains(UserRole.Administrator); if (willBeAdmin && !currentlyAdmin && AdminCount() >= 1) return false; if (!willBeAdmin && currentlyAdmin && AdminCount(userId) == 0) return false; user.Roles = roles; } col.Update(user); await Task.CompletedTask; return true; } public async Task ResetPasswordAsync(int userId) { var col = _db.GetCollection("users"); var user = col.FindById(userId); if (user == null) return string.Empty; using var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); var bytes = new byte[10]; rng.GetBytes(bytes); var newPassword = Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', 'X').Replace('/', 'Y'); user.PasswordHash = TotpHelper.HashPassword(newPassword); col.Update(user); await Task.CompletedTask; return newPassword; } private int AdminCount(int? excludeUserId = null) { var col = _db.GetCollection("users"); return col.FindAll().Count(u => u.Roles.Contains(UserRole.Administrator) && (!excludeUserId.HasValue || u.Id != excludeUserId.Value)); } }