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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user