145 lines
4.9 KiB
Markdown
145 lines
4.9 KiB
Markdown
# FastAPI Skeleton
|
|
|
|
A production-shaped starting point for a web app that serves both **HTML (HTMX)** and a **JSON API**.
|
|
|
|
## Stack
|
|
|
|
| Concern | Choice |
|
|
|----------------|--------|
|
|
| Web framework | FastAPI |
|
|
| HTML / interactivity | Jinja2 + HTMX |
|
|
| ORM | SQLAlchemy 2.0 (async) |
|
|
| Migrations | Alembic |
|
|
| Database | SQLite (default) or Postgres — one `DATABASE_URL` |
|
|
| Auth | JWT access + refresh tokens, rotation, DB-backed revocation |
|
|
| Authorization | RBAC: users → roles → permissions |
|
|
| Admin UI | Custom HTMX dashboard (user & role CRUD) |
|
|
| Config | env vars **or** `.env` (pydantic-settings) |
|
|
| Tooling | uv, pytest, ruff |
|
|
| Packaging | Dockerfile + docker-compose |
|
|
|
|
## Layout
|
|
|
|
```
|
|
app/
|
|
core/ config, database engine/session, security (hashing + JWT)
|
|
models/ SQLAlchemy models: user, role, permission, refresh_token
|
|
schemas/ Pydantic request/response models
|
|
crud/ data-access helpers
|
|
services/ auth token issuance / rotation
|
|
deps.py DI: get_db, current user, require_permission(...)
|
|
api/ JSON API: auth, users, roles
|
|
web/ HTML/HTMX routes: pages + admin dashboard
|
|
templates/ Jinja2 templates (+ admin partials)
|
|
static/ CSS
|
|
alembic/ migration environment + initial migration
|
|
scripts/seed.py bootstrap permissions, roles, first superuser
|
|
tests/ pytest (auth + RBAC)
|
|
```
|
|
|
|
## Quick start (local, SQLite)
|
|
|
|
```bash
|
|
# 1. Install dependencies (https://docs.astral.sh/uv/)
|
|
uv sync --extra dev
|
|
|
|
# 2. Configure (optional — sensible defaults exist)
|
|
cp .env.example .env # then edit SECRET_KEY etc.
|
|
|
|
# 3. Seed default roles/permissions + the first superuser
|
|
uv run python -m scripts.seed
|
|
|
|
# 4. Run
|
|
uv run uvicorn app.main:app --reload
|
|
```
|
|
|
|
Open:
|
|
- http://localhost:8000/ — HTMX dashboard (log in with the seeded superuser)
|
|
- http://localhost:8000/docs — interactive API docs
|
|
- http://localhost:8000/admin/users — admin dashboard
|
|
|
|
Default superuser (override via env/`.env`): `admin@example.com` / `changethis`.
|
|
|
|
## Run with Docker (Postgres)
|
|
|
|
```bash
|
|
docker compose up --build
|
|
```
|
|
|
|
This starts Postgres, seeds the database, and serves the app on
|
|
http://localhost:8000. The app connects with
|
|
`DATABASE_URL=postgresql+asyncpg://app:app@db:5432/app`.
|
|
|
|
## Configuration
|
|
|
|
Every setting is read from an environment variable first, then `.env`
|
|
(see `.env.example`). Key ones:
|
|
|
|
| Variable | Default | Notes |
|
|
|----------|---------|-------|
|
|
| `SECRET_KEY` | dev placeholder | **set a strong value in production** |
|
|
| `DATABASE_URL` | `sqlite+aiosqlite:///./app.db` | use `postgresql+asyncpg://...` for Postgres |
|
|
| `ACCESS_TOKEN_EXPIRE_MINUTES` | `15` | |
|
|
| `REFRESH_TOKEN_EXPIRE_DAYS` | `7` | |
|
|
| `AUTO_CREATE_TABLES` | `true` | create tables on startup (dev); use Alembic in prod |
|
|
| `COOKIE_SECURE` | `false` | set `true` behind HTTPS |
|
|
| `FIRST_SUPERUSER_EMAIL` / `_PASSWORD` | admin@example.com / changethis | used by seed |
|
|
|
|
## Auth model
|
|
|
|
- `POST /api/auth/register` — self-service signup (gets the default `user` role)
|
|
- `POST /api/auth/login` — OAuth2 password form (`username` = email) → access + refresh
|
|
- `POST /api/auth/refresh` — rotates: the presented refresh token is revoked, a new pair is issued
|
|
- `POST /api/auth/logout` — revoke one refresh token (body) or all for the user
|
|
|
|
The JSON API authenticates via `Authorization: Bearer <access>`. The HTMX UI
|
|
stores the access token in an httpOnly cookie so pages work without client JS.
|
|
|
|
Refresh tokens are stored server-side as a SHA-256 hash + `jti` with a `revoked`
|
|
flag, so logout and rotation genuinely invalidate them.
|
|
|
|
## RBAC
|
|
|
|
`users → roles → permissions`. Permissions follow `resource:action`
|
|
(`users:read`, `users:write`, `users:delete`, `roles:read`, `roles:write`).
|
|
Protect an endpoint with the dependency factory:
|
|
|
|
```python
|
|
@router.get("", dependencies=[Depends(require_permission("users:read"))])
|
|
async def list_users(...): ...
|
|
```
|
|
|
|
Superusers (`is_superuser=True`) bypass all permission checks.
|
|
|
|
## Migrations (Alembic)
|
|
|
|
`AUTO_CREATE_TABLES` is convenient for dev/SQLite. For real deployments use
|
|
migrations instead:
|
|
|
|
```bash
|
|
# apply the bundled initial migration
|
|
uv run alembic upgrade head
|
|
|
|
# after changing models, autogenerate the next revision
|
|
uv run alembic revision --autogenerate -m "describe change"
|
|
uv run alembic upgrade head
|
|
```
|
|
|
|
The Alembic env reads `DATABASE_URL` from app settings, so it targets the same
|
|
database as the app.
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
uv run pytest
|
|
```
|
|
|
|
Tests run against an in-memory SQLite database and cover the auth lifecycle
|
|
(register/login/me/refresh/logout) and RBAC enforcement.
|
|
|
|
## Where to go next
|
|
|
|
- Add your own models in `app/models/`, schemas in `app/schemas/`, routes in `app/api/` or `app/web/`.
|
|
- Add permissions to `scripts/seed.py` and guard routes with `require_permission(...)`.
|
|
- Swap `HS256` for asymmetric `RS256` keys if you need to verify tokens in other services.
|