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
- **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 <token>` 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