From acfa21ec389e883757f4018c9acf1015daa13a06 Mon Sep 17 00:00:00 2001 From: derekc Date: Mon, 25 May 2026 01:23:47 -0700 Subject: [PATCH] Update README to reflect current state Add rate limits, pagination, Ntfy events, ALLOWED_ORIGINS, security section, notify.py in project structure, corrected admin seed behaviour, and updated stack versions. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 91 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 5396ce1..e9101e3 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,19 @@ Built with FastAPI, vanilla JS, and MySQL — fully containerized with Docker Co - **Admin panel** — list all users, reset passwords, disable/enable accounts, delete users, and impersonate any user for debugging - **JWT auth** — Bearer token auth with impersonation support (admin tokens carry an `admin_id` claim) - **About page** — public explainer on what an infinity bottle is, why to track it, and how to get started +- **Push notifications** — Ntfy alerts for admin logins, new registrations, account changes, and service startup ## Stack | Layer | Technology | |---|---| -| Backend | FastAPI (Python 3.12), async SQLAlchemy 2.0, Uvicorn | +| Backend | FastAPI (Python 3.12), async SQLAlchemy 2.0.50, Uvicorn | | Database | MySQL 8 | | Frontend | Vanilla HTML/CSS/JS (no framework) | | Reverse Proxy | Nginx | -| Auth | JWT via `python-jose`, passwords hashed with `passlib[bcrypt==4.0.1]` | +| Auth | JWT via `python-jose`, passwords hashed with `passlib[bcrypt]` | +| Rate limiting | `slowapi` | +| Notifications | `ntfy` (optional) | | Container | Docker Compose | ## Project Structure @@ -40,6 +43,7 @@ bourbonacci/ │ │ ├── config.py # Pydantic settings (reads from .env) │ │ ├── database.py # Async engine, session factory, Base, init_db() │ │ ├── dependencies.py # get_db, get_current_user, get_current_admin +│ │ ├── limiter.py # slowapi rate limiter instance │ │ ├── models/ │ │ │ ├── user.py # User ORM model │ │ │ └── entry.py # Entry ORM model (add/remove types) @@ -53,7 +57,8 @@ bourbonacci/ │ │ │ ├── user.py # Pydantic schemas for user/auth │ │ │ └── entry.py # Pydantic schemas for entries/stats │ │ └── utils/ -│ │ └── security.py # JWT creation/decode, password hashing +│ │ ├── security.py # JWT creation/decode, password hashing +│ │ └── notify.py # Ntfy push notification helper │ ├── Dockerfile │ └── requirements.txt ├── frontend/ @@ -103,30 +108,30 @@ When impersonating a user, the nav shows **"Viewing as [name]"** and a **Return ## API Endpoints ### Auth -| Method | Path | Auth | Description | -|---|---|---|---| -| POST | `/api/auth/register` | No | Register new account, returns JWT | -| POST | `/api/auth/login` | No | Login, returns JWT | +| Method | Path | Auth | Rate limit | Description | +|---|---|---|---|---| +| POST | `/api/auth/register` | No | 5/min | Register new account, returns JWT | +| POST | `/api/auth/login` | No | 10/min | Login, returns JWT | ### Users -| Method | Path | Auth | Description | -|---|---|---|---| -| GET | `/api/users/me` | Yes | Get current user profile | -| PUT | `/api/users/me` | Yes | Update display name, timezone, bottle size | -| PUT | `/api/users/me/password` | Yes | Change password | +| Method | Path | Auth | Rate limit | Description | +|---|---|---|---|---| +| GET | `/api/users/me` | Yes | — | Get current user profile | +| PUT | `/api/users/me` | Yes | — | Update display name, timezone, bottle size | +| PUT | `/api/users/me/password` | Yes | 5/min | Change password | ### Entries | Method | Path | Auth | Description | |---|---|---|---| -| GET | `/api/entries` | Yes | List all entries for current user | +| GET | `/api/entries` | Yes | List entries for current user (`?limit=200&offset=0`, max 1000) | | POST | `/api/entries` | Yes | Create an entry (`add` or `remove`) | | DELETE | `/api/entries/{id}` | Yes | Delete an entry | | GET | `/api/entries/stats` | Yes | Aggregated stats for current user | ### Public -| Method | Path | Auth | Description | -|---|---|---|---| -| GET | `/api/public/stats` | No | Stats + bourbon list for all users | +| Method | Path | Auth | Rate limit | Description | +|---|---|---|---|---| +| GET | `/api/public/stats` | No | 30/min | Stats + bourbon list for all users (max 500) | ### Admin | Method | Path | Auth | Description | @@ -173,12 +178,13 @@ Authenticated routes expect `Authorization: Bearer ` header. ### Prerequisites - Docker + Docker Compose +- (Optional) Nginx Proxy Manager or equivalent for TLS termination ### Setup ```bash cp .env.example .env -# Edit .env — set real passwords and a secure SECRET_KEY +# Edit .env — set real passwords, a secure SECRET_KEY, and your domain ``` Generate a secure secret key: @@ -192,9 +198,9 @@ python3 -c "import secrets; print(secrets.token_hex(64))" docker compose up -d ``` -The app will be available at `http://localhost` (or whichever port is mapped in `docker-compose.yml`). +The app will be available at `http://localhost:8057` by default (configure the port mapping in `docker-compose.yml`). -The backend waits for MySQL to pass its healthcheck before starting. Tables are created automatically on first boot via SQLAlchemy's `create_all`. The admin account defined in `.env` is seeded/re-synced on every container start. +The backend waits for MySQL to pass its healthcheck before starting. Tables are created automatically on first boot via SQLAlchemy's `create_all`. The admin account defined in `.env` is seeded on first boot — subsequent restarts will not overwrite a manually changed password. ### Stop @@ -206,17 +212,44 @@ docker compose down -v ## Environment Variables -| Variable | Description | +| Variable | Required | Description | +|---|---|---| +| `MYSQL_ROOT_PASSWORD` | Yes | MySQL root password | +| `MYSQL_DATABASE` | Yes | Database name (default: `bourbonacci`) | +| `MYSQL_USER` | Yes | App DB user | +| `MYSQL_PASSWORD` | Yes | App DB password | +| `DATABASE_URL` | Yes | SQLAlchemy async DSN (`mysql+aiomysql://user:pass@db:3306/dbname`) | +| `SECRET_KEY` | Yes | JWT signing secret — keep long and random | +| `ACCESS_TOKEN_EXPIRE_MINUTES` | No | JWT TTL in minutes (default: `480`) | +| `ADMIN_USERNAME` | Yes | Admin account email, seeded on first boot | +| `ADMIN_PASSWORD` | Yes | Admin account password, seeded on first boot | +| `ALLOWED_ORIGINS` | No | Comma-separated CORS origins (default: `http://localhost`) | +| `NTFY_URL` | No | Full Ntfy topic URL (e.g. `https://ntfy.example.com/my-topic`). Leave blank to disable notifications. | +| `NTFY_TOKEN` | No | Ntfy access token for protected topics | + +## Ntfy Notifications + +When `NTFY_URL` is set, the following events trigger push notifications: + +| Event | Priority | |---|---| -| `MYSQL_ROOT_PASSWORD` | MySQL root password | -| `MYSQL_DATABASE` | Database name (default: `bourbonacci`) | -| `MYSQL_USER` | App DB user | -| `MYSQL_PASSWORD` | App DB password | -| `DATABASE_URL` | SQLAlchemy async DSN (`mysql+aiomysql://user:pass@db:3306/dbname`) | -| `SECRET_KEY` | JWT signing secret — keep long and random | -| `ACCESS_TOKEN_EXPIRE_MINUTES` | JWT TTL in minutes (default: `480`) | -| `ADMIN_USERNAME` | Admin account email, seeded on every container start | -| `ADMIN_PASSWORD` | Admin account password, re-synced on every container start | +| Service startup | Low | +| New user registration | Default | +| Admin login | High | +| User account disabled | Default | +| User account deleted | Default | + +## Security + +- Passwords hashed with bcrypt +- JWTs signed with HS256, configurable TTL +- Rate limiting on auth and password change endpoints +- Disabled accounts blocked at login and on every authenticated request +- All security headers set in Nginx (HSTS, CSP, X-Frame-Options, etc.) +- Gzip compression enabled for text responses +- Backend runs as a non-root user inside the container +- Container resource limits enforced (memory + CPU) +- Docker image digests pinned in `docker-compose.yml` ## Data Model