Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ DOMAIN=localhost
# DOMAIN=localhost.tiangolo.com

# Used by the backend to generate links in emails to the frontend
FRONTEND_HOST=http://localhost:5173
FRONTEND_HOST=http://localhost:5174
# In staging and production, set this env var to the frontend host, e.g.
# FRONTEND_HOST=https://dashboard.example.com

Expand All @@ -17,7 +17,7 @@ PROJECT_NAME="Full Stack FastAPI Project"
STACK_NAME=full-stack-fastapi-project

# Backend
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5174,https://localhost,https://localhost:5174,http://localhost.tiangolo.com"
SECRET_KEY=changethis
FIRST_SUPERUSER=admin@example.com
FIRST_SUPERUSER_PASSWORD=changethis
Expand Down
6 changes: 2 additions & 4 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ ENV PATH="/app/.venv/bin:$PATH"

# Install dependencies
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
COPY uv.lock pyproject.toml /app/

RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-workspace --package app

COPY ./backend/scripts /app/backend/scripts
Expand All @@ -36,8 +36,6 @@ COPY ./backend/app /app/backend/app
# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --package app

WORKDIR /app/backend/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Add avatar_url to User

Revision ID: a1b2c3d4e5f6
Revises: fe56fa70289e
Create Date: 2026-02-05 10:00:00.000000

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes


# revision identifiers, used by Alembic.
revision = 'a1b2c3d4e5f6'
down_revision = 'fe56fa70289e'
branch_labels = None
depends_on = None


def upgrade():
op.add_column('user', sa.Column('avatar_url', sa.String(length=500), nullable=True))


def downgrade():
op.drop_column('user', 'avatar_url')
33 changes: 32 additions & 1 deletion backend/app/api/routes/users.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import shutil
import uuid
from pathlib import Path
from typing import Any

from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from sqlmodel import col, delete, func, select

from app import crud
Expand Down Expand Up @@ -99,6 +101,35 @@ def update_user_me(
return current_user


@router.post("/me/avatar", response_model=UserPublic)
def update_user_avatar(
*, session: SessionDep, current_user: CurrentUser, file: UploadFile = File(...)
) -> Any:
"""
Upload user avatar.
"""
# Ensure upload directory exists
upload_dir = Path("app/static/uploads")
upload_dir.mkdir(parents=True, exist_ok=True)

# Generate unique filename
file_ext = Path(file.filename).suffix if file.filename else ""
file_name = f"{current_user.id}_{uuid.uuid4()}{file_ext}"
file_path = upload_dir / file_name

with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)

# Update user profile
# Assuming served at /static/uploads/
avatar_url = f"/static/uploads/{file_name}"
current_user.avatar_url = avatar_url
session.add(current_user)
session.commit()
session.refresh(current_user)
return current_user


@router.patch("/me/password", response_model=Message)
def update_password_me(
*, session: SessionDep, body: UpdatePassword, current_user: CurrentUser
Expand Down
2 changes: 1 addition & 1 deletion backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Settings(BaseSettings):
SECRET_KEY: str = secrets.token_urlsafe(32)
# 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
FRONTEND_HOST: str = "http://localhost:5173"
FRONTEND_HOST: str = "http://localhost:5174"
ENVIRONMENT: Literal["local", "staging", "production"] = "local"

BACKEND_CORS_ORIGINS: Annotated[
Expand Down
10 changes: 10 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os

import sentry_sdk
from fastapi import FastAPI
from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles
from starlette.middleware.cors import CORSMiddleware

from app.api.main import api_router
Expand Down Expand Up @@ -30,4 +33,11 @@ def custom_generate_unique_id(route: APIRoute) -> str:
allow_headers=["*"],
)

# Mount static files
upload_dir = "app/static"
if not os.path.exists(upload_dir):
os.makedirs(upload_dir)

app.mount("/static", StaticFiles(directory=upload_dir), name="static")

app.include_router(api_router, prefix=settings.API_V1_STR)
1 change: 1 addition & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class UserBase(SQLModel):
is_active: bool = True
is_superuser: bool = False
full_name: str | None = Field(default=None, max_length=255)
avatar_url: str | None = Field(default=None, max_length=500)


# Properties to receive via API on creation
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 11 additions & 10 deletions compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- "80:80"
- "8090:8080"
- "81:80"
- "8091:8080"
# Duplicate the command from compose.yml to add --api.insecure=true
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
Expand Down Expand Up @@ -48,17 +48,17 @@ services:
db:
restart: "no"
ports:
- "5432:5432"
- "5433:5432"

adminer:
restart: "no"
ports:
- "8080:8080"
- "8082:8080"

backend:
restart: "no"
ports:
- "8000:8000"
- "8001:8000"
build:
context: .
dockerfile: backend/Dockerfile
Expand All @@ -81,6 +81,7 @@ services:
# TODO: remove once coverage is done locally
volumes:
- ./backend/htmlcov:/app/backend/htmlcov
- ./backend/app/static:/app/backend/app/static
environment:
SMTP_HOST: "mailcatcher"
SMTP_PORT: "1025"
Expand All @@ -90,18 +91,18 @@ services:
mailcatcher:
image: schickling/mailcatcher
ports:
- "1080:1080"
- "1025:1025"
- "1081:1080"
- "1026:1025"

frontend:
restart: "no"
ports:
- "5173:80"
- "5174:80"
build:
context: .
dockerfile: frontend/Dockerfile
args:
- VITE_API_URL=http://localhost:8000
- VITE_API_URL=http://localhost:8001
- NODE_ENV=development

playwright:
Expand All @@ -127,7 +128,7 @@ services:
- ./frontend/blob-report:/app/frontend/blob-report
- ./frontend/test-results:/app/frontend/test-results
ports:
- 9323:9323
- 9324:9323

networks:
traefik-public:
Expand Down
4 changes: 2 additions & 2 deletions compose.traefik.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ services:
image: traefik:3.6
ports:
# Listen on port 80, default for HTTP, necessary to redirect to HTTPS
- 80:80
- 81:80
# Listen on port 443, default for HTTPS
- 443:443
- 444:443
restart: always
labels:
# Enable Traefik for this service, to make it available in the public network
Expand Down
5 changes: 5 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:
timeout: 10s
volumes:
- app-db-data:/var/lib/postgresql/data/pgdata
- app-static-data:/app/backend/app/static
env_file:
- .env
environment:
Expand Down Expand Up @@ -115,6 +116,9 @@ services:
timeout: 5s
retries: 5

volumes:
- app-static-data:/app/backend/app/static

build:
context: .
dockerfile: backend/Dockerfile
Expand Down Expand Up @@ -167,6 +171,7 @@ services:
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect
volumes:
app-db-data:
app-static-data:

networks:
traefik-public:
Expand Down
36 changes: 18 additions & 18 deletions development.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ docker compose watch

* Now you can open your browser and interact with these URLs:

Frontend, built with Docker, with routes handled based on the path: <http://localhost:5173>
Frontend, built with Docker, with routes handled based on the path: <http://localhost:5174>

Backend, JSON based web API based on OpenAPI: <http://localhost:8000>
Backend, JSON based web API based on OpenAPI: <http://localhost:8001>

Automatic interactive documentation with Swagger UI (from the OpenAPI backend): <http://localhost:8000/docs>
Automatic interactive documentation with Swagger UI (from the OpenAPI backend): <http://localhost:8001/docs>

Adminer, database web administration: <http://localhost:8080>
Adminer, database web administration: <http://localhost:8082>

Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8090>
Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8091>

**Note**: The first time you start your stack, it might take a minute for it to be ready. While the backend waits for the database to be ready and configures everything. You can check the logs to monitor it.

Expand All @@ -44,13 +44,13 @@ This is useful for:
* Verifying email content and formatting
* Debugging email-related functionality without sending real emails

The backend is automatically configured to use Mailcatcher when running with Docker Compose locally (SMTP on port 1025). All captured emails can be viewed at <http://localhost:1080>.
The backend is automatically configured to use Mailcatcher when running with Docker Compose locally (SMTP on port 1025). All captured emails can be viewed at <http://localhost:1081>.

## Local Development

The Docker Compose files are configured so that each of the services is available in a different port in `localhost`.

For the backend and frontend, they use the same port that would be used by their local development server, so, the backend is at `http://localhost:8000` and the frontend at `http://localhost:5173`.
For the backend and frontend, they use the same port that would be used by their local development server, so, the backend is at `http://localhost:8001` and the frontend at `http://localhost:5174`.

This way, you could turn off a Docker Compose service and start its local development service, and everything would keep working, because it all uses the same ports.

Expand All @@ -76,7 +76,7 @@ And then you can run the local development server for the backend:

```bash
cd backend
fastapi dev app/main.py
fastapi dev app/main.py --port 8001
```

## Docker Compose in `localhost.tiangolo.com`
Expand Down Expand Up @@ -188,19 +188,19 @@ The production or staging URLs would use these same paths, but with your own dom

Development URLs, for local development.

Frontend: <http://localhost:5173>
Frontend: <http://localhost:5174>

Backend: <http://localhost:8000>
Backend: <http://localhost:8001>

Automatic Interactive Docs (Swagger UI): <http://localhost:8000/docs>
Automatic Interactive Docs (Swagger UI): <http://localhost:8001/docs>

Automatic Alternative Docs (ReDoc): <http://localhost:8000/redoc>
Automatic Alternative Docs (ReDoc): <http://localhost:8001/redoc>

Adminer: <http://localhost:8080>
Adminer: <http://localhost:8082>

Traefik UI: <http://localhost:8090>
Traefik UI: <http://localhost:8091>

MailCatcher: <http://localhost:1080>
MailCatcher: <http://localhost:1081>

### Development URLs with `localhost.tiangolo.com` Configured

Expand All @@ -214,8 +214,8 @@ Automatic Interactive Docs (Swagger UI): <http://api.localhost.tiangolo.com/docs

Automatic Alternative Docs (ReDoc): <http://api.localhost.tiangolo.com/redoc>

Adminer: <http://localhost.tiangolo.com:8080>
Adminer: <http://localhost.tiangolo.com:8082>

Traefik UI: <http://localhost.tiangolo.com:8090>
Traefik UI: <http://localhost.tiangolo.com:8091>

MailCatcher: <http://localhost.tiangolo.com:1080>
MailCatcher: <http://localhost.tiangolo.com:1081>
2 changes: 1 addition & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ bun install
bun run dev
```

* Then open your browser at http://localhost:5173/.
* Then open your browser at http://localhost:5174/.

Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the frontend Docker image and start it, to test it in a production-like environment. But building the image at every change will not be as productive as running the local development server with live reload.

Expand Down
4 changes: 2 additions & 2 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:5173',
baseURL: 'http://localhost:5174',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
Expand Down Expand Up @@ -85,7 +85,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: 'bun run dev',
url: 'http://localhost:5173',
url: 'http://localhost:5174',
reuseExistingServer: !process.env.CI,
},
});
Loading
Loading