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");
}
}
}