using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Auth; public class ChangeLocalAdminPasswordHandler( IDbContextFactory dbContextFactory, ILocalPasswordHasher passwordHasher) : IRequestHandler> { public async Task> Handle( ChangeLocalAdminPassword request, CancellationToken cancellationToken) { foreach (BaseError error in LocalAdminHelpers.ValidatePassword(request.NewPassword)) { return error; } await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); List rows = await dbContext.ConfigElements .Where(c => c.Key == ConfigElementKey.AuthLocalAdminUsername.Key || c.Key == ConfigElementKey.AuthLocalAdminPasswordHash.Key || c.Key == ConfigElementKey.AuthSecurityStamp.Key) .ToListAsync(cancellationToken); ConfigElement userRow = rows.Find(r => r.Key == ConfigElementKey.AuthLocalAdminUsername.Key); ConfigElement hashRow = rows.Find(r => r.Key == ConfigElementKey.AuthLocalAdminPasswordHash.Key); ConfigElement stampRow = rows.Find(r => r.Key == ConfigElementKey.AuthSecurityStamp.Key); if (hashRow is null) { return BaseError.New("No local administrator is configured"); } string username = (request.Username ?? string.Empty).Trim(); bool userMatches = userRow is not null && string.Equals(userRow.Value, username, StringComparison.OrdinalIgnoreCase); LocalPasswordVerification result = passwordHasher.Verify(hashRow.Value, request.CurrentPassword ?? string.Empty); if (!userMatches || result == LocalPasswordVerification.Failed) { return BaseError.New("Current password is incorrect"); } // Atomic: the new hash and rotated stamp commit together, so a crash can't leave the new password // active with the old stamp still authorizing revoked sessions. string stamp = LocalAdminHelpers.NewSecurityStamp(); hashRow.Value = passwordHasher.Hash(request.NewPassword); if (stampRow is null) { dbContext.ConfigElements.Add(new ConfigElement { Key = ConfigElementKey.AuthSecurityStamp.Key, Value = stamp }); } else { stampRow.Value = stamp; } await dbContext.SaveChangesAsync(cancellationToken); return new LocalAdminPrincipal(userRow.Value, stamp); } }