Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
97 lines
5.8 KiB
C#
97 lines
5.8 KiB
C#
using Deal.Modules.Tenants.Application;
|
||
|
||
namespace Deal.Api.Hosting;
|
||
|
||
/// <summary>
|
||
/// Hosted-шаг bootstrap оператора при старте (Ruling 1 этапа 7): env DEAL_OPERATOR_* → OperatorBootstrapService.EnsureOperatorAsync.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Регистрируется после <see cref="TenantBootstrapService"/> и работает тем же паттерном: 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).
|
||
/// </remarks>
|
||
public sealed class OperatorBootstrapHostedService(
|
||
IServiceScopeFactory scopeFactory,
|
||
IConfiguration configuration,
|
||
IHostEnvironment environment,
|
||
ILogger<OperatorBootstrapHostedService> logger) : IHostedService
|
||
{
|
||
/// <inheritdoc />
|
||
public async Task StartAsync(CancellationToken ct)
|
||
{
|
||
await using var scope = scopeFactory.CreateAsyncScope();
|
||
var operatorBootstrapService = scope.ServiceProvider.GetRequiredService<OperatorBootstrapService>();
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
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);
|
||
}
|
||
}
|
||
}
|