146 lines
7.6 KiB
Markdown
146 lines
7.6 KiB
Markdown
❯ Please create a skeleton project for a typical web application that serves HTML and JSON data. The skeleton
|
||
should build out a basic scaffold using python FastAPI, HTMX, and an ORM that can use sqlite or postgres. The
|
||
skeleton should also support an admin dashboard, RBAC, and JWT token management interfaces for users. Since this
|
||
is a skeleton configuration should be accepted either by file or by environment variables. There should be a
|
||
Docker file as well. Please ask any clarifying questions and propose a plan for this project.
|
||
|
||
● I'll ask a few key questions to shape the skeleton, since several decisions here meaningfully change the
|
||
structure.
|
||
|
||
● User answered Claude's questions:
|
||
⎿ · Which ORM / data layer approach do you want? → SQLAlchemy 2.0 async (Recommended)
|
||
· How should the admin dashboard be built? → Custom HTMX dashboard (Recommended)
|
||
· What auth/JWT depth do you want in the skeleton? → Access + refresh + revocation (Recommended)
|
||
· Python tooling / dependency manager? → uv (Recommended)
|
||
|
||
● Good — all defaults confirmed. Let me present a concrete plan for approval before building.
|
||
|
||
● Here's my proposed plan for the FastAPI skeleton.
|
||
|
||
Stack (confirmed)
|
||
|
||
- FastAPI + Jinja2 + HTMX (server-rendered HTML, JSON API alongside)
|
||
- SQLAlchemy 2.0 async + Alembic migrations; SQLite (aiosqlite) or Postgres (asyncpg) via one DATABASE_URL
|
||
- JWT: access + refresh tokens with rotation and DB-backed revocation (logout / blocklist)
|
||
- RBAC: users ↔ roles ↔ permissions, enforced via FastAPI dependencies
|
||
- Custom HTMX admin dashboard (user/role CRUD)
|
||
- uv for deps; Docker + docker-compose (app + Postgres)
|
||
- Config via pydantic-settings — reads env vars and .env file
|
||
|
||
Project layout
|
||
|
||
fastapi_skeleton/
|
||
├── app/
|
||
│ ├── main.py # app factory, router + middleware wiring, lifespan
|
||
│ ├── core/
|
||
│ │ ├── config.py # pydantic-settings (env + .env)
|
||
│ │ ├── security.py # password hashing, JWT encode/decode
|
||
│ │ └── database.py # async engine, session, Base
|
||
│ ├── models/ # user, role, permission, refresh_token (revocation)
|
||
│ ├── schemas/ # pydantic request/response models
|
||
│ ├── crud/ # data-access helpers
|
||
│ ├── deps.py # get_db, get_current_user, require_permission(...)
|
||
│ ├── api/ # JSON API
|
||
│ │ ├── auth.py # register, login, refresh, logout
|
||
│ │ ├── users.py # me, user management
|
||
│ │ └── router.py
|
||
│ ├── web/ # HTML/HTMX routes
|
||
│ │ ├── pages.py # login page, dashboard home
|
||
│ │ └── admin.py # admin: users, roles, permissions CRUD (HTMX)
|
||
│ ├── templates/ # Jinja2: base, partials, admin/*
|
||
│ └── static/ # htmx.min.js, minimal css
|
||
├── alembic/ + alembic.ini # migrations
|
||
├── scripts/seed.py # bootstrap admin user + default roles/perms
|
||
├── tests/ # pytest: auth, rbac, smoke
|
||
├── pyproject.toml + uv.lock
|
||
├── Dockerfile # multi-stage, uv-based
|
||
├── docker-compose.yml # app + postgres
|
||
├── .env.example
|
||
├── .dockerignore / .gitignore
|
||
└── README.md
|
||
|
||
Key design points
|
||
|
||
- RBAC model: require_permission("users:write") dependency factory; roles aggregate permissions; a superuser flag
|
||
bypasses checks. Seed script creates admin/user roles + an initial admin.
|
||
- Auth flow: /api/auth/login → access (short) + refresh (long) tokens; /api/auth/refresh rotates and revokes the
|
||
old refresh token; /api/auth/logout revokes. Refresh tokens stored hashed in DB with a revoked flag.
|
||
- Web vs API auth: API uses Authorization: Bearer; the HTMX UI uses an httpOnly cookie holding the access token so
|
||
the dashboard works without JS token handling.
|
||
- DB portability: single DATABASE_URL; defaults to SQLite file so it runs with zero setup, Postgres via compose.
|
||
- Config: all settings (secret key, token TTLs, DB URL, CORS, admin bootstrap creds) overridable by env var or
|
||
.env.
|
||
- Tests: pytest + httpx async client against an in-memory SQLite, covering register/login/refresh/logout and an
|
||
RBAC permission check.
|
||
|
||
Build order
|
||
|
||
1. Tooling + config + Docker scaffolding
|
||
2. Database, models, Alembic
|
||
3. Security + auth API + revocation
|
||
4. RBAC deps + user/role API
|
||
5. HTMX templates + admin dashboard
|
||
6. Seed script, tests, README
|
||
|
||
This is a sizable scaffold (~35–40 files). Want me to build it all now, or would you prefer I build it phase by
|
||
phase so you can review as I go? I'll start with phase 1 either way unless you'd like to adjust anything above.
|
||
|
||
● Everything works. Let me verify the hand-written Alembic migration applies cleanly against a fresh database:
|
||
|
||
● Bash(cd /home/iroddis/dev/fastapi_skeleton
|
||
export DATABASE_URL="sqlite+aiosqlite:///./alembic_test.db" SECRET_KEY=test…)
|
||
⎿ == upgrade head ==
|
||
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
|
||
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
|
||
… +7 lines (ctrl+o to expand)
|
||
|
||
● Migrations work both directions. Let me stop the server and clean up:
|
||
|
||
● Bash(rm -f app.db alembic_test.db /tmp/skel_cookies.txt /tmp/skeleton_server.log; echo "cleaned")
|
||
⎿ cleaned
|
||
|
||
● Stop Task
|
||
⎿ SECRET_KEY=test-secret AUTO_CREATE_TABLES=true uv run uvicorn app.main:app --port 8099 · stopped
|
||
|
||
● Verified and working. Here's what I confirmed:
|
||
|
||
Results
|
||
|
||
Static checks
|
||
- ruff check . → All checks passed (B008 ignored — that rule fires on FastAPI's Depends()-in-defaults idiom, which
|
||
is intentional)
|
||
- uv sync --extra dev resolved and installed cleanly on Python 3.14
|
||
|
||
Test suite — uv run pytest → 7 passed
|
||
- Auth lifecycle: register → login → /me → refresh (with rotation) → confirms old refresh token is revoked →
|
||
logout invalidates the rotated token
|
||
- Wrong password rejected; unauthenticated /me returns 401
|
||
- RBAC: reader role can GET but not POST users (403); superuser bypasses checks; user with no roles is forbidden;
|
||
registration grants the default user role
|
||
|
||
Live smoke test (seeded SQLite, real uvicorn server)
|
||
- Login page renders; unauthenticated / → 302 redirect to /login
|
||
- API bearer-token login works; /api/users/me returns the admin with all permissions; /api/roles → 200
|
||
- Web cookie flow: form login → 303 + httpOnly access_token cookie → dashboard and /admin/users render → HTMX
|
||
create-user returns the table partial with the new row
|
||
- RBAC denial confirmed live: a normal registered user gets 403 on POST /api/users but 200 on GET /api/users
|
||
|
||
Alembic — the hand-written 0001_initial migration applies (upgrade head creates all 7 tables) and reverses cleanly
|
||
(downgrade base).
|
||
|
||
One fix made during verification
|
||
|
||
app/services/auth.py was passing datetime.fromtimestamp(payload["exp"]), but PyJWT leaves exp as a timezone-aware
|
||
datetime in the payload dict (it only converts to epoch inside the encoded token). Changed it to use
|
||
payload["exp"] directly — this was the cause of the initial 5 failures, now all green.
|
||
|
||
The skeleton is complete and functional. To run it yourself:
|
||
|
||
uv sync --extra dev
|
||
uv run python -m scripts.seed # admin@example.com / changethis
|
||
uv run uvicorn app.main:app --reload # http://localhost:8000
|
||
|
||
Test artifacts (app.db, cookie jar, logs) were cleaned up, and the background server was stopped.
|
||
|
||
|