# 🥃 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 | Authenticated routes expect `Authorization: Bearer ` header. ### Entry Schema ```json { "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 ```bash cp .env.example .env # Edit .env — set real passwords and a secure SECRET_KEY ``` Generate a secure secret key: ```bash python3 -c "import secrets; print(secrets.token_hex(32))" ``` ### Run ```bash 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 ```bash 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` | ## 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`