From 5a7d7859fb43e4ac2840b0f7e248910dac8c5947 Mon Sep 17 00:00:00 2001 From: YehorI Date: Sun, 28 Sep 2025 12:36:25 +0300 Subject: [PATCH] refactoring --- Makefile | 131 ++++++++++++++++++ README.md | 34 ----- backend/Dockerfile | 10 +- .../20240927_0003_email_confirmation.py | 28 ---- .../20240927_0004_user_profile_details.py | 2 +- backend/app/api/routes/auth.py | 4 +- backend/app/core/config.py | 2 +- backend/app/db/init.py | 71 ++++++++++ backend/app/main.py | 58 +------- backend/pyproject.toml | 28 +++- backend/requirements-dev.txt | 9 -- backend/requirements.txt | 13 -- docker-compose.yaml | 21 +-- 13 files changed, 253 insertions(+), 158 deletions(-) create mode 100644 Makefile delete mode 100644 backend/alembic/versions/20240927_0003_email_confirmation.py create mode 100644 backend/app/db/init.py delete mode 100644 backend/requirements-dev.txt delete mode 100644 backend/requirements.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b6746f1 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 75e0d59..c4f20e6 100644 --- a/README.md +++ b/README.md @@ -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 | Пароль | diff --git a/backend/Dockerfile b/backend/Dockerfile index fcd6d80..ec3613b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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 diff --git a/backend/alembic/versions/20240927_0003_email_confirmation.py b/backend/alembic/versions/20240927_0003_email_confirmation.py deleted file mode 100644 index a2dc44b..0000000 --- a/backend/alembic/versions/20240927_0003_email_confirmation.py +++ /dev/null @@ -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') diff --git a/backend/alembic/versions/20240927_0004_user_profile_details.py b/backend/alembic/versions/20240927_0004_user_profile_details.py index 8d9e704..c139c03 100644 --- a/backend/alembic/versions/20240927_0004_user_profile_details.py +++ b/backend/alembic/versions/20240927_0004_user_profile_details.py @@ -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 diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index 512994c..6ef6bfc 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -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 { diff --git a/backend/app/core/config.py b/backend/app/core/config.py index d6ab6d0..050071d 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -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 diff --git a/backend/app/db/init.py b/backend/app/db/init.py new file mode 100644 index 0000000..cb0d0e1 --- /dev/null +++ b/backend/app/db/init.py @@ -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() diff --git a/backend/app/main.py b/backend/app/main.py index 711a903..22279b6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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"} diff --git a/backend/pyproject.toml b/backend/pyproject.toml index a229377..5392ccb 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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 diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt deleted file mode 100644 index 582d320..0000000 --- a/backend/requirements-dev.txt +++ /dev/null @@ -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 diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index a7fd47f..0000000 --- a/backend/requirements.txt +++ /dev/null @@ -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 diff --git a/docker-compose.yaml b/docker-compose.yaml index e9a735c..2d1a84b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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