238 lines
8.1 KiB
Markdown
238 lines
8.1 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)
|
|
```
|
|
|
|
## Starting a new project
|
|
|
|
Fork or copy this repo, then update these six things before writing any code:
|
|
|
|
### 1. Project package name — `pyproject.toml`
|
|
|
|
```toml
|
|
[project]
|
|
name = "my-app" # used by uv/pip
|
|
description = "What your app does."
|
|
```
|
|
|
|
### 2. Application display name — `.env` / environment
|
|
|
|
The name shown in the browser title bar, nav bar, and `/docs` page title is
|
|
driven by `APP_NAME` in your `.env` (or any environment variable override):
|
|
|
|
```bash
|
|
APP_NAME="My App"
|
|
```
|
|
|
|
This is the only runtime change needed — templates read `{{ app_name }}`
|
|
from `settings.app_name`, which is sourced from this variable. No template
|
|
edits required.
|
|
|
|
### 3. Superuser credentials — `.env`
|
|
|
|
```bash
|
|
FIRST_SUPERUSER_EMAIL=admin@yourapp.com
|
|
FIRST_SUPERUSER_PASSWORD=<strong-password>
|
|
```
|
|
|
|
### 4. Secret key — `.env`
|
|
|
|
```bash
|
|
SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
|
|
```
|
|
|
|
Never commit a real secret key. The placeholder in `.env.example` is intentionally weak.
|
|
|
|
### 5. Docker / Postgres identifiers — `docker-compose.yml`
|
|
|
|
The compose file uses `app` as the Postgres username, password, and database
|
|
name. Rename them consistently:
|
|
|
|
```yaml
|
|
# db service
|
|
POSTGRES_USER: myapp
|
|
POSTGRES_PASSWORD: <strong-password>
|
|
POSTGRES_DB: myapp
|
|
|
|
# app service
|
|
DATABASE_URL: postgresql+asyncpg://myapp:<strong-password>@db:5432/myapp
|
|
```
|
|
|
|
### 6. This README
|
|
|
|
Replace the title and description with your project's own.
|
|
|
|
---
|
|
|
|
After these changes, run the normal setup:
|
|
|
|
```bash
|
|
uv sync --extra dev
|
|
cp .env.example .env # edit the values above
|
|
uv run python -m scripts.seed
|
|
uv run uvicorn app.main:app --reload
|
|
```
|
|
|
|
## 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 |
|
|
| `LDAP_ENABLED` | `false` | validate logins against LDAP/AD (see below) |
|
|
|
|
## 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.
|
|
|
|
### LDAP / Active Directory (optional)
|
|
|
|
Set `LDAP_ENABLED=true` to validate logins against a directory. On a successful
|
|
bind, a local user is **provisioned on first login** (with an unusable local
|
|
password and the roles in `LDAP_DEFAULT_ROLES`) so RBAC, tokens, and the admin
|
|
UI work unchanged. Existing accounts still authenticate against the local
|
|
database as a fallback, so the seeded superuser keeps working.
|
|
|
|
Two lookup modes (configured in `.env`, see `.env.example`):
|
|
|
|
- **Direct bind** — set `LDAP_BIND_DN_TEMPLATE`, e.g.
|
|
`uid={username},ou=people,dc=example,dc=com`; the login name is substituted
|
|
and used to bind directly.
|
|
- **Search + bind** — leave the template empty and set `LDAP_BIND_DN` /
|
|
`LDAP_BIND_PASSWORD` (a service account) plus `LDAP_USER_SEARCH_BASE` /
|
|
`LDAP_USER_SEARCH_FILTER`; the user is located, then re-bound to verify the
|
|
password.
|
|
|
|
The local user's email/name come from the `LDAP_ATTR_EMAIL` / `LDAP_ATTR_FULL_NAME`
|
|
attributes. Use `ldaps://` (`LDAP_USE_SSL=true`) or `LDAP_START_TLS=true` in
|
|
production so credentials aren't sent in cleartext.
|
|
|
|
## 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.
|