71 lines
3.5 KiB
Markdown
71 lines
3.5 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for working in this repo. Read alongside `README.md` (user-facing setup).
|
|
|
|
## What this is
|
|
|
|
A FastAPI skeleton serving **both** a JSON API (`/api/*`) and server-rendered
|
|
HTML via **Jinja2 + HTMX** (`/`, `/admin/*`). Async SQLAlchemy 2.0 ORM, JWT auth
|
|
with refresh-token revocation, and RBAC (users → roles → permissions).
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
uv sync --extra dev # install (incl. dev tools)
|
|
uv run python -m scripts.seed # seed perms/roles/superuser (idempotent)
|
|
uv run uvicorn app.main:app --reload # dev server on :8000
|
|
uv run pytest # tests (in-memory SQLite)
|
|
uv run ruff check . [--fix] # lint
|
|
uv run alembic upgrade head # apply migrations
|
|
uv run alembic revision --autogenerate -m "msg" # new migration after model changes
|
|
docker compose up --build # app + Postgres, auto-seeded
|
|
```
|
|
|
|
## Architecture / layering
|
|
|
|
Request → router (`app/api/` JSON or `app/web/` HTML) → `app/crud/` (data access)
|
|
→ `app/models/`. Schemas in `app/schemas/`, cross-cutting DI in `app/deps.py`,
|
|
token logic in `app/services/auth.py`. Keep route handlers thin; put DB logic in
|
|
`crud/`, not in routers.
|
|
|
|
## Conventions that matter
|
|
|
|
- **Everything async.** Engine, sessions, CRUD, routes, tests. Use `await`; never
|
|
add a sync DB call. `get_db` (in `app/core/database.py`) yields the request session.
|
|
- **Relationships use `lazy="selectin"`** (`User.roles`, `Role.permissions`) so
|
|
attributes are safe to read after the session closes — required under async.
|
|
Preserve this when adding relationships that templates/schemas read.
|
|
- **Config** is `app/core/config.py` (`pydantic-settings`). Add new settings there;
|
|
they're auto-read from env vars (uppercased field name) or `.env`. Env wins.
|
|
- **RBAC**: guard API routes with `Depends(require_permission("resource:action"))`;
|
|
guard web routes with `web_require(...)` in `app/web/admin.py`. Superusers bypass
|
|
all checks (`User.is_superuser` / `User.has_permission`). Add new permission
|
|
strings to `scripts/seed.py`'s `PERMISSIONS`/`ROLES`.
|
|
- **Auth, two transports**: API uses `Authorization: Bearer`; the HTMX UI uses an
|
|
httpOnly cookie (`settings.cookie_name`). `app/deps.py` reads both. Web auth
|
|
failures raise `NotAuthenticated`, handled in `main.py` as a redirect to `/login`.
|
|
- **Refresh tokens** are stored server-side (hash + `jti` + `revoked`) in the
|
|
`refresh_tokens` table. `services/auth.rotate_refresh_token` revokes-then-reissues.
|
|
Note: JWT payload `exp` is a tz-aware `datetime` (PyJWT only epoch-encodes inside
|
|
the token) — pass it straight to the model, don't `fromtimestamp` it.
|
|
|
|
## Database
|
|
|
|
One `DATABASE_URL` drives SQLite (`sqlite+aiosqlite://`, the default) or Postgres
|
|
(`postgresql+asyncpg://`). `AUTO_CREATE_TABLES=true` calls `Base.metadata.create_all`
|
|
on startup for dev convenience; production should use Alembic instead. `init_db()`
|
|
imports `app.models` inside the function to avoid a circular import.
|
|
|
|
## Tests
|
|
|
|
`tests/conftest.py` builds an in-memory SQLite engine with `StaticPool`, creates
|
|
tables, and overrides `get_db`. Fixtures: `client` (httpx ASGI), `seeded`
|
|
(perms+roles), `make_user`. `asyncio_mode = "auto"` — write `async def test_*`
|
|
without decorators.
|
|
|
|
## When extending
|
|
|
|
Add a feature → model in `models/` (+ register in `models/__init__.py`), schema in
|
|
`schemas/`, CRUD in `crud/`, routes in `api/` and/or `web/`, then a migration. If it
|
|
needs access control, add the permission to the seed and guard the routes.
|