using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Auth; public class SeedLocalAdminFromEnvironmentHandler( IDbContextFactory dbContextFactory, ILocalPasswordHasher passwordHasher) : IRequestHandler> { public async Task> Handle( SeedLocalAdminFromEnvironment request, CancellationToken cancellationToken) { string username = (request.Username ?? string.Empty).Trim(); if (username.Length == 0) { username = "admin"; } if (username.Length > LocalAdminHelpers.MaxUsernameLength) { return BaseError.New("Seed username is too long"); } foreach (BaseError error in LocalAdminHelpers.ValidatePassword(request.Password)) { 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); // Overwrite (recovery/bootstrap) atomically: username + new hash + rotated stamp commit together. Upsert(dbContext, rows, ConfigElementKey.AuthLocalAdminUsername.Key, username); Upsert(dbContext, rows, ConfigElementKey.AuthLocalAdminPasswordHash.Key, passwordHasher.Hash(request.Password)); Upsert(dbContext, rows, ConfigElementKey.AuthSecurityStamp.Key, LocalAdminHelpers.NewSecurityStamp()); await dbContext.SaveChangesAsync(cancellationToken); return Unit.Default; } private static void Upsert(TvContext dbContext, List existing, string key, string value) { ConfigElement row = existing.Find(r => r.Key == key); if (row is null) { dbContext.ConfigElements.Add(new ConfigElement { Key = key, Value = value }); } else { row.Value = value; } } }