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 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 01:23:47 -07:00
parent 84b07e33e7
commit acfa21ec38
+62 -29
View File
@@ -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 - **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) - **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 - **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 ## Stack
| Layer | Technology | | 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 | | Database | MySQL 8 |
| Frontend | Vanilla HTML/CSS/JS (no framework) | | Frontend | Vanilla HTML/CSS/JS (no framework) |
| Reverse Proxy | Nginx | | 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 | | Container | Docker Compose |
## Project Structure ## Project Structure
@@ -40,6 +43,7 @@ bourbonacci/
│ │ ├── config.py # Pydantic settings (reads from .env) │ │ ├── config.py # Pydantic settings (reads from .env)
│ │ ├── database.py # Async engine, session factory, Base, init_db() │ │ ├── database.py # Async engine, session factory, Base, init_db()
│ │ ├── dependencies.py # get_db, get_current_user, get_current_admin │ │ ├── dependencies.py # get_db, get_current_user, get_current_admin
│ │ ├── limiter.py # slowapi rate limiter instance
│ │ ├── models/ │ │ ├── models/
│ │ │ ├── user.py # User ORM model │ │ │ ├── user.py # User ORM model
│ │ │ └── entry.py # Entry ORM model (add/remove types) │ │ │ └── entry.py # Entry ORM model (add/remove types)
@@ -53,7 +57,8 @@ bourbonacci/
│ │ │ ├── user.py # Pydantic schemas for user/auth │ │ │ ├── user.py # Pydantic schemas for user/auth
│ │ │ └── entry.py # Pydantic schemas for entries/stats │ │ │ └── entry.py # Pydantic schemas for entries/stats
│ │ └── utils/ │ │ └── utils/
│ │ ── security.py # JWT creation/decode, password hashing │ │ ── security.py # JWT creation/decode, password hashing
│ │ └── notify.py # Ntfy push notification helper
│ ├── Dockerfile │ ├── Dockerfile
│ └── requirements.txt │ └── requirements.txt
├── frontend/ ├── frontend/
@@ -103,30 +108,30 @@ When impersonating a user, the nav shows **"Viewing as [name]"** and a **Return
## API Endpoints ## API Endpoints
### Auth ### Auth
| Method | Path | Auth | Description | | Method | Path | Auth | Rate limit | Description |
|---|---|---|---| |---|---|---|---|---|
| POST | `/api/auth/register` | No | Register new account, returns JWT | | POST | `/api/auth/register` | No | 5/min | Register new account, returns JWT |
| POST | `/api/auth/login` | No | Login, returns JWT | | POST | `/api/auth/login` | No | 10/min | Login, returns JWT |
### Users ### Users
| Method | Path | Auth | Description | | Method | Path | Auth | Rate limit | Description |
|---|---|---|---| |---|---|---|---|---|
| GET | `/api/users/me` | Yes | Get current user profile | | GET | `/api/users/me` | Yes | — | Get current user profile |
| PUT | `/api/users/me` | Yes | Update display name, timezone, bottle size | | PUT | `/api/users/me` | Yes | — | Update display name, timezone, bottle size |
| PUT | `/api/users/me/password` | Yes | Change password | | PUT | `/api/users/me/password` | Yes | 5/min | Change password |
### Entries ### Entries
| Method | Path | Auth | Description | | 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`) | | POST | `/api/entries` | Yes | Create an entry (`add` or `remove`) |
| DELETE | `/api/entries/{id}` | Yes | Delete an entry | | DELETE | `/api/entries/{id}` | Yes | Delete an entry |
| GET | `/api/entries/stats` | Yes | Aggregated stats for current user | | GET | `/api/entries/stats` | Yes | Aggregated stats for current user |
### Public ### Public
| Method | Path | Auth | Description | | Method | Path | Auth | Rate limit | Description |
|---|---|---|---| |---|---|---|---|---|
| GET | `/api/public/stats` | No | Stats + bourbon list for all users | | GET | `/api/public/stats` | No | 30/min | Stats + bourbon list for all users (max 500) |
### Admin ### Admin
| Method | Path | Auth | Description | | Method | Path | Auth | Description |
@@ -173,12 +178,13 @@ Authenticated routes expect `Authorization: Bearer <token>` header.
### Prerequisites ### Prerequisites
- Docker + Docker Compose - Docker + Docker Compose
- (Optional) Nginx Proxy Manager or equivalent for TLS termination
### Setup ### Setup
```bash ```bash
cp .env.example .env 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: Generate a secure secret key:
@@ -192,9 +198,9 @@ python3 -c "import secrets; print(secrets.token_hex(64))"
docker compose up -d 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 ### Stop
@@ -206,17 +212,44 @@ docker compose down -v
## Environment Variables ## 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 | | Service startup | Low |
| `MYSQL_DATABASE` | Database name (default: `bourbonacci`) | | New user registration | Default |
| `MYSQL_USER` | App DB user | | Admin login | High |
| `MYSQL_PASSWORD` | App DB password | | User account disabled | Default |
| `DATABASE_URL` | SQLAlchemy async DSN (`mysql+aiomysql://user:pass@db:3306/dbname`) | | User account deleted | Default |
| `SECRET_KEY` | JWT signing secret — keep long and random |
| `ACCESS_TOKEN_EXPIRE_MINUTES` | JWT TTL in minutes (default: `480`) | ## Security
| `ADMIN_USERNAME` | Admin account email, seeded on every container start |
| `ADMIN_PASSWORD` | Admin account password, re-synced on every container start | - 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 ## Data Model