"""Идемпотентный бутстрап: справочники + учётная запись администратора. Запуск: `python -m app.bootstrap` (вызывается из entrypoint.sh после миграций). """ from __future__ import annotations from sqlmodel import Session, select from app.core.config import settings from app.core.security import hash_password, verify_password from app.db.session import engine from app.models import User from app.seed.reference_data import seed_reference_data def _ensure_admin(session: Session) -> None: if not settings.admin_bootstrap_enabled: return password = (settings.admin_password or "").strip() username = (settings.admin_username or "admin").strip() existing = session.exec(select(User).where(User.role == "admin")).first() if existing is None: if not password: raise RuntimeError("Отказ создавать администратора без пароля (ADMIN_PASSWORD пуст).") # Логин администратора = его никнейм (отдельной таблицы админов нет). session.add( User( nickname=username, role="admin", auth_provider="local", password_hash=hash_password(password), ) ) session.commit() print(f"[bootstrap] Создан администратор: {username}") return # Администратор уже существует. if not settings.is_development: # В prod пароль НЕ перезаписываем (мог быть изменён через панель). return # DEV: подтягиваем логин/пароль из .env (env — источник истины в деве). changed: list[str] = [] if password and not verify_password(password, existing.password_hash or ""): existing.password_hash = hash_password(password) changed.append("пароль") if username and existing.nickname != username: existing.nickname = username changed.append("логин") if changed: session.add(existing) session.commit() print(f"[bootstrap] DEV: администратор обновлён из .env ({', '.join(changed)})") def bootstrap() -> None: with Session(engine) as session: seed_reference_data(session) # идемпотентно _ensure_admin(session) if __name__ == "__main__": bootstrap()