using Deal.Modules.Tenants.Application;
namespace Deal.Api.Hosting;
///
/// Hosted-шаг bootstrap оператора при старте (Ruling 1 этапа 7): env DEAL_OPERATOR_* → OperatorBootstrapService.EnsureOperatorAsync.
///
///
/// Регистрируется после и работает тем же паттерном: OperatorBootstrapService —
/// scoped (его IOperatorAuthStore живёт на scoped DealDbContext), поэтому резолвится в собственном scope из
/// IServiceScopeFactory (как TenantService в TenantBootstrapService). Идемпотентен: существующего оператора
/// не пересоздаёт и пароль не перезаписывает. В Development без env-кред используется dev-дефолт
/// operator/operator (зеркало dev-seed admin/admin, логируется как dev-режим); в Production без env-кред
/// шаг пропускается с warning — оператора заводит админ позже через env и рестарт хоста (кода регистрации
/// оператора нет). Частичная конфигурация (задана ровно одна из DEAL_OPERATOR_LOGIN/DEAL_OPERATOR_PASSWORD)
/// логируется warning — не молчаливый дефолт (решение ревью Task 2): в Development используются dev-дефолты,
/// в Production шаг пропускается. Секреты (пароли) в логи не пишутся (правило этапов 1–6).
///
public sealed class OperatorBootstrapHostedService(
IServiceScopeFactory scopeFactory,
IConfiguration configuration,
IHostEnvironment environment,
ILogger logger) : IHostedService
{
///
public async Task StartAsync(CancellationToken ct)
{
await using var scope = scopeFactory.CreateAsyncScope();
var operatorBootstrapService = scope.ServiceProvider.GetRequiredService();
var login = configuration[OperatorBootstrapService.LoginEnvKey];
var password = configuration[OperatorBootstrapService.PasswordEnvKey];
bool hasLogin = !string.IsNullOrWhiteSpace(login);
bool hasPassword = !string.IsNullOrWhiteSpace(password);
bool allowDevelopmentDefaults = environment.IsDevelopment();
// Предупреждения о пропуске/частичной конфигурации логируются ДО вызова шага — сам шаг
// в этих случаях ничего не создаёт (EnsureOperatorAsync возвращает null, хранилище не тронуто).
if (!hasLogin || !hasPassword)
{
if (hasLogin != hasPassword)
{
LogPartialConfigurationWarning(hasLogin, allowDevelopmentDefaults);
}
else if (!allowDevelopmentDefaults)
{
// Production без кред: пропуск с warning (Ruling 1) — кода регистрации оператора нет.
logger.LogWarning(
"DEAL_OPERATOR_LOGIN/DEAL_OPERATOR_PASSWORD не заданы (Production) — bootstrap оператора "
+ "пропущен. Оператор заводится позже: задайте env DEAL_OPERATOR_* и перезапустите хост.");
}
else
{
// Development без кред: штатный dev-дефолт (зеркало dev-seed admin/admin).
logger.LogInformation(
"DEAL_OPERATOR_LOGIN/DEAL_OPERATOR_PASSWORD не заданы (Development) — используется "
+ "dev-дефолт оператора {DefaultLogin}.",
OperatorBootstrapService.DefaultOperatorLogin);
}
}
var ensuredLogin = await operatorBootstrapService.EnsureOperatorAsync(
login, password, allowDevelopmentDefaults, ct);
if (ensuredLogin is not null)
{
logger.LogInformation("Bootstrap оператора: оператор {Login} присутствует.", ensuredLogin);
}
}
///
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
// Логирует warning о неполной env-конфигурации (задана одна из двух переменных).
// hasLogin: Задан ли логин (пароль при этом пуст).
// allowDevelopmentDefaults: Разрешены ли dev-дефолты (Development).
private void LogPartialConfigurationWarning(bool hasLogin, bool allowDevelopmentDefaults)
{
string missingEnv = hasLogin
? OperatorBootstrapService.PasswordEnvKey
: OperatorBootstrapService.LoginEnvKey;
if (allowDevelopmentDefaults)
{
logger.LogWarning(
"Env-конфигурация оператора неполна: не задан {MissingEnv}. В Development используются "
+ "dev-дефолты оператора {DefaultLogin}.",
missingEnv, OperatorBootstrapService.DefaultOperatorLogin);
}
else
{
logger.LogWarning(
"Env-конфигурация оператора неполна: не задан {MissingEnv}. Bootstrap оператора в Production "
+ "пропущен — оператор заводится позже через env DEAL_OPERATOR_* и рестарт хоста.",
missingEnv);
}
}
}