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