Skip to content

Prepare project for production deployment#2

Merged
Qomserver merged 1 commit intomainfrom
cursor/prepare-project-for-production-deployment-a673
Aug 17, 2025
Merged

Prepare project for production deployment#2
Qomserver merged 1 commit intomainfrom
cursor/prepare-project-for-production-deployment-a673

Conversation

@Qomserver
Copy link
Owner

@Qomserver Qomserver commented Aug 9, 2025

User description

Implement a production-ready FastAPI system with authentication, task management, database, Docker, and CI.


Open in Cursor Open in Web

PR Type

Enhancement


Description

  • Complete production-ready FastAPI application setup

  • JWT authentication with refresh tokens and user management

  • Task CRUD operations with ownership controls

  • Docker containerization with PostgreSQL database

  • CI/CD pipeline with GitHub Actions and pre-commit hooks


Diagram Walkthrough

flowchart LR
  A["FastAPI App"] --> B["JWT Auth System"]
  A --> C["Task Management"]
  A --> D["Database Layer"]
  B --> E["User Registration/Login"]
  C --> F["CRUD Operations"]
  D --> G["SQLAlchemy + Alembic"]
  A --> H["Docker Setup"]
  A --> I["CI/CD Pipeline"]
Loading

File Walkthrough

Relevant files
Enhancement
15 files
main.py
FastAPI application factory with middleware setup               
+51/-0   
models.py
SQLAlchemy models for users, tasks, refresh tokens             
+50/-0   
schemas.py
Pydantic schemas for API request/response validation         
+60/-0   
security.py
JWT token creation and password hashing utilities               
+38/-0   
db.py
Database connection and session management                             
+18/-0   
deps.py
FastAPI dependency injection for authentication                   
+39/-0   
auth.py
Authentication endpoints for register, login, refresh       
+94/-0   
tasks.py
Task CRUD API endpoints with ownership                                     
+66/-0   
users.py
User management endpoints for superusers                                 
+15/-0   
health.py
Health check and metrics endpoints                                             
+26/-0   
20250809_0001_create_tables.py
Initial database migration for all tables                               
+64/-0   
Dockerfile
Multi-stage Docker build for production deployment             
+41/-0   
docker-compose.yml
Docker Compose setup with PostgreSQL database                       
+46/-0   
ci.yml
GitHub Actions CI pipeline with linting and tests               
+27/-0   
Makefile
Development and deployment automation commands                     
+38/-0   
Configuration changes
2 files
config.py
Application settings with environment variables                   
+26/-0   
env.py
Alembic migration environment configuration                           
+57/-0   
Dependencies
1 files
requirements.txt
Python dependencies for FastAPI and production tools         
+22/-0   
Documentation
1 files
README.md
Complete project documentation and setup instructions       
+61/-12 
Additional files
16 files
.pre-commit-config.yaml +20/-0   
alembic.ini +37/-0   
entrypoint.sh +25/-0   
gunicorn_conf.py +12/-0   
__init__.py [link]   
script.py.mako +26/-0   
__init__.py [link]   
styles.css +1/-0     
base.html +20/-0   
index.html +9/-0     
login.html +5/-0     
register.html +5/-0     
tasks.html +5/-0     
setup.cfg +12/-0   
test_auth.py +20/-0   
test_health.py +14/-0   

…setup

Co-authored-by: gglct.com <gglct.com@gmail.com>
@cursor
Copy link

cursor bot commented Aug 9, 2025

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@Qomserver Qomserver marked this pull request as ready for review August 17, 2025 08:13
@Qomserver Qomserver merged commit 33666f8 into main Aug 17, 2025
0 of 2 checks passed
@qodo-code-review
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 Security concerns

Sensitive configuration handling:
The code uses a default jwt_secret and wildcard forwarded_allow_ips in Gunicorn, which can be unsafe if deployed behind proxies without proper configuration. Ensure strong secrets are required in production, restrict CORS to trusted origins, and configure proxy headers (e.g., X-Forwarded-For) safely.

⚡ Recommended focus areas for review

Security Default

Hardcoded insecure defaults for jwt_secret and permissive CORS origins could leak into production if env vars are not overridden. Consider enforcing non-default secrets in non-development environments and validating CORS origins from environment variables.

jwt_secret: str = "change-me"
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 30
refresh_token_expire_days: int = 7

cors_origins: List[str] = ["http://localhost:8000"]

database_url: str = "sqlite:///./app.sqlite3"
Rate Limit Config

The limiter key function uses client IP but ignores the configured rate_limit setting; no per-route limits are applied. Validate that desired limits are enforced (e.g., decorators or middleware config) and ensure proxies forward client IPs correctly in production.

limiter = Limiter(key_func=lambda request: request.client.host)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, lambda request, exc: HTMLResponse("Rate limit exceeded", status_code=429))
app.add_middleware(SlowAPIMiddleware)
Refresh Rotation Expiry

When rotating refresh tokens, the new token inherits expires_at from the old DB record, potentially shortening its lifetime and causing unexpected expirations. Confirm whether rotation should grant a fresh expiry and, if so, compute a new expires_at aligned with the new token’s exp.

# rotate refresh token
db_token.revoked = True
db.add(RefreshToken(user_id=user.id, token=new_refresh, revoked=False, expires_at=db_token.expires_at))
db.commit()

return TokenPair(access_token=access, refresh_token=new_refresh)

@qodo-code-review
Copy link

qodo-code-review bot commented Aug 17, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Unsafe default secrets and CORS

The app ships with insecure defaults: a hardcoded JWT secret ("change-me") and
permissive CORS origins set via env without robust validation. For production
readiness, require JWT_SECRET to be set (fail fast if default), derive CORS
origins from a validated list (e.g., comma-separated env parsed to exact hosts),
and consider environment-specific config to prevent running with SQLite or
insecure settings in production.

Examples:

app/src/config.py [6-23]
class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", case_sensitive=False)

    app_name: str = "ProdReadyApp"
    env: str = "development"
    root_path: str = ""
    metrics_enabled: bool = True

    jwt_secret: str = "change-me"
    jwt_algorithm: str = "HS256"

 ... (clipped 8 lines)

Solution Walkthrough:

Before:

# app/src/config.py
class Settings(BaseSettings):
    jwt_secret: str = "change-me"
    cors_origins: List[str] = ["http://localhost:8000"]
    database_url: str = "sqlite:///./app.sqlite3"
    env: str = "development"
    ...

After:

# app/src/config.py
from pydantic import field_validator, AnyHttpUrl

class Settings(BaseSettings):
    env: str = "development"
    jwt_secret: str
    cors_origins: List[AnyHttpUrl] = []
    database_url: str
    ...

    @field_validator('jwt_secret')
    def validate_secret(cls, v, info):
        if info.data.get('env') == 'production' and v == 'change-me':
            raise ValueError("JWT_SECRET must be set in production")
        return v
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies critical security vulnerabilities and design flaws, such as a hardcoded jwt_secret and insecure default database_url, which are vital for a production-ready application.

High
Possible issue
Fix static/template paths

Fix template and static directories to use paths relative to the running working
directory inside the container. Using "app/src/..." will break when the app runs
with WORKDIR=/app and code already under /app. Point directly to "src/...".

app/src/main.py [38-40]

-templates = Jinja2Templates(directory="app/src/templates")
+templates = Jinja2Templates(directory="src/templates")
 
-app.mount("/static", StaticFiles(directory="app/src/static"), name="static")
+app.mount("/static", StaticFiles(directory="src/static"), name="static")
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: This is a critical fix, as the application would fail to find template and static files when run inside Docker or via the local uvicorn command due to incorrect pathing.

High
Correct config file paths

Align Alembic config path and gunicorn config with the container’s WORKDIR.
Since WORKDIR is /app and files are copied into /app, drop the leading "app/" to
avoid file-not-found at runtime.

app/docker/entrypoint.sh [24-25]

-alembic -c app/alembic.ini upgrade head
-exec gunicorn 'src.main:create_app()' -k uvicorn.workers.UvicornWorker -c app/gunicorn_conf.py
+alembic -c alembic.ini upgrade head
+exec gunicorn 'src.main:create_app()' -k uvicorn.workers.UvicornWorker -c gunicorn_conf.py
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: This is a critical fix, as the container would fail to start because the alembic and gunicorn commands point to incorrect file paths relative to the container's working directory.

High
Fix Alembic module imports

Update imports to match the installed package layout when running from /app.
Using "app.src" will fail under PYTHONPATH=/app; import from "src" instead so
Alembic can autogenerate and apply migrations.

app/src/alembic/env.py [7-8]

-from app.src.config import settings
-from app.src.models import Base
+from src.config import settings
+from src.models import Base
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: This is a critical fix, as database migrations run via the entrypoint.sh in Docker would fail due to ModuleNotFoundError because of incorrect import paths.

High
Security
Validate input and set correct expiry

Validate the request body with a schema to avoid ambiguous plain-string parsing
and enforce correct content type. Also set the new refresh token's expires_at
based on its own exp claim rather than copying the old expiry.

app/src/routers/auth.py [52-85]

+from ..schemas import TokenPair
+from pydantic import BaseModel
+from ..security import decode_token, create_access_token, create_refresh_token
+from jose import jwt
+
+class RefreshRequest(BaseModel):
+    refresh_token: str
+
 @router.post("/refresh", response_model=TokenPair)
-def refresh_token(token: str, db: Session = Depends(get_db)):
-    # token provided as plain body string (or use schema); kept simple
-    from ..security import decode_token
-
+def refresh_token(req: RefreshRequest, db: Session = Depends(get_db)):
+    token = req.refresh_token
     try:
         payload = decode_token(token)
     except ValueError:
         raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token")
 
     if payload.get("type") != "refresh":
         raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Not a refresh token")
 
     email = payload.get("sub")
     if not email:
         raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token payload")
 
     user = db.query(User).filter(User.email == email).first()
     if user is None:
         raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
 
     db_token = db.query(RefreshToken).filter(RefreshToken.token == token, RefreshToken.revoked == False).first()  # noqa: E712
     if db_token is None or db_token.expires_at < datetime.now(timezone.utc):
         raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Refresh token invalid or expired")
 
     access = create_access_token(subject=user.email)
     new_refresh = create_refresh_token(subject=user.email)
 
-    # rotate refresh token
+    new_exp = datetime.fromtimestamp(jwt.get_unverified_claims(new_refresh)["exp"], tz=timezone.utc)
+
     db_token.revoked = True
-    db.add(RefreshToken(user_id=user.id, token=new_refresh, revoked=False, expires_at=db_token.expires_at))
+    db.add(RefreshToken(user_id=user.id, token=new_refresh, revoked=False, expires_at=new_exp))
     db.commit()
 
     return TokenPair(access_token=access, refresh_token=new_refresh)
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug in the token rotation logic where the new refresh token's expiration date was not being updated, and also improves the API design by using a Pydantic model for request validation.

High
Possible issue
Remove implicit schema creation

Avoid calling create_all at app startup; it can race with Alembic migrations and
mask schema drift. Let Alembic manage the schema and remove implicit DDL from
the request path.

app/src/main.py [21]

-Base.metadata.create_all(bind=engine)
+# Rely on Alembic migrations to manage schema creation/updates.
+# Base.metadata.create_all(bind=engine)
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that using both Alembic and create_all is a bad practice that can hide schema drift, and recommends relying solely on Alembic for robust database management.

Medium
  • More

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments