Files
fitguide/README.md
T

215 lines
6.5 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 |
## 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.