diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..00ad6d3 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,36 @@ +# Minimal compose for local dev — just the data stores +# Usage: docker compose -f docker-compose.dev.yml up -d + +version: "3.9" + +services: + postgres: + image: pgvector/pgvector:pg16 + environment: + - POSTGRES_USER=fracta + - POSTGRES_PASSWORD=devpass + - POSTGRES_DB=fractafrag + volumes: + - pgdata:/var/lib/postgresql/data + - ./db/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U fracta -d fractafrag"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + command: redis-server --appendonly yes + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + pgdata: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 38c45ae..3393857 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,38 +1,36 @@ # docker-compose.override.yml — Local dev overrides -# This file is automatically picked up by docker compose - -version: "3.9" +# Automatically picked up by `docker compose up` services: api: volumes: - ./services/api:/app - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] ports: - - "8000:8000" # Direct access for debugging + - "8000:8000" frontend: volumes: - ./services/frontend:/app - /app/node_modules - command: npm run dev -- --host 0.0.0.0 + command: ["npx", "vite", "--host", "0.0.0.0"] ports: - - "5173:5173" # Vite dev server direct access + - "5173:5173" mcp: volumes: - ./services/mcp:/app ports: - - "3200:3200" # Direct MCP access + - "3200:3200" renderer: ports: - - "3100:3100" # Direct renderer access + - "3100:3100" postgres: ports: - - "5432:5432" # Direct DB access for dev tools + - "5432:5432" redis: ports: - - "6379:6379" # Direct Redis access for dev tools + - "6379:6379" diff --git a/docker-compose.yml b/docker-compose.yml index 0385a60..6811ac3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: # ─── Reverse Proxy ────────────────────────────────────────── @@ -7,10 +5,9 @@ services: image: nginx:alpine ports: - "80:80" - - "443:443" volumes: - ./services/nginx/conf:/etc/nginx/conf.d:ro - - ./services/nginx/certs:/etc/ssl/certs:ro + - renders:/renders:ro depends_on: api: condition: service_healthy @@ -25,7 +22,6 @@ services: dockerfile: Dockerfile environment: - VITE_API_URL=${VITE_API_URL:-http://localhost/api} - - VITE_MCP_URL=${VITE_MCP_URL:-http://localhost/mcp} restart: unless-stopped # ─── API (FastAPI) ────────────────────────────────────────── @@ -34,18 +30,18 @@ services: context: ./services/api dockerfile: Dockerfile environment: - - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag} - - DATABASE_URL_SYNC=postgresql://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag} + - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag} + - DATABASE_URL_SYNC=postgresql://${POSTGRES_USER:-fracta}:${DB_PASS:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag} - REDIS_URL=${REDIS_URL:-redis://redis:6379/0} - - JWT_SECRET=${JWT_SECRET} + - JWT_SECRET=${JWT_SECRET:-dev-secret-change-in-production} - JWT_ALGORITHM=${JWT_ALGORITHM:-HS256} - - JWT_ACCESS_TOKEN_EXPIRE_MINUTES=${JWT_ACCESS_TOKEN_EXPIRE_MINUTES:-15} + - JWT_ACCESS_TOKEN_EXPIRE_MINUTES=${JWT_ACCESS_TOKEN_EXPIRE_MINUTES:-60} - JWT_REFRESH_TOKEN_EXPIRE_DAYS=${JWT_REFRESH_TOKEN_EXPIRE_DAYS:-30} - - TURNSTILE_SECRET=${TURNSTILE_SECRET} - - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} - - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} + - TURNSTILE_SECRET=${TURNSTILE_SECRET:-} + - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-} - RENDERER_URL=http://renderer:3100 - - BYOK_MASTER_KEY=${BYOK_MASTER_KEY} + - BYOK_MASTER_KEY=${BYOK_MASTER_KEY:-dev-byok-key} depends_on: postgres: condition: service_healthy @@ -56,6 +52,7 @@ services: interval: 10s timeout: 5s retries: 5 + start_period: 15s restart: unless-stopped # ─── MCP Server ───────────────────────────────────────────── @@ -65,7 +62,7 @@ services: dockerfile: Dockerfile environment: - API_BASE_URL=http://api:8000 - - MCP_API_KEY_SALT=${MCP_API_KEY_SALT} + - MCP_API_KEY_SALT=${MCP_API_KEY_SALT:-dev-salt} - REDIS_URL=${REDIS_URL:-redis://redis:6379/0} depends_on: api: @@ -80,7 +77,7 @@ services: shm_size: "512mb" environment: - MAX_RENDER_DURATION=${MAX_RENDER_DURATION:-8} - - OUTPUT_DIR=${RENDER_OUTPUT_DIR:-/renders} + - OUTPUT_DIR=/renders volumes: - renders:/renders restart: unless-stopped @@ -90,22 +87,20 @@ services: build: context: ./services/api dockerfile: Dockerfile - command: celery -A app.worker.celery_app worker --loglevel=info --concurrency=4 + command: ["python", "-m", "celery", "-A", "app.worker", "worker", "--loglevel=info", "--concurrency=2"] environment: - - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag} - - DATABASE_URL_SYNC=postgresql://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag} + - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag} + - DATABASE_URL_SYNC=postgresql://${POSTGRES_USER:-fracta}:${DB_PASS:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag} - REDIS_URL=${REDIS_URL:-redis://redis:6379/0} - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - - OPENAI_API_KEY=${OPENAI_API_KEY} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} - RENDERER_URL=http://renderer:3100 - - BYOK_MASTER_KEY=${BYOK_MASTER_KEY} + - BYOK_MASTER_KEY=${BYOK_MASTER_KEY:-dev-byok-key} depends_on: postgres: condition: service_healthy redis: condition: service_healthy - renderer: - condition: service_started restart: unless-stopped # ─── PostgreSQL + pgvector ────────────────────────────────── @@ -113,7 +108,7 @@ services: image: pgvector/pgvector:pg16 environment: - POSTGRES_USER=${POSTGRES_USER:-fracta} - - POSTGRES_PASSWORD=${DB_PASS} + - POSTGRES_PASSWORD=${DB_PASS:-devpass} - POSTGRES_DB=${POSTGRES_DB:-fractafrag} volumes: - pgdata:/var/lib/postgresql/data diff --git a/services/api/Dockerfile b/services/api/Dockerfile index b1fd160..f98d8e9 100644 --- a/services/api/Dockerfile +++ b/services/api/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.12-slim WORKDIR /app -# Install system deps +# Install system deps (curl for healthcheck) RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ build-essential \ @@ -10,12 +10,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # Install Python deps COPY pyproject.toml . -RUN pip install --no-cache-dir -e ".[dev]" +RUN pip install --no-cache-dir ".[dev]" # Copy app code COPY . . +EXPOSE 8000 + # Default command (overridden in dev by docker-compose.override.yml) CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] - -EXPOSE 8000 diff --git a/services/api/app/middleware/auth.py b/services/api/app/middleware/auth.py index 285171b..870e784 100644 --- a/services/api/app/middleware/auth.py +++ b/services/api/app/middleware/auth.py @@ -7,7 +7,7 @@ from typing import Optional from fastapi import Depends, HTTPException, status, Request, Response from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jose import jwt, JWTError -from passlib.context import CryptContext +import bcrypt from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select @@ -17,18 +17,17 @@ from app.models import User from app.redis import get_redis settings = get_settings() -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") bearer_scheme = HTTPBearer(auto_error=False) # ── Password Hashing ────────────────────────────────────── def hash_password(password: str) -> str: - return pwd_context.hash(password) + return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=12)).decode("utf-8") def verify_password(plain: str, hashed: str) -> bool: - return pwd_context.verify(plain, hashed) + return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8")) # ── JWT Token Management ────────────────────────────────── diff --git a/services/api/app/models/models.py b/services/api/app/models/models.py index fea3869..5f3b3f0 100644 --- a/services/api/app/models/models.py +++ b/services/api/app/models/models.py @@ -97,7 +97,7 @@ class EngagementEvent(Base): shader_id = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="CASCADE"), nullable=False) event_type = Column(String, nullable=False) dwell_secs = Column(Float, nullable=True) - metadata = Column(JSONB, nullable=True) + event_metadata = Column("metadata", JSONB, nullable=True) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) diff --git a/services/api/app/routers/feed.py b/services/api/app/routers/feed.py index f480ffd..4f20e9e 100644 --- a/services/api/app/routers/feed.py +++ b/services/api/app/routers/feed.py @@ -80,7 +80,7 @@ async def report_dwell( shader_id=body.shader_id, event_type="dwell", dwell_secs=body.dwell_secs, - metadata={"replayed": body.replayed}, + event_metadata={"replayed": body.replayed}, ) db.add(event) # TODO: Update user taste vector (Track F) diff --git a/services/api/app/routers/mcp_keys.py b/services/api/app/routers/mcp_keys.py index 4dca0dc..9b3bfd8 100644 --- a/services/api/app/routers/mcp_keys.py +++ b/services/api/app/routers/mcp_keys.py @@ -5,7 +5,7 @@ from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select -from passlib.context import CryptContext +import bcrypt from app.database import get_db from app.models import User, ApiKey @@ -13,18 +13,16 @@ from app.schemas import ApiKeyCreate, ApiKeyPublic, ApiKeyCreated from app.middleware.auth import get_current_user, require_tier router = APIRouter() -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def generate_api_key() -> tuple[str, str, str]: """Generate an API key. Returns (full_key, prefix, hash).""" raw = secrets.token_bytes(32) - # base58-like encoding using alphanumeric chars import base64 encoded = base64.b32encode(raw).decode().rstrip("=").lower() full_key = f"ff_key_{encoded}" - prefix = full_key[:16] # ff_key_ + 8 chars - key_hash = pwd_context.hash(full_key) + prefix = full_key[:16] + key_hash = bcrypt.hashpw(full_key.encode("utf-8"), bcrypt.gensalt(rounds=10)).decode("utf-8") return full_key, prefix, key_hash diff --git a/services/api/pyproject.toml b/services/api/pyproject.toml index 3c7f562..7c0b129 100644 --- a/services/api/pyproject.toml +++ b/services/api/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "pgvector>=0.3.6", "redis>=5.2.0", "celery[redis]>=5.4.0", - "passlib[bcrypt]>=1.7.4", + "bcrypt>=4.2.0", "python-jose[cryptography]>=3.3.0", "cryptography>=43.0.0", "httpx>=0.28.0", diff --git a/services/frontend/Dockerfile b/services/frontend/Dockerfile index ff055db..d21aa39 100644 --- a/services/frontend/Dockerfile +++ b/services/frontend/Dockerfile @@ -2,16 +2,13 @@ FROM node:20-alpine WORKDIR /app -COPY package*.json ./ -RUN npm ci +COPY package.json ./ +RUN npm install COPY . . -# Build for production (overridden in dev) -RUN npm run build - -# Serve with a simple static server -RUN npm install -g serve -CMD ["serve", "-s", "dist", "-l", "5173"] - EXPOSE 5173 + +# In production: build and serve static files +# In dev: overridden to `npx vite --host 0.0.0.0` +CMD ["sh", "-c", "npm run build && npx serve -s dist -l 5173"] diff --git a/services/frontend/package.json b/services/frontend/package.json index 2352c82..662ec78 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "vite build", "preview": "vite preview" }, "dependencies": { diff --git a/services/renderer/Dockerfile b/services/renderer/Dockerfile index b046deb..2457756 100644 --- a/services/renderer/Dockerfile +++ b/services/renderer/Dockerfile @@ -15,7 +15,7 @@ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true COPY package*.json ./ -RUN npm ci +RUN npm install COPY . .