286 lines
8.6 KiB
C#
286 lines
8.6 KiB
C#
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<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;
|
|
|
|
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<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;
|
|
|
|
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> { UserRole.Author } : new List<UserRole> { UserRole.Administrator }
|
|
};
|
|
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;
|
|
_settingsService.SetCurrentUser(user.Id);
|
|
|
|
await _settingsService.InitializeAsync();
|
|
await _loc.InitAsync();
|
|
}
|
|
|
|
public Task<List<UserEntity>> GetAllUsersAsync()
|
|
{
|
|
var col = _db.GetCollection<UserEntity>("users");
|
|
return Task.FromResult(col.FindAll().OrderBy(u => u.Name).ToList());
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|