From c971bafa6dbe50787cd2a014ef489ac55f762034 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:35:30 +0000 Subject: [PATCH] fix: resolve all production deployment blockers and security gaps Co-authored-by: cliff-de-tech <137389025+cliff-de-tech@users.noreply.github.com> Agent-Logs-Url: https://github.com/cliff-de-tech/Post-Bot/sessions/273440a2-8d23-4c5d-882a-cd3a8d7d8eb5 --- backend/Dockerfile | 4 ++-- backend/alembic.ini | 4 +++- backend/routes/posts.py | 17 ++++++++--------- render.yaml | 4 +++- services/ai_service.py | 2 +- services/encryption.py | 3 ++- web/next.config.js | 4 ++++ 7 files changed, 23 insertions(+), 15 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 38874d1..b1d0e33 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -23,7 +23,7 @@ USER appuser EXPOSE 8000 # Production Command: Run Alembic migrations, then start Gunicorn with Uvicorn workers -# -w 4: Use 4 worker processes (adjust based on CPU cores) +# -w ${GUNICORN_WORKERS:-2}: Worker count (default 2; set GUNICORN_WORKERS env var to tune) # -k uvicorn.workers.UvicornWorker: Use async workers # --bind 0.0.0.0:8000: Listen on all interfaces -CMD ["sh", "-c", "cd /app/backend && alembic upgrade head && cd /app && gunicorn -w 4 -k uvicorn.workers.UvicornWorker backend.app:app --bind 0.0.0.0:8000"] +CMD ["sh", "-c", "cd /app/backend && alembic upgrade head && cd /app && gunicorn -w ${GUNICORN_WORKERS:-2} -k uvicorn.workers.UvicornWorker backend.app:app --bind 0.0.0.0:8000"] diff --git a/backend/alembic.ini b/backend/alembic.ini index cd6492f..0c5002d 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -84,7 +84,9 @@ path_separator = os # database URL. This is consumed by the user-maintained env.py script only. # other means of configuring database URLs may be customized within the env.py # file. -sqlalchemy.url = driver://user:pass@localhost/dbname +# database URL is set at runtime by migrations/env.py from the DATABASE_URL env var. +# Do NOT set it here; the value below is intentionally blank. +sqlalchemy.url = [post_write_hooks] diff --git a/backend/routes/posts.py b/backend/routes/posts.py index f66a8b5..cc72fe2 100644 --- a/backend/routes/posts.py +++ b/backend/routes/posts.py @@ -105,12 +105,15 @@ publish_limiter = None try: - from middleware.clerk_auth import get_current_user + from middleware.clerk_auth import get_current_user, require_auth except ImportError: logger.error("clerk_auth_import_failed", detail="Authentication middleware unavailable - all authenticated endpoints will reject requests") async def get_current_user(): """Fallback that rejects all requests when auth middleware is unavailable.""" raise HTTPException(status_code=503, detail="Authentication service unavailable") + async def require_auth(): + """Fallback that rejects all requests when auth middleware is unavailable.""" + raise HTTPException(status_code=503, detail="Authentication service unavailable") from services.db import get_database from repositories.posts import PostRepository @@ -157,11 +160,11 @@ class BatchGenerateRequest(BaseModel): @router.post("/generate-preview") async def generate_preview( req: GenerateRequest, - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(require_auth) ): """Generate an AI post preview from context. - Rate limited to 10 requests per hour per user to prevent abuse. + Requires authentication. Rate limited to 10 requests per hour per user to prevent abuse. Supports multiple AI providers with tier enforcement: - Free tier: Always routes to Groq (fast, free) @@ -172,12 +175,8 @@ async def generate_preview( if not generate_post_with_ai: raise HTTPException(status_code=503, detail="AI service not available") - # Use authenticated user_id if available, otherwise fall back to request body - user_id = None - if current_user and current_user.get("user_id"): - user_id = current_user["user_id"] - elif req.user_id: - user_id = req.user_id + # Use the authenticated user's ID only — req.user_id is ignored to prevent impersonation + user_id = current_user["user_id"] # Rate limiting check (10 requests/hour for AI generation) if RATE_LIMITING_ENABLED and post_generation_limiter and user_id: diff --git a/render.yaml b/render.yaml index 28ee8d4..ccb554c 100644 --- a/render.yaml +++ b/render.yaml @@ -216,9 +216,11 @@ services: - key: NEXT_PUBLIC_API_URL sync: false - # Clerk Authentication (public key) + # Clerk Authentication - key: NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY sync: false + - key: CLERK_SECRET_KEY + sync: false # OAuth Redirect URI - key: NEXT_PUBLIC_REDIRECT_URI diff --git a/services/ai_service.py b/services/ai_service.py index c4b152a..62ede6e 100644 --- a/services/ai_service.py +++ b/services/ai_service.py @@ -70,7 +70,7 @@ OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '') ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY', '') MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY', '') -GITHUB_USERNAME = os.getenv('GITHUB_USERNAME', 'cliff-de-tech') +GITHUB_USERNAME = os.getenv('GITHUB_USERNAME', '') # Model configurations GROQ_MODEL = "llama-3.3-70b-versatile" diff --git a/services/encryption.py b/services/encryption.py index 83826bb..aa22b97 100644 --- a/services/encryption.py +++ b/services/encryption.py @@ -51,7 +51,8 @@ # SECURITY: Determine environment mode # - "production" = strict mode, encryption required # - "development" or unset = permissive mode for local dev -ENV = os.getenv('ENV', 'development').lower() +# Check both ENV and ENVIRONMENT (render.yaml/docker-compose set ENVIRONMENT=production) +ENV = os.getenv('ENV', os.getenv('ENVIRONMENT', 'development')).lower() IS_PRODUCTION = ENV == 'production' # Load encryption key from environment diff --git a/web/next.config.js b/web/next.config.js index 133b2c1..11771f1 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -2,6 +2,9 @@ const nextConfig = { reactStrictMode: true, + // Required for Docker standalone deployment (copies server.js + minimal deps) + output: 'standalone', + // Enable gzip compression compress: true, @@ -42,6 +45,7 @@ const nextConfig = { { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, + { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, { key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.clerk.io https://*.clerk.accounts.dev; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://images.unsplash.com https://media.licdn.com https://img.clerk.com; connect-src 'self' https://*.clerk.accounts.dev https://api.clerk.io " + (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000') + "; font-src 'self'; frame-src 'self' https://*.clerk.accounts.dev;"