using System.Security.Claims; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using SharepointToolbox.Web.Core.Models; using SharepointToolbox.Web.Infrastructure.Persistence; namespace SharepointToolbox.Web.Services.Auth; public class UserService : IUserService { private readonly UserRepository _repo; private readonly IPasswordHasher _hasher; private readonly ILogger _logger; public UserService(UserRepository repo, IPasswordHasher hasher, ILogger logger) { _repo = repo; _hasher = hasher; _logger = logger; } public async Task ProvisionAsync(ClaimsPrincipal principal) { var email = principal.FindFirstValue(ClaimTypes.Email) ?? principal.FindFirstValue("preferred_username") ?? throw new InvalidOperationException("OIDC token has no email claim."); var display = principal.FindFirstValue("name") ?? principal.FindFirstValue(ClaimTypes.Name) ?? email; var existing = await _repo.FindByEmailAsync(email); if (existing is not null) { existing.LastLogin = DateTimeOffset.UtcNow; existing.DisplayName = display; await _repo.UpsertAsync(existing); return existing; } // First user ever → Admin; subsequent → TechN0 var all = await _repo.LoadAsync(); var role = all.Count == 0 ? UserRole.Admin : UserRole.TechN0; var user = new AppUser { Email = email, DisplayName = display, Role = role, Provider = AuthProvider.Entra, CreatedAt = DateTimeOffset.UtcNow, LastLogin = DateTimeOffset.UtcNow }; await _repo.UpsertAsync(user); return user; } public Task GetByEmailAsync(string email) => _repo.FindByEmailAsync(email); public Task> GetAllAsync() => _repo.LoadAsync(); public async Task UpdateRoleAsync(string userId, UserRole role) { var users = (await _repo.LoadAsync()).ToList(); var user = users.FirstOrDefault(u => u.Id == userId) ?? throw new KeyNotFoundException($"User '{userId}' not found among {users.Count} stored users."); var oldRole = user.Role; user.Role = role; await _repo.UpsertAsync(user); // Verify the write landed by re-reading the row from disk. var persisted = (await _repo.LoadAsync()).FirstOrDefault(u => u.Id == user.Id)?.Role; _logger.LogInformation( "UpdateRoleAsync: {Email} (id {Id}) {OldRole} → {NewRole}; persisted value now {Persisted}.", user.Email, user.Id, oldRole, role, persisted); return oldRole; } public Task DeleteAsync(string userId) => _repo.DeleteAsync(userId); public async Task CreateLocalUserAsync(string email, string displayName, UserRole role, string password) { email = email.Trim(); if (string.IsNullOrWhiteSpace(email)) throw new InvalidOperationException("Email is required."); if (string.IsNullOrWhiteSpace(password)) throw new InvalidOperationException("Password is required."); if (await _repo.FindByEmailAsync(email) is not null) throw new InvalidOperationException($"A user with email '{email}' already exists."); // First user ever → Admin; otherwise use the requested role var all = await _repo.LoadAsync(); var effectiveRole = all.Count == 0 ? UserRole.Admin : role; var user = new AppUser { Email = email, DisplayName = string.IsNullOrWhiteSpace(displayName) ? email : displayName.Trim(), Role = effectiveRole, Provider = AuthProvider.Local, CreatedAt = DateTimeOffset.UtcNow, LastLogin = null }; user.PasswordHash = _hasher.HashPassword(user, password); await _repo.UpsertAsync(user); return user; } public async Task ValidateLocalCredentialsAsync(string email, string password) { var user = await _repo.FindByEmailAsync(email); if (user is null || user.Provider != AuthProvider.Local || string.IsNullOrEmpty(user.PasswordHash)) return null; var result = _hasher.VerifyHashedPassword(user, user.PasswordHash, password); if (result == PasswordVerificationResult.Failed) return null; // Transparently upgrade the hash if the algorithm parameters changed if (result == PasswordVerificationResult.SuccessRehashNeeded) user.PasswordHash = _hasher.HashPassword(user, password); user.LastLogin = DateTimeOffset.UtcNow; await _repo.UpsertAsync(user); return user; } public async Task SetPasswordAsync(string userId, string newPassword) { if (string.IsNullOrWhiteSpace(newPassword)) throw new InvalidOperationException("Password is required."); var users = (await _repo.LoadAsync()).ToList(); var user = users.FirstOrDefault(u => u.Id == userId) ?? throw new KeyNotFoundException($"User {userId} not found."); if (user.Provider != AuthProvider.Local) throw new InvalidOperationException("Only local accounts have passwords."); user.PasswordHash = _hasher.HashPassword(user, newPassword); await _repo.UpsertAsync(user); } public async Task ChangePasswordAsync(string userId, string currentPassword, string newPassword) { if (string.IsNullOrWhiteSpace(newPassword)) throw new InvalidOperationException("New password is required."); var users = (await _repo.LoadAsync()).ToList(); var user = users.FirstOrDefault(u => u.Id == userId) ?? throw new KeyNotFoundException($"User {userId} not found."); if (user.Provider != AuthProvider.Local || string.IsNullOrEmpty(user.PasswordHash)) return false; var result = _hasher.VerifyHashedPassword(user, user.PasswordHash, currentPassword); if (result == PasswordVerificationResult.Failed) return false; user.PasswordHash = _hasher.HashPassword(user, newPassword); await _repo.UpsertAsync(user); return true; } }