feat: завершить проект — добавить недостающие файлы, тесты и CI/CD

- backend/app/schemas/auth.py, ai_match.py, escrow.py (схемы)
- frontend/components/ui/button.tsx (UI компонент)
- email-validator в requirements.txt
- frontend/tsconfig.json, tailwind.config.js, postcss.config.js
- frontend: login page, projects/[id] page, ai-match page
- Dockerfile для backend и frontend
- docker-compose.yml с app-контейнерами и healthcheck
- .env.example с полными переменными окружения
- backend/tests/ — pytest тесты (conftest + test_health)
- .drone.yml — CI/CD пайплайн для Drone CI
- README.md — полный гайд по деплою
This commit is contained in:
2026-07-03 13:28:19 +00:00
parent ec2e9bf508
commit 4ccf4b7184
20 changed files with 693 additions and 46 deletions
+10 -1
View File
@@ -1,24 +1,33 @@
# Database
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/freelancer_match
# Redis
REDIS_URL=redis://localhost:6379/0
# JWT
SECRET_KEY=your-secret-key-change-in-production
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=7
# OpenAI (для AI-матчинга)
OPENAI_API_KEY=sk-...
EMBEDDING_MODEL=text-embedding-3-small
# OAuth
GOOGLE_CLIENT_ID=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
# Stripe (Escrow)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=...
SMTP_USER=noreply@freelancermatch.com
SMTP_PASSWORD=...
EMAIL_FROM=noreply@freelancermatch.com
# CORS
ALLOWED_ORIGINS=["http://localhost:3000","https://freelancermatch.com"]
+12
View File
@@ -0,0 +1,12 @@
FROM python:3.12-slim AS base
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+17
View File
@@ -0,0 +1,17 @@
"""Схемы AI-матчинга."""
from pydantic import BaseModel, Field
class AIMatchRequest(BaseModel):
project_id: str = Field(..., description="ID проекта")
limit: int = Field(default=10, ge=1, le=50)
min_score: float | None = Field(default=None, ge=0.0, le=1.0)
class AIMatchResponse(BaseModel):
freelancer_id: str
name: str
skills_matched: list[str]
match_score: float
reasons: list[str]
+8
View File
@@ -0,0 +1,8 @@
"""Схемы авторизации."""
from pydantic import BaseModel
class TokenPair(BaseModel):
access_token: str
refresh_token: str
+14
View File
@@ -0,0 +1,14 @@
"""Схемы escrow-транзакций."""
from pydantic import BaseModel, Field
class EscrowCreate(BaseModel):
project_id: str = Field(..., description="ID проекта")
client_id: str = Field(..., description="ID клиента")
freelancer_id: str = Field(..., description="ID фрилансера")
amount: float = Field(..., gt=0, description="Сумма в рублях")
class EscrowRelease(BaseModel):
transaction_id: str = Field(..., description="ID транзакции")
+1
View File
@@ -4,6 +4,7 @@ sqlalchemy[asyncio]==2.0.37
alembic==1.14.1
asyncpg==0.30.0
pydantic-settings==2.7.1
pydantic[email]==2.9.2
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
openai==1.58.1
+1
View File
@@ -0,0 +1 @@
# Tests for freelancer-match backend
+83
View File
@@ -0,0 +1,83 @@
"""Конфигурация тестов."""
import asyncio
from typing import AsyncGenerator
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.core.database import Base
# Создаём тестовую БД в памяти (SQLite)
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
engine = create_async_engine(TEST_DATABASE_URL, echo=False)
async_session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@pytest.fixture(scope="session")
def event_loop():
"""Создаём event loop для тестов."""
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest_asyncio.fixture(autouse=True)
async def setup_db():
"""Создаём таблицы перед каждым тестом и удаляем после."""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest_asyncio.fixture
async def db_session() -> AsyncGenerator[AsyncSession, None]:
"""Тестовая сессия БД."""
async with async_session_factory() as session:
yield session
@pytest_asyncio.fixture
async def client(db_session) -> AsyncGenerator[AsyncClient, None]:
"""HTTP клиент для тестирования API."""
from app.main import app
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
@pytest_asyncio.fixture
async def test_user(db_session):
"""Создаём тестового пользователя."""
from app.models.user import User
user = User(email="test@example.com", password_hash="$2b$12$LJ3m4ys6LJ3m4ys6LJ3m4e", role="freelancer")
db_session.add(user)
await db_session.commit()
return user
@pytest_asyncio.fixture
async def test_project(db_session, test_user):
"""Создаём тестовый проект."""
from app.models.project import Project
project = Project(
client_id=test_user.id,
title="Тестовый проект",
description="Описание проекта для тестирования",
category="web-development",
required_skills=["python", "fastapi"],
budget_min=1000.0,
budget_max=5000.0,
)
db_session.add(project)
await db_session.commit()
return project
+13
View File
@@ -0,0 +1,13 @@
"""Тесты для health endpoint."""
import pytest
@pytest.mark.asyncio
async def test_health(client):
"""Проверка что health endpoint возвращает ok."""
response = await client.get("/api/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["service"] == "freelancer-match"