namespace foodmarket.Api.Infrastructure; /// Опции «быстрого входа» на тестовом стенде. По умолчанию выключен. /// Включается ТОЛЬКО переменной окружения QuickLogin__Enabled=true /// в stage-compose. В прод-compose этой переменной нет — флаг остаётся /// в false и ни эндпоинт списка аккаунтов, ни «мастер-пароль» не включаются. /// /// Жёсткий предохранитель : даже если флаг /// случайно пробросили в прод, но OpenIddict:Issuer указывает на /// admin.food-market.kz — быстрый вход всё равно выключен. Это защищает от /// ошибок конфигурации (ошибочный set-env на prod-env) — конфиг-опечатка /// не должна открыть прод. public sealed class QuickLoginOptions { /// Главный флаг. При false любой код, относящийся к quick-login, /// должен вести себя как в проде (эндпоинт 404, мастер-пароль не работает, /// плашка не рендерится). public bool Enabled { get; set; } = false; /// Единый пароль-ключ для стенда. При включённом флаге подходит /// к ЛЮБОМУ активному аккаунту в /connect/token grant=password. /// Собственный пароль пользователя тоже продолжает работать (чтобы не /// ломать e2e, seeded admin и т.д.). public string MasterPassword { get; set; } = "123456"; /// Эффективный флаг с учётом safety-override по Issuer. Используй /// везде, где решается «пускать мастер-пароль / отдавать список». Чтение /// сравнивает Issuer с prod-доменом admin.food-market.kz — если совпадает /// (или Issuer пустой), быстрый вход считается выключенным и в лог /// пишется Warning один раз на старте через . public bool EffectivelyEnabled(string? issuer, out string reason) { if (!Enabled) { reason = "QuickLogin:Enabled=false"; return false; } // Issuer обязателен — если его нет, не рискуем: dev/stage без явного // Issuer не должны открывать мастер-пароль, иначе любое неверно // настроенное окружение превращается в стенд. if (string.IsNullOrWhiteSpace(issuer)) { reason = "OpenIddict:Issuer пустой — не могу подтвердить, что это стенд"; return false; } // Прод-домен в lower-case: Uri сам нормализует хост, но подстрахуем. if (!Uri.TryCreate(issuer, UriKind.Absolute, out var uri)) { reason = $"OpenIddict:Issuer '{issuer}' не парсится как Uri"; return false; } var host = uri.Host.ToLowerInvariant(); // ТОЛЬКО точное совпадение с прод-хостом. Любой поддомен (test.admin.*, // dev.admin.*) — это стенд, его трогать не надо. Если в будущем // появится второй прод-хост, сюда дописывается новое условие. if (host == "admin.food-market.kz") { reason = $"OpenIddict:Issuer host={host} — это ПРОД, быстрый вход принудительно выключен"; return false; } reason = $"OpenIddict:Issuer host={host} — стенд, быстрый вход включён"; return true; } /// Одноразовый лог при старте приложения. Пишется в лог, чтобы /// в проде всегда было видно: «флаг пришёл, но Issuer=prod, выключили». /// Вызывается из Program.cs после Build(). public void LogSafetyDecision(ILogger logger, string? issuer) { var effective = EffectivelyEnabled(issuer, out var reason); if (effective) { logger.LogWarning("QuickLogin включён для стенда: {Reason}. " + "Это норма для test.admin.food-market.kz. В проде должно быть выключено.", reason); } else if (Enabled) { // Enabled=true, но effective=false — явный safety-переключатель. Это серьёзно. logger.LogWarning("QuickLogin:Enabled=true, но быстрый вход НЕ активирован. {Reason}", reason); } // Если Enabled=false — молчим, это обычный прод. } }