Skip to content

fix: production deployment blockers and security gaps#23

Draft
Copilot wants to merge 1 commit intomainfrom
copilot/push-to-production
Draft

fix: production deployment blockers and security gaps#23
Copilot wants to merge 1 commit intomainfrom
copilot/push-to-production

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 24, 2026

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 — Added output: 'standalone'. The Dockerfile copies .next/standalone; without this key next build never creates it and the image build fails.
  • render.yaml — Added CLERK_SECRET_KEY to postbot-web. Required for clerkMiddleware to verify JWTs server-side; missing key breaks all protected routes (/dashboard, /settings, /onboarding).
  • backend/routes/posts.py — Switched /api/post/generate-preview from Depends(get_current_user) (optional) to Depends(require_auth) (enforced). Anonymous callers had user_id = None, silently bypassing the if ... and user_id rate-limit guard — unlimited free AI generation with no token. Also removed the elif req.user_id fallback that would have allowed caller-supplied user_id to impersonate another user's quota/settings.

🔴 Security

  • services/encryption.pyIS_PRODUCTION was always False on Render because the app sets ENVIRONMENT=production, not ENV. The fail-fast on missing ENCRYPTION_KEY was unreachable.
    # Before
    ENV = os.getenv('ENV', 'development').lower()
    # After
    ENV = os.getenv('ENV', os.getenv('ENVIRONMENT', 'development')).lower()

🟡 Reliability / Config

  • services/ai_service.py — Removed 'cliff-de-tech' as the hardcoded fallback for GITHUB_USERNAME. Missing env var now produces '' (visible in logs) instead of silently querying the wrong account.
  • web/next.config.js — Added Strict-Transport-Security: max-age=63072000; includeSubDomains; preload to 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 placeholder sqlalchemy.url = ******localhost/dbname with a blank value; migrations/env.py sets it at runtime from DATABASE_URL.

@cliff-de-tech
Copy link
Copy Markdown
Owner

There are conflicts

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-body user_id fallback, 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. However require_auth currently sets user_id via payload.get("sub"), which can be None if the claim is missing. In that case this route would still proceed (and also skip rate limiting because of the and user_id guard), despite claiming "Requires authentication". Add an explicit check that user_id is 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', '')
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
GITHUB_USERNAME = os.getenv('GITHUB_USERNAME', '')

Copilot uses AI. Check for mistakes.
@@ -172,12 +175,8 @@ async def generate_preview(
if not generate_post_with_ai:
raise HTTPException(status_code=503, detail="AI service not available")

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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")

Copilot uses AI. Check for mistakes.
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"]
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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",
)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants