5.7 KiB
5.7 KiB
🥃 Bourbonacci
A personal bourbon tracking web app. Log pours, track your collection, and view stats across users. Built with FastAPI, vanilla JS, and MySQL — fully containerized with Docker Compose.
Stack
| Layer | Technology |
|---|---|
| Backend | FastAPI (Python 3.12), async SQLAlchemy 2.0, Uvicorn |
| Database | MySQL 8 |
| Frontend | Vanilla HTML/CSS/JS (no framework) |
| Reverse Proxy | Nginx (Alpine) |
| Auth | JWT via python-jose, passwords hashed with passlib[bcrypt] |
| Container | Docker Compose |
Project Structure
bourbonacci/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI app entry point, lifespan (DB init)
│ │ ├── config.py # Pydantic settings
│ │ ├── database.py # Async engine, session factory, Base
│ │ ├── dependencies.py # get_db, get_current_user
│ │ ├── models/
│ │ │ ├── user.py # User ORM model
│ │ │ └── entry.py # Entry ORM model (add/remove types)
│ │ ├── routers/
│ │ │ ├── auth.py # POST /api/auth/register, /api/auth/login
│ │ │ ├── users.py # GET/PATCH /api/users/me
│ │ │ ├── entries.py # CRUD /api/entries, GET /api/entries/stats
│ │ │ └── public.py # GET /api/public/stats (unauthenticated)
│ │ ├── schemas/
│ │ │ ├── user.py # Pydantic schemas for user/auth
│ │ │ └── entry.py # Pydantic schemas for entries/stats
│ │ └── utils/
│ │ └── security.py # JWT creation, password hashing/verification
│ ├── Dockerfile
│ └── requirements.txt
├── frontend/
│ ├── index.html # Landing / public stats page
│ ├── login.html
│ ├── register.html
│ ├── dashboard.html # Authenticated home with stats
│ ├── log.html # Log a new entry
│ ├── profile.html # User profile / settings
│ ├── css/style.css
│ └── js/
│ ├── api.js # Fetch wrapper, base URL, token handling
│ └── auth.js # Login/register/logout logic
├── nginx/
│ └── default.conf # Serves frontend, proxies /api/ to backend:8000
├── docker-compose.yml
└── .env.example
API Endpoints
Auth
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
No | Register, returns JWT |
| POST | /api/auth/login |
No | Login, returns JWT |
Users
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/users/me |
Yes | Get current user profile |
| PATCH | /api/users/me |
Yes | Update display name / timezone |
Entries
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/entries |
Yes | List all entries for current user |
| 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 | Aggregated stats for all users |
Admin
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/admin/users |
Admin | List all users |
| POST | /api/admin/users |
Admin | Create a user |
| POST | /api/admin/users/{id}/reset-password |
Admin | Force-reset a user's password |
| POST | /api/admin/users/{id}/disable |
Admin | Disable a user account |
| POST | /api/admin/users/{id}/enable |
Admin | Re-enable a user account |
| DELETE | /api/admin/users/{id} |
Admin | Hard-delete a user |
| POST | /api/admin/users/{id}/impersonate |
Admin | Get a token scoped as that user |
| POST | /api/admin/unimpersonate |
Admin | Swap back to the admin token |
Authenticated routes expect Authorization: Bearer <token> header.
Entry Schema
{
"entry_type": "add" | "remove",
"date": "YYYY-MM-DD",
"bourbon_name": "string (required for add)",
"proof": 90.0,
"amount_shots": 1.0,
"notes": "optional string"
}
Stats return current_total_shots (adds minus removes) and estimated_proof (weighted average across add entries).
Getting Started
Prerequisites
- Docker + Docker Compose
Setup
cp .env.example .env
# Edit .env — set real passwords and a secure SECRET_KEY
Generate a secure secret key:
python3 -c "import secrets; print(secrets.token_hex(32))"
Run
docker compose up -d
The app will be available at http://localhost:8057.
The backend waits for MySQL to pass its healthcheck before starting. Tables are created automatically on first boot via SQLAlchemy's init_db().
Stop
docker compose down
# To also remove the database volume:
docker compose down -v
Environment Variables
| Variable | Description | Default |
|---|---|---|
MYSQL_ROOT_PASSWORD |
MySQL root password | — |
MYSQL_DATABASE |
Database name | bourbonacci |
MYSQL_USER |
App DB user | bourbonacci |
MYSQL_PASSWORD |
App DB password | — |
DATABASE_URL |
SQLAlchemy async DSN | mysql+aiomysql://... |
SECRET_KEY |
JWT signing secret (keep long & random) | — |
ACCESS_TOKEN_EXPIRE_MINUTES |
JWT TTL in minutes | 480 |
ADMIN_USERNAME |
Admin account email (seeded on every start) | — |
ADMIN_PASSWORD |
Admin account password (re-synced on every start) | — |
Data Model
users
id,email(unique),password_hash,display_name,timezone,created_at
entries
id,user_id(FK),entry_type(add/remove),date,bourbon_name,proof,amount_shots,notes,created_at