AuthorBuddy/Services/AuthService.cs

287 lines
8.6 KiB
C#
Raw Normal View History

2026-05-21 11:04:42 +02:00
using AuthorBuddy.Web.Data;
using LiteDB;
namespace AuthorBuddy.Web.Services;
public class AuthService : IAuthService
{
private readonly ILiteDatabase _db;
private readonly ISettingsService _settingsService;
2026-05-25 06:38:16 +02:00
private readonly ILocalizationService _loc;
2026-05-21 11:04:42 +02:00
private UserEntity? _currentUser;
private string? _pending2FASecret;
private bool _requires2FA;
2026-05-25 06:38:16 +02:00
public AuthService(ILiteDatabase db, ISettingsService settingsService, ILocalizationService loc)
2026-05-21 11:04:42 +02:00
{
_db = db;
_settingsService = settingsService;
2026-05-25 06:38:16 +02:00
_loc = loc;
2026-05-21 11:04:42 +02:00
}
public bool IsLoggedIn => _currentUser != null;
public bool Is2FARequired => _requires2FA;
2026-05-25 06:38:16 +02:00
public UserEntity? CurrentUser => _currentUser;
2026-05-21 11:04:42 +02:00
public async Task<bool> LoginAsync(string username, string password)
{
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindOne(u => u.Name == username);
if (user == null || string.IsNullOrEmpty(user.PasswordHash))
return false;
2026-05-25 06:38:16 +02:00
if (!user.IsActive)
return false;
2026-05-21 11:04:42 +02:00
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<UserEntity>("users");
var user = col.FindById(userId);
if (user != null)
await SetAuthenticatedUser(user);
}
public async Task<bool> RegisterAsync(string username, string password)
{
var col = _db.GetCollection<UserEntity>("users");
var existing = col.FindOne(u => u.Name == username);
if (existing != null) return false;
2026-05-25 06:38:16 +02:00
var hasUsers = col.FindAll().Any();
2026-05-21 11:04:42 +02:00
var user = new UserEntity
{
Name = username,
PasswordHash = TotpHelper.HashPassword(password),
TwoFactorEnabled = false,
2026-05-25 06:38:16 +02:00
TwoFactorSecret = string.Empty,
2026-06-21 07:52:03 +02:00
Roles = hasUsers ? new List<UserRole> { UserRole.Author } : new List<UserRole> { UserRole.Administrator }
2026-05-21 11:04:42 +02:00
};
col.Insert(user);
await SetAuthenticatedUser(user);
return true;
}
public Task<UserEntity?> GetCurrentUserAsync()
{
return Task.FromResult(_currentUser);
}
public async Task<bool> Verify2FAAsync(string code)
{
if (string.IsNullOrEmpty(_pending2FASecret)) return false;
if (!TotpHelper.VerifyCode(_pending2FASecret, code))
return false;
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindOne(u => u.TwoFactorSecret == _pending2FASecret);
if (user == null) return false;
await SetAuthenticatedUser(user);
return true;
}
public Task<string> Generate2FASecretAsync()
{
return Task.FromResult(TotpHelper.GenerateSecret());
}
public string Get2FAProvisioningUri(string secret, string username)
{
return TotpHelper.GetProvisioningUri(secret, username);
}
public async Task<bool> Enable2FAAsync(string secret)
{
if (_currentUser == null) return false;
var col = _db.GetCollection<UserEntity>("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<bool> Disable2FAAsync()
{
if (_currentUser == null) return false;
var col = _db.GetCollection<UserEntity>("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<bool> Is2FAEnabledAsync()
{
if (_currentUser == null) return false;
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindById(_currentUser.Id);
return user?.TwoFactorEnabled ?? false;
}
public async Task<string?> GetCurrent2FASecretAsync()
{
if (_currentUser == null) return null;
var col = _db.GetCollection<UserEntity>("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;
2026-05-25 06:38:16 +02:00
_settingsService.SetCurrentUser(user.Id);
await _settingsService.InitializeAsync();
await _loc.InitAsync();
}
2026-05-21 11:04:42 +02:00
2026-05-25 06:38:16 +02:00
public Task<List<UserEntity>> GetAllUsersAsync()
{
2026-05-21 11:04:42 +02:00
var col = _db.GetCollection<UserEntity>("users");
2026-05-25 06:38:16 +02:00
return Task.FromResult(col.FindAll().OrderBy(u => u.Name).ToList());
}
2026-05-21 11:04:42 +02:00
2026-05-25 06:38:16 +02:00
public async Task<bool> SetUserActiveAsync(int userId, bool active)
{
if (_currentUser != null && userId == _currentUser.Id)
return false;
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindById(userId);
if (user == null) return false;
user.IsActive = active;
col.Update(user);
return true;
}
public async Task<bool> DeleteUserAsync(int userId)
{
if (_currentUser != null && userId == _currentUser.Id)
return false;
var col = _db.GetCollection<UserEntity>("users");
var user = col.FindById(userId);
if (user == null) return false;
col.Delete(userId);
return true;
2026-05-21 11:04:42 +02:00
}
2026-06-21 07:52:03 +02:00
public async Task<bool> CreateUserAsync(string username, string password, string email, List<UserRole> roles)
{
var col = _db.GetCollection<UserEntity>("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<bool> UpdateUserAsync(int userId, string? email, List<UserRole>? roles)
{
var col = _db.GetCollection<UserEntity>("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<string> ResetPasswordAsync(int userId)
{
var col = _db.GetCollection<UserEntity>("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<UserEntity>("users");
return col.FindAll().Count(u =>
u.Roles.Contains(UserRole.Administrator) &&
(!excludeUserId.HasValue || u.Id != excludeUserId.Value));
}
2026-05-21 11:04:42 +02:00
}