удаление test-контура (дополнение)
This commit is contained in:
@@ -38,7 +38,7 @@ def _ensure_admin(session: Session) -> None:
|
||||
|
||||
# Администратор уже существует.
|
||||
if not settings.is_development:
|
||||
# В test/prod пароль НЕ перезаписываем (мог быть изменён через панель).
|
||||
# В prod пароль НЕ перезаписываем (мог быть изменён через панель).
|
||||
return
|
||||
|
||||
# DEV: подтягиваем логин/пароль из .env (env — источник истины в деве).
|
||||
|
||||
+19
-10
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import model_validator
|
||||
from pydantic import field_validator, model_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
# Единый .env лежит в КОРНЕ репозитория (рядом с .env.example) — читается одинаково
|
||||
@@ -18,6 +18,9 @@ _DEFAULT_SECRET_KEY = "change-me-dev-secret-not-for-production"
|
||||
_DEFAULT_ADMIN_PASSWORD = "change-me-admin-password"
|
||||
_MIN_SECRET_KEY_LENGTH = 32
|
||||
|
||||
# Допустимые окружения. Отдельного test-контура нет: всё проверяется в development.
|
||||
_APP_ENVS = ("development", "production")
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
@@ -27,14 +30,13 @@ class Settings(BaseSettings):
|
||||
case_sensitive=False,
|
||||
)
|
||||
|
||||
# ── Главный переключатель окружения: development | test | production ───────
|
||||
# ── Главный переключатель окружения: development | production ─────────────
|
||||
# development — нативный dev (uvicorn + vite), БД в ./data/dev/, вход Telegram+ник.
|
||||
# test — прод-клон в Docker локально (порт 8080), ведёт себя как прод.
|
||||
# production — Docker на Pi; контейнер форсит это значение, игнорируя .env.
|
||||
app_env: str = "development"
|
||||
log_level: str = "INFO"
|
||||
|
||||
# Публикация локального окружения (dev/test) наружу через VPS-туннель.
|
||||
# Публикация локального dev-окружения наружу через VPS-туннель.
|
||||
# Читает ЛАУНЧЕР (run.ps1/run.sh): local — только localhost; vps — плюс SSH-туннель
|
||||
# на forbidden-stars.ru. Влияет на cookie_secure (vps ⇒ снаружи HTTPS ⇒ Secure-cookie).
|
||||
local_public: str = "local"
|
||||
@@ -91,17 +93,13 @@ class Settings(BaseSettings):
|
||||
стартовый bootstrap в lifespan и синхронизацию админа из .env."""
|
||||
return self.app_env.lower() == "development"
|
||||
|
||||
@property
|
||||
def is_test(self) -> bool:
|
||||
return self.app_env.lower() == "test"
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
return self.app_env.lower() == "production"
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""БД: dev — отдельный файл дева; test и prod — том контейнера (/data)."""
|
||||
"""БД: dev — отдельный файл дева; prod — том контейнера (/data)."""
|
||||
return self.dev_database_url if self.is_development else self.prod_database_url
|
||||
|
||||
@property
|
||||
@@ -122,13 +120,24 @@ class Settings(BaseSettings):
|
||||
def cookie_domain_value(self) -> str | None:
|
||||
return self.cookie_domain or None
|
||||
|
||||
@field_validator("app_env")
|
||||
@classmethod
|
||||
def _known_app_env(cls, value: str) -> str:
|
||||
"""Неизвестное окружение — ошибка старта, а не молчаливое «почти прод»: любое
|
||||
значение, кроме development, выбирает прод-пути к данным и выключает dev-вход."""
|
||||
if value.lower() not in _APP_ENVS:
|
||||
raise ValueError(
|
||||
f"APP_ENV={value!r} не поддерживается — допустимо: {', '.join(_APP_ENVS)}"
|
||||
)
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _forbid_default_secrets_in_prod(self) -> "Settings":
|
||||
"""Fail-fast: в production не стартуем с дефолтными/слабыми секретами (#59).
|
||||
|
||||
Деплой, скопировавший .env.example дословно (или забывший поле), иначе поднялся бы
|
||||
с общеизвестным ключом подписи JWT (подделка любого токена, включая админский) и
|
||||
известным паролем администратора. В dev/test проверка не мешает — там дефолты норма."""
|
||||
известным паролем администратора. В dev проверка не мешает — там дефолты норма."""
|
||||
if self.app_env.lower() != "production":
|
||||
return self
|
||||
problems: list[str] = []
|
||||
|
||||
+4
-4
@@ -132,7 +132,7 @@ async def _lifespan(_app: FastAPI):
|
||||
hub.bind_loop(asyncio.get_running_loop())
|
||||
|
||||
# В DEV приложение само подтягивает справочники и админа из .env при старте
|
||||
# (в test/prod это делает entrypoint.sh; в pytest отключено FS_STARTUP_BOOTSTRAP=0).
|
||||
# (в prod это делает entrypoint.sh; в pytest отключено FS_STARTUP_BOOTSTRAP=0).
|
||||
if settings.is_development and os.getenv("FS_STARTUP_BOOTSTRAP", "1") != "0":
|
||||
try:
|
||||
from app.bootstrap import bootstrap
|
||||
@@ -151,7 +151,7 @@ async def _lifespan(_app: FastAPI):
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
# Схему API (openapi.json + Swagger/ReDoc) отдаём только в dev/test: она нужна для
|
||||
# Схему API (openapi.json + Swagger/ReDoc) отдаём только в dev: она нужна для
|
||||
# `npm run gen:api` (генерация типов фронта) и удобной отладки. В production закрываем —
|
||||
# незачем облегчать разведку поверхности API анонимам (#61).
|
||||
docs_enabled = not settings.is_production
|
||||
@@ -165,7 +165,7 @@ def create_app() -> FastAPI:
|
||||
)
|
||||
|
||||
# CORS нужен только в dev (vite на :5173 и API на :8000 — разные origin).
|
||||
# В test/prod (и dev через VPS-туннель) всё single-origin → CORS не подключаем.
|
||||
# В prod (и в dev через VPS-туннель) всё single-origin → CORS не подключаем.
|
||||
if settings.is_development and settings.cors_origins_list:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -204,7 +204,7 @@ def create_app() -> FastAPI:
|
||||
app.include_router(r, prefix="/api")
|
||||
|
||||
# DEV-роутеры (вход по нику, жёсткое удаление аккаунтов) — только в development
|
||||
# и только если код физически есть (в test/prod-образе dev_*-файлы исключены
|
||||
# и только если код физически есть (в прод-образе dev_*-файлы исключены
|
||||
# .dockerignore, импорт просто не выполнится).
|
||||
if settings.is_development:
|
||||
for mod_name in ("dev_auth", "dev_admin"):
|
||||
|
||||
@@ -153,7 +153,7 @@ def set_user_password(
|
||||
|
||||
|
||||
# Удаление аккаунта — намеренно НЕ здесь: это dev-only возможность, вынесена в
|
||||
# routers/dev_admin.py (исключён из прод/тест-образа). В проде аккаунт только
|
||||
# routers/dev_admin.py (исключён из прод-образа). В проде аккаунт только
|
||||
# отключается (PATCH is_active), удалять нельзя.
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""DEV-ТОЛЬКО роутер: жёсткое удаление аккаунта игрока.
|
||||
|
||||
Этот файл ФИЗИЧЕСКИ исключён из прод/тест-образа (.dockerignore), а роутер
|
||||
Этот файл ФИЗИЧЕСКИ исключён из прод-образа (.dockerignore), а роутер
|
||||
подключается лишь когда APP_ENV == development (см. app/main.py). На фронте кнопка
|
||||
удаления вырезается из прод-сборки тришейкингом (import.meta.env.DEV). Так
|
||||
возможность удаления не попадает ни в прод, ни в тест — там аккаунт можно только
|
||||
возможность удаления не попадает в прод — там аккаунт можно только
|
||||
отключить (PATCH is_active).
|
||||
|
||||
Семантика («вычёркивание из партий»): аккаунт удаляется, а партии сохраняются —
|
||||
|
||||
@@ -74,7 +74,7 @@ def set_player_password(session: Session, user_id: int, new_password: str) -> Us
|
||||
|
||||
|
||||
# Жёсткое удаление пользователя — dev-only, в services/admin_service нет намеренно:
|
||||
# логика вынесена в routers/dev_admin.py (файл исключён из прод/тест-образа).
|
||||
# логика вынесена в routers/dev_admin.py (файл исключён из прод-образа).
|
||||
|
||||
|
||||
# ─── Группы ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user