diff --git a/README.md b/README.md index 35b7dbb..d9e0639 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ A self-hosted, multi-user web app for backyard chicken keepers to track egg prod ### Prerequisites - Docker and Docker Compose +- A reverse proxy with HTTPS (e.g. Nginx Proxy Manager) — required for secure cookie authentication ### Setup @@ -38,36 +39,40 @@ A self-hosted, multi-user web app for backyard chicken keepers to track egg prod cd yolkbook ``` -2. Create a `.env` file in the project root: - ```env - # MySQL - MYSQL_ROOT_PASSWORD=your_root_password - MYSQL_DATABASE=eggtracker - MYSQL_USER=eggtracker - MYSQL_PASSWORD=your_db_password - - # Super admin account (created/synced automatically on every startup) - ADMIN_USERNAME=admin - ADMIN_PASSWORD=your_admin_password - - # JWT signing secret — generate with: openssl rand -hex 32 - JWT_SECRET=your_long_random_secret +2. Copy `.env.example` to `.env` and fill in your values: + ```bash + cp .env.example .env ``` + Key variables: + + | Variable | Description | + |----------|-------------| + | `MYSQL_ROOT_PASSWORD` | MySQL root password — generate with `openssl rand -hex 16` | + | `MYSQL_PASSWORD` | MySQL app user password | + | `ADMIN_USERNAME` | Username for the built-in admin account | + | `ADMIN_PASSWORD` | Password for the built-in admin account | + | `JWT_SECRET` | JWT signing secret — generate with `openssl rand -hex 32` | + | `SECURE_COOKIES` | Set to `true` (default) when behind HTTPS; `false` for local HTTP testing only | + | `ALLOWED_ORIGINS` | Comma-separated list of external origins allowed to call the API. Leave empty if accessed only through the bundled nginx frontend. | + 3. Start the stack: ```bash docker compose up -d --build ``` -4. Open your browser at `http://localhost:8056/login` and sign in with the admin credentials you set in `.env`. +4. Point your reverse proxy at port `8056` with HTTPS enabled. + +5. Open your browser at `https:///login` and sign in with the admin credentials you set in `.env`. The database schema is applied automatically on first start via `mysql/init.sql`. The admin user is created (or synced) automatically every time the API starts. ## Authentication -- **Login / Register** — the landing page (`/login`) has both a sign-in form and a self-registration link so users can create their own accounts. -- **JWT tokens** — stored in `localStorage`, valid for 30 days. -- **Admin password** — always sourced from the `ADMIN_PASSWORD` env var. Changing it in `.env` and restarting will update the admin's password. +- **Login / Register** — the landing page (`/login`) has both a sign-in form and a self-registration link. +- **Sessions** — after login, a session cookie is set by the server. It is `HttpOnly` (inaccessible to JavaScript), `Secure` (HTTPS only), and `SameSite=Strict` (CSRF protection). Valid for 30 days. +- **Logout** — the `/api/auth/logout` endpoint clears the session cookie server-side. +- **Admin password** — always sourced from the `ADMIN_PASSWORD` env var. Changing it in `.env` and restarting updates the admin's password automatically. ## Admin Panel @@ -80,7 +85,7 @@ Accessible at `/admin` for admin accounts. Features: | Delete | Permanently remove a user and all their data | | Login As | Impersonate a user to view or edit their data directly | -When impersonating a user, an amber banner appears in the nav with a **Return to Admin** button. +When impersonating a user, an amber banner appears in the nav with a **Return to Admin** button. The original admin session is preserved server-side — no admin credentials are stored in the browser during impersonation. ## User Settings @@ -89,6 +94,21 @@ The gear icon (⚙) in the top-right nav opens the Settings panel: - **Timezone** — choose from a full list of IANA timezones or click *Detect automatically*. Affects what "today" is when pre-filling date fields and the 30-day/7-day windows on the dashboard and budget pages. - **Change Password** — update your own password (requires current password). +## Security + +| Feature | Details | +|---------|---------| +| Authentication | HttpOnly + Secure + SameSite=Strict session cookie; no token in JS-accessible storage | +| CSRF protection | SameSite=Strict cookie prevents cross-site request forgery without explicit tokens | +| Password hashing | bcrypt | +| CORS | Locked to same origin by default; configurable via `ALLOWED_ORIGINS` | +| Rate limiting | Login: 5 req/min · Register: 3 req/min · Admin endpoints: 10 req/min | +| Security headers | X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy, Content-Security-Policy | +| Subresource Integrity | Chart.js CDN script pinned with SHA-384 hash | +| Input validation | Server-side via Pydantic; all user-rendered content HTML-escaped | +| SQL injection | SQLAlchemy ORM with parameterized queries throughout | +| Container security | API runs as non-root user; all volume mounts read-only except database | + ## Migrating an Existing Install (pre-multi-user) If you have an existing single-user install, run the migration script before rebuilding: @@ -107,17 +127,17 @@ All existing data will be automatically assigned to the admin account on first s The FastAPI backend is available at `/api`. Interactive docs (Swagger UI) are at `/api/docs`. -| Prefix | Description | -|------------------|------------------------------------------| -| `/api/auth` | Login, register, change password, timezone | -| `/api/admin` | User management (admin only) | -| `/api/eggs` | Egg collection records | -| `/api/flock` | Flock size history | -| `/api/feed` | Feed purchase records | -| `/api/other` | Other purchases (bedding, snacks, etc.) | -| `/api/stats` | Dashboard, budget, and monthly summary | +| Prefix | Description | +|------------------|----------------------------------------------------| +| `/api/auth` | Login, logout, register, change password, timezone | +| `/api/admin` | User management (admin only) | +| `/api/eggs` | Egg collection records | +| `/api/flock` | Flock size history | +| `/api/feed` | Feed purchase records | +| `/api/other` | Other purchases (bedding, snacks, etc.) | +| `/api/stats` | Dashboard, budget, and monthly summary | -All data endpoints require a valid JWT (`Authorization: Bearer `). Data is always scoped to the authenticated user. +All data endpoints require an authenticated session cookie. Requests are automatically authenticated by the browser — no `Authorization` header is needed. ## Project Structure @@ -125,13 +145,13 @@ All data endpoints require a valid JWT (`Authorization: Bearer `). Data i yolkbook/ ├── backend/ │ ├── main.py # FastAPI app entry point + startup seeding -│ ├── auth.py # JWT utilities, password hashing, auth dependencies +│ ├── auth.py # JWT utilities, password hashing, cookie helpers, auth dependencies │ ├── models.py # SQLAlchemy models │ ├── schemas.py # Pydantic schemas │ ├── database.py # DB connection │ ├── routers/ -│ │ ├── auth_router.py # /api/auth — login, register, settings -│ │ ├── admin.py # /api/admin — user management +│ │ ├── auth_router.py # /api/auth — login, logout, register, settings +│ │ ├── admin.py # /api/admin — user management + impersonation │ │ ├── eggs.py │ │ ├── flock.py │ │ ├── feed.py @@ -148,6 +168,8 @@ yolkbook/ │ │ ├── js/ │ │ │ ├── api.js # Shared fetch helpers │ │ │ ├── auth.js # Auth utilities, nav, settings modal +│ │ │ ├── login.js # Login/register page logic +│ │ │ ├── admin.js # Admin panel │ │ │ ├── history.js # Collection history table (used on log page) │ │ │ └── dashboard.js │ │ └── css/style.css @@ -155,6 +177,7 @@ yolkbook/ ├── mysql/ │ ├── init.sql # Schema for fresh installs │ └── migrate_v2.sql # Migration for pre-multi-user installs +├── .env.example # Template — copy to .env and fill in values ├── docker-compose.yml └── .env # Secrets — not committed ```