using ErsatzTV.Application.Auth; using ErsatzTV.Core; using LanguageExt; using MediatR; namespace ErsatzTV.Services.RunOnce; /// /// Recovery/bootstrap: when Auth:LocalAdmin:Password is configured, (re)seeds the single local /// administrator at startup (issue #295). Overwrites any existing credential and rotates the security /// stamp, so an operator locked out of the browser UI can reset by setting the env and restarting. A /// no-op when unset. Follows the RunOnce pattern (waits for the database to be ready — the migrator is a /// BackgroundService, so registration order alone does not guarantee the schema exists). /// public class LocalAdminSeedService( IServiceScopeFactory serviceScopeFactory, IConfiguration configuration, SystemStartup systemStartup, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await Task.Yield(); string password = configuration["Auth:LocalAdmin:Password"]; if (string.IsNullOrWhiteSpace(password)) { return; } await systemStartup.WaitForDatabase(stoppingToken); if (stoppingToken.IsCancellationRequested) { return; } string username = configuration["Auth:LocalAdmin:Username"]; try { using IServiceScope scope = serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); Either result = await mediator.Send(new SeedLocalAdminFromEnvironment(username, password), stoppingToken); result.Match( Right: _ => logger.LogWarning( "Seeded the local administrator from Auth:LocalAdmin:* configuration (any existing " + "credential was overwritten and all sessions revoked). Unset Auth:LocalAdmin:Password " + "after signing in."), Left: error => logger.LogError( "Failed to seed the local administrator from configuration: {Error}", error.Value)); } catch (Exception ex) { logger.LogError(ex, "Failed to seed the local administrator from configuration"); } } }