using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Auth; public class ClaimLocalAdminHandler(IDbContextFactory dbContextFactory, ILocalPasswordHasher passwordHasher) : IRequestHandler> { public async Task> Handle( ClaimLocalAdmin request, CancellationToken cancellationToken) { foreach (BaseError error in LocalAdminHelpers.ValidateNewCredentials(request.Username, request.Password)) { return error; } string username = request.Username.Trim(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); // Fast path for the common already-configured case (clean 409). The real first-claim-wins guard is // the unique index on ConfigElement.Key + the single atomic SaveChanges below: two concurrent claims // both pass this check, but only one INSERT of the three credential rows commits — the loser's // SaveChanges violates the unique Key index and rolls back wholesale (no mixed-state credential). bool alreadyConfigured = await dbContext.ConfigElements .AnyAsync(c => c.Key == ConfigElementKey.AuthLocalAdminPasswordHash.Key, cancellationToken); if (alreadyConfigured) { return BaseError.New("A local administrator has already been configured"); } string stamp = LocalAdminHelpers.NewSecurityStamp(); dbContext.ConfigElements.AddRange( new ConfigElement { Key = ConfigElementKey.AuthLocalAdminUsername.Key, Value = username }, new ConfigElement { Key = ConfigElementKey.AuthLocalAdminPasswordHash.Key, Value = passwordHasher.Hash(request.Password) }, new ConfigElement { Key = ConfigElementKey.AuthSecurityStamp.Key, Value = stamp }); try { await dbContext.SaveChangesAsync(cancellationToken); } catch (DbUpdateException) { // A write conflict here is (almost always) a lost first-claim race — a concurrent claim inserted // these keys first (unique Key index). Confirm the row now exists on a fresh context before // reporting "already configured"; otherwise this was a genuine/transient DB error → rethrow rather // than mask it. await using TvContext verifyContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); bool nowConfigured = await verifyContext.ConfigElements .AnyAsync(c => c.Key == ConfigElementKey.AuthLocalAdminPasswordHash.Key, cancellationToken); if (nowConfigured) { return BaseError.New("A local administrator has already been configured"); } throw; } return new LocalAdminPrincipal(username, stamp); } }