Initial commit: backend scaffold, auth, frontend login

- FastAPI + SQLAlchemy async + Alembic + Postgres backend
- Auth: JWT access + rotating refresh tokens, argon2, roles, audit log
- React 19 + Vite frontend: login page, protected route, auth context
- Docker Compose: postgres, redis, migrate, api, worker (placeholder), frontend/nginx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 15:07:15 +03:00
co-authored by Claude Sonnet 5
commit 2664cb8213
71 changed files with 4999 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
"""Служебные команды.
python -m app.cli bootstrap # создать первого администратора
python -m app.cli gen-keys # сгенерировать SECRET_KEY и ENCRYPTION_KEY
"""
from __future__ import annotations
import argparse
import asyncio
import secrets
import sys
from sqlalchemy import select
from app.core.config import settings
from app.core.security import hash_password
from app.db.models.user import User, UserRole
from app.db.session import session_scope
async def bootstrap() -> int:
"""Создаёт первого администратора из переменных окружения.
Идемпотентна: повторный запуск ничего не меняет и пароль не сбрасывает.
"""
if not settings.first_admin_password:
print("FIRST_ADMIN_PASSWORD не задан в .env", file=sys.stderr)
return 1
email = settings.first_admin_email.strip().lower()
async with session_scope() as session:
existing = await session.scalar(select(User).where(User.email == email))
if existing is not None:
print(f"Пользователь {email} уже существует — ничего не изменено.")
return 0
session.add(
User(
email=email,
full_name=settings.first_admin_name,
password_hash=hash_password(settings.first_admin_password),
role=UserRole.ADMIN,
)
)
print(f"Администратор {email} создан.")
print("Смените пароль после первого входа и уберите FIRST_ADMIN_PASSWORD из .env.")
return 0
def gen_keys() -> int:
from cryptography.fernet import Fernet
print(f"SECRET_KEY={secrets.token_urlsafe(64)}")
print(f"ENCRYPTION_KEY={Fernet.generate_key().decode()}")
return 0
def main() -> int:
parser = argparse.ArgumentParser(prog="app.cli", description="Служебные команды lux_fiscal")
parser.add_argument("command", choices=["bootstrap", "gen-keys"])
args = parser.parse_args()
if args.command == "bootstrap":
return asyncio.run(bootstrap())
return gen_keys()
if __name__ == "__main__":
raise SystemExit(main())