using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using BCrypt.Net; using FinlyticBackend.Database; using FinlyticBackend.Entities; using FinlyticCore.Models.Auth; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace FinlyticBackend.Services; /// /// Interface for user management and authentication operations. /// public interface IUserService { /// Authenticates user credentials and returns JWT response. Task AuthenticateAsync(LoginRequestDto request, CancellationToken cancellationToken = default); /// Registers a new user account. Task RegisterUserAsync(RegisterRequestDto request, CancellationToken cancellationToken = default); /// Changes the initial password for a user. Task ChangeInitialPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken = default); /// Creates a new user by an admin. Task CreateUserByAdminAsync(CreateUserRequestDto request, CancellationToken cancellationToken = default); /// Updates an existing user account. Task UpdateUserAsync(Guid userId, UpdateUserRequestDto request, CancellationToken cancellationToken = default); /// Resets a user's password by admin. Task ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken = default); /// Registers or updates FCM device token. Task RegisterOrUpdateFcmTokenAsync(Guid userId, string fcmToken, string deviceName, CancellationToken cancellationToken = default); /// Gets all registered users. Task> GetAllUsersAsync(CancellationToken cancellationToken = default); /// Deactivates a user account. Task DeactivateUserAsync(Guid userId, CancellationToken cancellationToken = default); /// Seeds default admin account if not present. Task SeedDefaultAdminAsync(string defaultAdminPassword, CancellationToken cancellationToken = default); } public class UserService : IUserService { private readonly IServiceScopeFactory _scopeFactory; private readonly IJwtTokenService _jwtTokenService; private readonly ILogger _logger; public UserService( IServiceScopeFactory scopeFactory, IJwtTokenService jwtTokenService, ILogger logger) { _scopeFactory = scopeFactory; _jwtTokenService = jwtTokenService; _logger = logger; } /// public async Task AuthenticateAsync(LoginRequestDto request, CancellationToken cancellationToken = default) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var user = await dbContext.Users .Include(u => u.DeviceTokens) .FirstOrDefaultAsync(u => u.Email.ToLower() == request.Email.ToLower(), cancellationToken); if (user == null || !user.IsActive) { _logger.LogWarning("[{Channel}] Authentication failed: User '{Email}' not found or inactive.", "AuthChannel", request.Email); return null; } if (!BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash)) { _logger.LogWarning("[{Channel}] Authentication failed: Invalid password for '{Email}'.", "AuthChannel", request.Email); return null; } user.LastLoginAt = DateTime.UtcNow; await dbContext.SaveChangesAsync(cancellationToken); var (token, expiresAt) = _jwtTokenService.GenerateToken(user); return new AuthResponseDto { Token = token, UserId = user.Id, Email = user.Email, FullName = user.FullName, Role = user.Role, ThemePreference = string.IsNullOrWhiteSpace(user.ThemePreference) ? "fluent_dark" : user.ThemePreference, FcmTokens = user.DeviceTokens.Select(t => t.FcmToken).ToList(), ExpiresAt = expiresAt, RequiresPasswordChange = user.RequiresPasswordChange }; } /// public async Task RegisterUserAsync(RegisterRequestDto request, CancellationToken cancellationToken = default) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); string normalizedEmail = request.Email.Trim().ToLowerInvariant(); bool exists = await dbContext.Users.AnyAsync(u => u.Email.ToLower() == normalizedEmail, cancellationToken); if (exists) { _logger.LogWarning("[{Channel}] Registration failed: Email '{Email}' is already taken.", "AuthChannel", request.Email); return null; } string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password); var newUser = new UserEntity { Email = normalizedEmail, PasswordHash = passwordHash, FullName = string.IsNullOrWhiteSpace(request.FullName) ? normalizedEmail.Split('@')[0] : request.FullName, Role = "User", ThemePreference = "fluent_dark", // Default Theme for FluentAvalonia IsActive = true, CreatedAt = DateTime.UtcNow, LastLoginAt = DateTime.UtcNow }; dbContext.Users.Add(newUser); await dbContext.SaveChangesAsync(cancellationToken); var (token, expiresAt) = _jwtTokenService.GenerateToken(newUser); return new AuthResponseDto { Token = token, UserId = newUser.Id, Email = newUser.Email, FullName = newUser.FullName, Role = newUser.Role, ThemePreference = newUser.ThemePreference, FcmTokens = new List(), ExpiresAt = expiresAt }; } /// public async Task CreateUserByAdminAsync(CreateUserRequestDto request, CancellationToken cancellationToken = default) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); string normalizedEmail = request.Email.Trim().ToLowerInvariant(); bool exists = await dbContext.Users.AnyAsync(u => u.Email.ToLower() == normalizedEmail, cancellationToken); if (exists) { _logger.LogWarning("[{Channel}] Cannot create user: Email '{Email}' already exists.", "AuthChannel", request.Email); return null; } string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password); var newUser = new UserEntity { Email = normalizedEmail, PasswordHash = passwordHash, FullName = request.FullName, Role = string.Equals(request.Role, "Admin", StringComparison.OrdinalIgnoreCase) ? "Admin" : "User", ThemePreference = "fluent_dark", IsActive = true, RequiresPasswordChange = true, CreatedAt = DateTime.UtcNow }; dbContext.Users.Add(newUser); await dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("[{Channel}] Admin created user '{Email}' with Role '{Role}'.", "AuthChannel", newUser.Email, newUser.Role); return MapToUserDto(newUser); } /// public async Task UpdateUserAsync(Guid userId, UpdateUserRequestDto request, CancellationToken cancellationToken = default) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var user = await dbContext.Users .Include(u => u.DeviceTokens) .FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); if (user == null) return null; if (!string.IsNullOrWhiteSpace(request.Role)) { user.Role = request.Role; } if (request.IsActive.HasValue) { user.IsActive = request.IsActive.Value; } if (!string.IsNullOrWhiteSpace(request.FullName)) { user.FullName = request.FullName; } await dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("[{Channel}] Updated user '{UserId}' details.", "AuthChannel", userId); return MapToUserDto(user); } /// public async Task ChangeInitialPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken = default) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); if (user == null || !user.RequiresPasswordChange) return false; user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(newPassword); user.RequiresPasswordChange = false; await dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("[{Channel}] User '{UserId}' changed their initial password.", "AuthChannel", userId); return true; } /// public async Task ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken = default) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); if (user == null) return false; user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(newPassword); user.RequiresPasswordChange = true; await dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("[{Channel}] Admin reset password for user '{UserId}'.", "AuthChannel", userId); return true; } /// public async Task RegisterOrUpdateFcmTokenAsync(Guid userId, string fcmToken, string deviceName, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(fcmToken)) return false; using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var user = await dbContext.Users .Include(u => u.DeviceTokens) .FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); if (user == null || !user.IsActive) return false; var existingToken = user.DeviceTokens.FirstOrDefault(t => t.FcmToken == fcmToken); if (existingToken != null) { existingToken.DeviceName = deviceName; existingToken.LastUsedAt = DateTime.UtcNow; } else { user.DeviceTokens.Add(new UserDeviceTokenEntity { UserId = user.Id, FcmToken = fcmToken, DeviceName = deviceName, RegisteredAt = DateTime.UtcNow, LastUsedAt = DateTime.UtcNow }); } await dbContext.SaveChangesAsync(cancellationToken); return true; } /// public async Task> GetAllUsersAsync(CancellationToken cancellationToken = default) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var users = await dbContext.Users .AsNoTracking() .Include(u => u.DeviceTokens) .OrderByDescending(u => u.CreatedAt) .ToListAsync(cancellationToken); return users.Select(MapToUserDto).ToList(); } /// public async Task DeactivateUserAsync(Guid userId, CancellationToken cancellationToken = default) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); if (user == null) return false; user.IsActive = false; await dbContext.SaveChangesAsync(cancellationToken); return true; } /// public async Task SeedDefaultAdminAsync(string defaultAdminPassword, CancellationToken cancellationToken = default) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); string adminEmail = "admin@finlytic.com"; bool exists = await dbContext.Users.AnyAsync(u => u.Email == adminEmail, cancellationToken); if (!exists) { string password = !string.IsNullOrWhiteSpace(defaultAdminPassword) ? defaultAdminPassword : "AdminDefaultPassword2026!"; var adminUser = new UserEntity { Email = adminEmail, PasswordHash = BCrypt.Net.BCrypt.HashPassword(password), FullName = "System Administrator", Role = "Admin", ThemePreference = "fluent_dark", IsActive = true, CreatedAt = DateTime.UtcNow }; dbContext.Users.Add(adminUser); await dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("[{Channel}] Seeded default Admin user: {Email}", "AuthChannel", adminEmail); } } private static UserDto MapToUserDto(UserEntity user) { return new UserDto { Id = user.Id, Email = user.Email, FullName = user.FullName, Role = user.Role, IsActive = user.IsActive, ThemePreference = string.IsNullOrWhiteSpace(user.ThemePreference) ? "fluent_dark" : user.ThemePreference, FcmTokens = user.DeviceTokens?.Select(t => t.FcmToken).ToList() ?? new List(), CreatedAt = user.CreatedAt, LastLoginAt = user.LastLoginAt }; } }