Update README: auth, security, env vars, project structure
- Fix authentication section: JWT now in HttpOnly cookie, not localStorage - Fix API section: session cookie auth, no Authorization header needed - Add SECURE_COOKIES and ALLOWED_ORIGINS to setup instructions - Add Security section documenting headers, CORS, rate limiting, etc. - Document server-side impersonation (no admin token in browser) - Add logout endpoint to API table - Update project structure with login.js - Note HTTPS reverse proxy requirement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
73
README.md
73
README.md
@@ -29,6 +29,7 @@ A self-hosted, multi-user web app for backyard chicken keepers to track egg prod
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Docker and Docker Compose
|
- Docker and Docker Compose
|
||||||
|
- A reverse proxy with HTTPS (e.g. Nginx Proxy Manager) — required for secure cookie authentication
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
@@ -38,36 +39,40 @@ A self-hosted, multi-user web app for backyard chicken keepers to track egg prod
|
|||||||
cd yolkbook
|
cd yolkbook
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create a `.env` file in the project root:
|
2. Copy `.env.example` to `.env` and fill in your values:
|
||||||
```env
|
```bash
|
||||||
# MySQL
|
cp .env.example .env
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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:
|
3. Start the stack:
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
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://<your-domain>/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.
|
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
|
## 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.
|
- **Login / Register** — the landing page (`/login`) has both a sign-in form and a self-registration link.
|
||||||
- **JWT tokens** — stored in `localStorage`, valid for 30 days.
|
- **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.
|
||||||
- **Admin password** — always sourced from the `ADMIN_PASSWORD` env var. Changing it in `.env` and restarting will update the admin's password.
|
- **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
|
## Admin Panel
|
||||||
|
|
||||||
@@ -80,7 +85,7 @@ Accessible at `/admin` for admin accounts. Features:
|
|||||||
| Delete | Permanently remove a user and all their data |
|
| Delete | Permanently remove a user and all their data |
|
||||||
| Login As | Impersonate a user to view or edit their data directly |
|
| 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
|
## 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.
|
- **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).
|
- **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)
|
## Migrating an Existing Install (pre-multi-user)
|
||||||
|
|
||||||
If you have an existing single-user install, run the migration script before rebuilding:
|
If you have an existing single-user install, run the migration script before rebuilding:
|
||||||
@@ -108,8 +128,8 @@ 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`.
|
The FastAPI backend is available at `/api`. Interactive docs (Swagger UI) are at `/api/docs`.
|
||||||
|
|
||||||
| Prefix | Description |
|
| Prefix | Description |
|
||||||
|------------------|------------------------------------------|
|
|------------------|----------------------------------------------------|
|
||||||
| `/api/auth` | Login, register, change password, timezone |
|
| `/api/auth` | Login, logout, register, change password, timezone |
|
||||||
| `/api/admin` | User management (admin only) |
|
| `/api/admin` | User management (admin only) |
|
||||||
| `/api/eggs` | Egg collection records |
|
| `/api/eggs` | Egg collection records |
|
||||||
| `/api/flock` | Flock size history |
|
| `/api/flock` | Flock size history |
|
||||||
@@ -117,7 +137,7 @@ The FastAPI backend is available at `/api`. Interactive docs (Swagger UI) are at
|
|||||||
| `/api/other` | Other purchases (bedding, snacks, etc.) |
|
| `/api/other` | Other purchases (bedding, snacks, etc.) |
|
||||||
| `/api/stats` | Dashboard, budget, and monthly summary |
|
| `/api/stats` | Dashboard, budget, and monthly summary |
|
||||||
|
|
||||||
All data endpoints require a valid JWT (`Authorization: Bearer <token>`). 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
|
## Project Structure
|
||||||
|
|
||||||
@@ -125,13 +145,13 @@ All data endpoints require a valid JWT (`Authorization: Bearer <token>`). Data i
|
|||||||
yolkbook/
|
yolkbook/
|
||||||
├── backend/
|
├── backend/
|
||||||
│ ├── main.py # FastAPI app entry point + startup seeding
|
│ ├── 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
|
│ ├── models.py # SQLAlchemy models
|
||||||
│ ├── schemas.py # Pydantic schemas
|
│ ├── schemas.py # Pydantic schemas
|
||||||
│ ├── database.py # DB connection
|
│ ├── database.py # DB connection
|
||||||
│ ├── routers/
|
│ ├── routers/
|
||||||
│ │ ├── auth_router.py # /api/auth — login, register, settings
|
│ │ ├── auth_router.py # /api/auth — login, logout, register, settings
|
||||||
│ │ ├── admin.py # /api/admin — user management
|
│ │ ├── admin.py # /api/admin — user management + impersonation
|
||||||
│ │ ├── eggs.py
|
│ │ ├── eggs.py
|
||||||
│ │ ├── flock.py
|
│ │ ├── flock.py
|
||||||
│ │ ├── feed.py
|
│ │ ├── feed.py
|
||||||
@@ -148,6 +168,8 @@ yolkbook/
|
|||||||
│ │ ├── js/
|
│ │ ├── js/
|
||||||
│ │ │ ├── api.js # Shared fetch helpers
|
│ │ │ ├── api.js # Shared fetch helpers
|
||||||
│ │ │ ├── auth.js # Auth utilities, nav, settings modal
|
│ │ │ ├── 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)
|
│ │ │ ├── history.js # Collection history table (used on log page)
|
||||||
│ │ │ └── dashboard.js
|
│ │ │ └── dashboard.js
|
||||||
│ │ └── css/style.css
|
│ │ └── css/style.css
|
||||||
@@ -155,6 +177,7 @@ yolkbook/
|
|||||||
├── mysql/
|
├── mysql/
|
||||||
│ ├── init.sql # Schema for fresh installs
|
│ ├── init.sql # Schema for fresh installs
|
||||||
│ └── migrate_v2.sql # Migration for pre-multi-user installs
|
│ └── migrate_v2.sql # Migration for pre-multi-user installs
|
||||||
|
├── .env.example # Template — copy to .env and fill in values
|
||||||
├── docker-compose.yml
|
├── docker-compose.yml
|
||||||
└── .env # Secrets — not committed
|
└── .env # Secrets — not committed
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user