fix: production deployment blockers and security gaps#23
fix: production deployment blockers and security gaps#23
Conversation
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
|
There are conflicts |
There was a problem hiding this comment.
Pull request overview
This PR addresses production deployment blockers and closes auth/security gaps across the Next.js frontend and FastAPI backend, with additional reliability/config hardening for Render deployments.
Changes:
- Enabled Next.js standalone output for Docker builds and tightened frontend security headers (HSTS).
- Fixed production-environment detection for encryption fail-fast behavior and adjusted AI service env defaults.
- Enforced authentication for
/api/post/generate-preview, removed request-bodyuser_idfallback, and updated Render/env/Docker/Alembic config for production readiness.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| web/next.config.js | Enables output: 'standalone' for Docker builds and adds HSTS to headers. |
| services/encryption.py | Detects production via ENVIRONMENT as well as ENV so encryption requirements apply on Render. |
| services/ai_service.py | Removes hardcoded fallback GitHub username (now defaults to empty). |
| render.yaml | Adds CLERK_SECRET_KEY to the frontend service env vars for server-side JWT verification. |
| backend/routes/posts.py | Requires auth for generate-preview and ensures user identity comes only from authenticated context. |
| backend/alembic.ini | Leaves sqlalchemy.url blank since migrations set it from DATABASE_URL at runtime. |
| backend/Dockerfile | Parameterizes Gunicorn worker count via GUNICORN_WORKERS with a lower default. |
Comments suppressed due to low confidence (1)
backend/routes/posts.py:183
user_id = current_user["user_id"]assumes the dependency always returns a non-empty subject. Howeverrequire_authcurrently setsuser_idviapayload.get("sub"), which can beNoneif the claim is missing. In that case this route would still proceed (and also skip rate limiting because of theand user_idguard), despite claiming "Requires authentication". Add an explicit check thatuser_idis present/non-empty and fail with 401 (or treat it as an invalid token) before continuing.
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:
allowed, rate_info = post_generation_limiter.is_allowed(user_id)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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', '') |
There was a problem hiding this comment.
GITHUB_USERNAME is now defaulted to an empty string, but this constant is not referenced anywhere else in ai_service.py (only defined). Keeping an unused config value here is confusing and can drift from the real source of truth (e.g., settings/env usage elsewhere). Consider removing it from this module entirely, or wiring it into the code path that actually needs it.
| GITHUB_USERNAME = os.getenv('GITHUB_USERNAME', '') |
| @@ -172,12 +175,8 @@ async def generate_preview( | |||
| if not generate_post_with_ai: | |||
| raise HTTPException(status_code=503, detail="AI service not available") | |||
|
|
|||
There was a problem hiding this comment.
This endpoint now ignores req.user_id to prevent impersonation, but the request schema (GenerateRequest) still accepts user_id and tests/clients may continue sending it. Silent ignoring can mask client bugs and makes the API contract ambiguous. Consider removing user_id from GenerateRequest for this endpoint, or explicitly rejecting requests that include user_id (or that include a value different from the authenticated user) with a 400/403.
| # Validate that any client-supplied user_id matches the authenticated user | |
| req_user_id = getattr(req, "user_id", None) | |
| if req_user_id is not None and req_user_id != current_user["user_id"]: | |
| logger.warning( | |
| "user_id_mismatch_in_request", | |
| requested_user_id=str(req_user_id), | |
| auth_user_id=str(current_user["user_id"]), | |
| ) | |
| raise HTTPException(status_code=403, detail="user_id in request does not match authenticated user") |
| 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"] |
There was a problem hiding this comment.
Authentication/impersonation behavior changed here (authenticated user ID is now enforced and request-body user_id is ignored), but the backend tests for /api/post/generate-preview still send user_id in the JSON and don’t assert that mismatched user_id is rejected/ignored. Add/adjust tests to cover: (1) providing a different user_id than the authenticated user results in 400/403 (or is rejected), and (2) request succeeds without requiring a body user_id.
| user_id = current_user["user_id"] | |
| user_id = current_user["user_id"] | |
| # If the request body includes a user_id, ensure it matches the authenticated user | |
| request_user_id = getattr(req, "user_id", None) | |
| if request_user_id is not None and request_user_id != user_id: | |
| logger.warning( | |
| "user_id_mismatch_in_generate_preview_request", | |
| authenticated_user_id=(user_id[:8] if isinstance(user_id, str) else user_id), | |
| request_user_id=(request_user_id[:8] if isinstance(request_user_id, str) else request_user_id), | |
| ) | |
| raise HTTPException( | |
| status_code=403, | |
| detail="user_id in request body does not match authenticated user", | |
| ) |
Eight production readiness issues — one hard Docker build failure, two auth/security gaps, and several reliability/config issues — none of which were previously addressed.
🔴 Blockers
web/next.config.js— Addedoutput: 'standalone'. The Dockerfile copies.next/standalone; without this keynext buildnever creates it and the image build fails.render.yaml— AddedCLERK_SECRET_KEYtopostbot-web. Required forclerkMiddlewareto verify JWTs server-side; missing key breaks all protected routes (/dashboard,/settings,/onboarding).backend/routes/posts.py— Switched/api/post/generate-previewfromDepends(get_current_user)(optional) toDepends(require_auth)(enforced). Anonymous callers haduser_id = None, silently bypassing theif ... and user_idrate-limit guard — unlimited free AI generation with no token. Also removed theelif req.user_idfallback that would have allowed caller-supplieduser_idto impersonate another user's quota/settings.🔴 Security
services/encryption.py—IS_PRODUCTIONwas alwaysFalseon Render because the app setsENVIRONMENT=production, notENV. The fail-fast on missingENCRYPTION_KEYwas unreachable.🟡 Reliability / Config
services/ai_service.py— Removed'cliff-de-tech'as the hardcoded fallback forGITHUB_USERNAME. Missing env var now produces''(visible in logs) instead of silently querying the wrong account.web/next.config.js— AddedStrict-Transport-Security: max-age=63072000; includeSubDomains; preloadto security headers block.backend/Dockerfile— Parameterized Gunicorn workers:-w ${GUNICORN_WORKERS:-2}. Default 4 workers on Render's free tier (512 MB / 1 vCPU) risks OOM.backend/alembic.ini— Replaced the placeholdersqlalchemy.url = ******localhost/dbnamewith a blank value;migrations/env.pysets it at runtime fromDATABASE_URL.