refactoring

This commit is contained in:
YehorI 2025-09-28 12:36:25 +03:00
parent 4215c800ee
commit 5a7d7859fb
13 changed files with 253 additions and 158 deletions

131
Makefile Normal file
View File

@ -0,0 +1,131 @@
.PHONY: help build migrate migrate-create start dev stop logs clean test lint format check-db reset-db shell
# Default target
help: ## Show this help message
@echo "Available commands:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
# Docker build commands
build: ## Build Docker containers
docker compose build
build-no-cache: ## Build Docker containers without cache
docker compose build --no-cache
# Database commands
migrate: build ## Run database migrations in Docker
docker compose run --rm backend python -m app.db.init
migrate-create: ## Create a new migration (use NAME=migration_name)
@if [ -z "$(NAME)" ]; then \
echo "Usage: make migrate-create NAME=migration_name"; \
exit 1; \
fi
docker compose run --rm backend alembic revision --autogenerate -m "$(NAME)"
migrate-upgrade: ## Upgrade to specific revision (use REV=revision_id)
@if [ -z "$(REV)" ]; then \
echo "Usage: make migrate-upgrade REV=revision_id"; \
exit 1; \
fi
docker compose run --rm backend alembic upgrade "$(REV)"
migrate-downgrade: ## Downgrade to specific revision (use REV=revision_id)
@if [ -z "$(REV)" ]; then \
echo "Usage: make migrate-downgrade REV=revision_id"; \
exit 1; \
fi
docker compose run --rm backend alembic downgrade "$(REV)"
migrate-history: ## Show migration history
docker compose run --rm backend alembic history
migrate-current: ## Show current migration revision
docker compose run --rm backend alembic current
check-db: ## Check database connection and status
docker compose run --rm backend python -c "from app.db.init import check_database_connection; print('✅ Database OK' if check_database_connection() else '❌ Database connection failed')"
reset-db: ## Reset database (remove and recreate)
@echo "⚠️ This will remove all data! Are you sure? (y/N): "; \
read confirm; \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
docker compose down -v; \
echo "🗑️ Database reset. Run 'make migrate' to recreate."; \
else \
echo "❌ Database reset cancelled."; \
fi
# Development commands
start: migrate ## Run migrations and start all services
docker compose up -d
dev: migrate ## Run migrations and start services with logs
docker compose up
start-no-migrate: build ## Start services without running migrations
docker compose up -d
dev-no-migrate: build ## Start services with logs without migrations
docker compose up
stop: ## Stop all Docker services
docker compose down
restart: stop start ## Restart all services with migrations
# Utility commands
logs: ## Show Docker container logs
docker compose logs -f
logs-backend: ## Show backend container logs only
docker compose logs -f backend
logs-frontend: ## Show frontend container logs only
docker compose logs -f frontend
shell: ## Open shell in backend Docker container
docker compose exec backend /bin/bash
shell-run: ## Run a new backend container with shell access
docker compose run --rm backend /bin/bash
# Testing and quality (run in Docker)
test: ## Run tests in Docker
docker compose run --rm backend python -m pytest
test-verbose: ## Run tests with verbose output in Docker
docker compose run --rm backend python -m pytest -v
lint: ## Run linting in Docker
docker compose run --rm backend python -m ruff check .
format: ## Format code in Docker
docker compose run --rm backend python -m ruff format .
# Cleanup commands
clean: ## Clean up Docker containers and images
docker compose down --rmi local --volumes --remove-orphans
clean-all: ## Clean up all Docker resources (containers, images, volumes)
docker compose down --rmi all --volumes --remove-orphans
docker system prune -f
# Development workflow commands
setup: build migrate ## Initial setup: build containers and run migrations
@echo "✅ Setup complete! Run 'make dev' to start the development server."
deploy-check: build lint test migrate ## Check if ready for deployment
@echo "✅ Deployment checks passed!"
status: ## Show status of Docker services
docker compose ps
# Production commands
prod-start: migrate ## Start services in production mode (detached)
docker compose up -d
prod-stop: ## Stop production services
docker compose down
prod-restart: prod-stop prod-start ## Restart production services

View File

@ -36,40 +36,6 @@
Docker Compose автоматически переопределяет `ALABUGA_SQLITE_PATH=/data/app.db`, чтобы база сохранялась во внешнем volume. Для локального запуска вне Docker оставьте путь `./data/app.db` из примера.
## Локальная разработка backend
```bash
cd backend
python -m venv .venv
source .venv/bin/activate
export PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1
pip install -r requirements-dev.txt
# подготовьте переменные окружения (однократно)
cp .env.example .env
# при необходимости включите в .env подтверждение почты
# ALABUGA_REQUIRE_EMAIL_CONFIRMATION=true
# база поднимется сама: при старте приложения автоматически выполняется Alembic upgrade head.
# Для ручной подготовки можно вызвать команду ниже — она прогоняет миграции и добавляет демо-данные.
cd ..
python -m scripts.seed_data
cd backend
# Запуск API
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
## Локальная разработка фронтенда
```bash
cd frontend
npm install
cp .env.example .env
npm run dev
```
## Пользовательские учётные записи (сидированные)
| Роль | Email | Пароль |

View File

@ -2,17 +2,19 @@ FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
POETRY_VIRTUALENVS_CREATE=false \
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential libpq-dev \
&& apt-get install -y --no-install-recommends build-essential libpq-dev curl \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Install uv
RUN pip install --no-cache-dir uv
COPY pyproject.toml ./
RUN uv pip install --system --no-cache -e .
COPY . /app

View File

@ -1,28 +0,0 @@
"""Add email confirmation fields to users"""
from __future__ import annotations
from datetime import datetime
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '20240927_0003'
down_revision = '20240611_0002'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column('users', sa.Column('is_email_confirmed', sa.Boolean(), nullable=False, server_default=sa.true()))
op.add_column('users', sa.Column('email_confirmation_token', sa.String(length=128), nullable=True))
op.add_column('users', sa.Column('email_confirmed_at', sa.DateTime(timezone=True), nullable=True))
op.execute('UPDATE users SET is_email_confirmed = 1')
op.alter_column('users', 'is_email_confirmed', server_default=None)
def downgrade() -> None:
op.drop_column('users', 'email_confirmed_at')
op.drop_column('users', 'email_confirmation_token')
op.drop_column('users', 'is_email_confirmed')

View File

@ -7,7 +7,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '20240927_0004'
down_revision = '20240927_0003'
down_revision = '20240611_0002'
branch_labels = None
depends_on = None

View File

@ -82,7 +82,7 @@ def register(user_in: UserRegister, db: Session = Depends(get_db)) -> Token | di
token = issue_confirmation_token(user, db)
return {
"detail": "Мы отправили письмо с подтверждением. Введите код, чтобы активировать аккаунт.",
"debug_token": token if settings.environment != 'production' else None,
"debug_token": token if settings.debug else None,
}
# 4. Если подтверждение выключено, сразу создаём JWT и возвращаем его фронтенду.
@ -110,7 +110,7 @@ def request_confirmation(payload: EmailRequest, db: Session = Depends(get_db)) -
token = issue_confirmation_token(user, db)
hint = None
if settings.environment != 'production':
if settings.debug:
hint = token
return {

View File

@ -18,7 +18,7 @@ class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=BASE_DIR / ".env", env_prefix="ALABUGA_", extra="ignore")
project_name: str = "Alabuga Gamification API"
environment: str = "local"
debug: bool = False
secret_key: str = "super-secret-key-change-me"
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 60 * 12

71
backend/app/db/init.py Normal file
View File

@ -0,0 +1,71 @@
"""Database initialization and migration utilities."""
from __future__ import annotations
import sys
from pathlib import Path
from alembic import command
from alembic.config import Config
from sqlalchemy import text
from app.core.config import settings
from app.db.session import engine
ALEMBIC_CONFIG = Path(__file__).resolve().parents[2] / "alembic.ini"
def get_alembic_config() -> Config:
"""Get configured Alembic Config object."""
config = Config(str(ALEMBIC_CONFIG))
config.set_main_option("sqlalchemy.url", str(settings.database_url))
return config
def check_database_connection() -> bool:
"""Check if database connection is working."""
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
return True
except Exception as e:
print(f"Database connection failed: {e}")
return False
def run_migrations() -> bool:
"""Run database migrations to head."""
try:
config = get_alembic_config()
command.upgrade(config, "head")
print("✅ Database migrations completed successfully")
return True
except Exception as e:
print(f"❌ Migration failed: {e}")
return False
def init_database() -> bool:
"""Initialize database: check connection and run migrations."""
print("🔄 Initializing database...")
if not check_database_connection():
print("❌ Cannot connect to database")
return False
if not run_migrations():
print("❌ Failed to run migrations")
return False
print("✅ Database initialization completed successfully")
return True
def main() -> None:
"""CLI entry point for database initialization."""
success = init_database()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View File

@ -2,20 +2,13 @@
from __future__ import annotations
from pathlib import Path
from alembic import command
from alembic.config import Config
from alembic.script import ScriptDirectory
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import inspect, text
from app.api.routes import admin, auth, journal, missions, onboarding, store, users
from app.core.config import settings
from app.db.session import engine
ALEMBIC_CONFIG = Path(__file__).resolve().parents[1] / "alembic.ini"
# Import all models to ensure they're registered with Base.metadata
from app import models # This imports all models through the __init__.py
app = FastAPI(title=settings.project_name)
@ -29,51 +22,6 @@ app.add_middleware(
)
def run_migrations() -> None:
"""Гарантируем, что база обновлена до последней схемы Alembic."""
config = Config(str(ALEMBIC_CONFIG))
config.set_main_option("sqlalchemy.url", str(settings.database_url))
inspector = inspect(engine)
script = ScriptDirectory.from_config(config)
head_revision = script.get_current_head()
tables = inspector.get_table_names()
current_revision: str | None = None
if "alembic_version" in tables:
with engine.begin() as conn:
row = conn.execute(text("SELECT version_num FROM alembic_version LIMIT 1")).fetchone()
current_revision = row[0] if row else None
if "alembic_version" not in tables or current_revision is None:
if tables:
# База создана через Base.metadata.create_all. Добавляем отсутствующие поля вручную
# и фиксируем версию как актуальную, чтобы последующие миграции применялись штатно.
user_columns = {col["name"] for col in inspector.get_columns("users")}
with engine.begin() as conn:
if "preferred_branch" not in user_columns:
conn.execute(text("ALTER TABLE users ADD COLUMN preferred_branch VARCHAR(160)"))
if "motivation" not in user_columns:
conn.execute(text("ALTER TABLE users ADD COLUMN motivation TEXT"))
conn.execute(text("CREATE TABLE IF NOT EXISTS alembic_version (version_num VARCHAR(32) NOT NULL)"))
conn.execute(text("DELETE FROM alembic_version"))
conn.execute(text("INSERT INTO alembic_version (version_num) VALUES (:rev)"), {"rev": head_revision})
return
# Таблиц ещё нет — создадим их миграциями.
command.upgrade(config, "head")
return
command.upgrade(config, "head")
@app.on_event("startup")
def on_startup() -> None:
"""Прогоняем миграции перед обработкой запросов."""
run_migrations()
app.include_router(auth.router)
app.include_router(users.router)
app.include_router(missions.router)
@ -87,4 +35,4 @@ app.include_router(admin.router)
def healthcheck() -> dict[str, str]:
"""Простой ответ для Docker healthcheck."""
return {"status": "ok", "environment": settings.environment}
return {"status": "ok"}

View File

@ -3,7 +3,33 @@ name = "alabuga-backend"
version = "0.1.0"
description = "Геймифицированный модуль платформы 'Алабуга'"
requires-python = ">=3.13"
dependencies = []
dependencies = [
"fastapi==0.111.0",
"uvicorn[standard]==0.30.1",
"SQLAlchemy>=2.0.36,<3",
"alembic>=1.14.0,<2",
"pydantic==2.9.2",
"pydantic-settings==2.10.1",
"passlib[bcrypt]==1.7.4",
"python-jose[cryptography]==3.3.0",
"python-multipart==0.0.9",
"bcrypt==4.1.3",
"email-validator==2.1.1",
"fastapi-pagination==0.12.24",
"Jinja2==3.1.4"
]
[project.optional-dependencies]
dev = [
"pytest==8.2.2",
"pytest-asyncio==0.23.7",
"httpx==0.27.0",
"coverage==7.5.3",
"black==24.4.2",
"isort==5.13.2",
"ruff==0.4.7",
"mypy==1.10.0"
]
[tool.black]
line-length = 100

View File

@ -1,9 +0,0 @@
-r requirements.txt
pytest==8.2.2
pytest-asyncio==0.23.7
httpx==0.27.0
coverage==7.5.3
black==24.4.2
isort==5.13.2
ruff==0.4.7
mypy==1.10.0

View File

@ -1,13 +0,0 @@
fastapi==0.111.0
uvicorn[standard]==0.30.1
SQLAlchemy>=2.0.36,<3
alembic>=1.14.0,<2
pydantic==2.9.2
pydantic-settings==2.10.1
passlib[bcrypt]==1.7.4
python-jose[cryptography]==3.3.0
python-multipart==0.0.9
bcrypt==4.1.3
email-validator==2.1.1
fastapi-pagination==0.12.24
Jinja2==3.1.4

View File

@ -1,5 +1,3 @@
version: '3.9'
services:
backend:
build:
@ -11,10 +9,10 @@ services:
- backend-data:/data
- ./backend:/app
env_file:
- backend/.env.example
environment:
ALABUGA_ENVIRONMENT: docker
- backend/.env
depends_on: []
networks:
- app-network
frontend:
build:
@ -22,18 +20,21 @@ services:
command: npm run dev -- --hostname 0.0.0.0 --port 3000
ports:
- '3000:3000'
env_file:
- frontend/.env
environment:
NEXT_PUBLIC_API_URL: http://localhost:8000
NEXT_INTERNAL_API_URL: http://backend:8000
NEXT_PUBLIC_DEMO_EMAIL: candidate@alabuga.space
NEXT_PUBLIC_DEMO_PASSWORD: orbita123
NEXT_PUBLIC_DEMO_HR_EMAIL: hr@alabuga.space
NEXT_PUBLIC_DEMO_HR_PASSWORD: orbita123
volumes:
- ./frontend:/app
- /app/node_modules
depends_on:
- backend
networks:
- app-network
volumes:
backend-data:
networks:
app-network:
driver: bridge