Initial Claude implementation
This commit is contained in:
commit
e2d3ab9c2a
63 files changed
+3930
No files matched your search
@@ -0,0 +1,144 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user