diff --git a/AdventureLog-Setup-Summary.md b/AdventureLog-Setup-Summary.md index 9215d81..d613905 100644 --- a/AdventureLog-Setup-Summary.md +++ b/AdventureLog-Setup-Summary.md @@ -20,74 +20,90 @@ Live at: **https://go.jgitta.com** --- -## Caddy Routing (Critical — Non-Obvious) +## Caddy Routing (Corrected 2026-07-02 — now matches official docs) -AdventureLog has a SvelteKit API proxy that deletes the `csrftoken` cookie from the browser on every request, making direct browser→Django API calls impossible. The routing is split: +AdventureLog authenticates through the SvelteKit frontend: on login the frontend keeps the +Django session **server-side** and does NOT hand the browser a Django `sessionid` cookie (it even +clears `csrftoken` in the browser). Therefore **all `/api/*` and `/auth/*` traffic must go through +the frontend**, which proxies to Django server-to-server with the correct session + CSRF token. +Only static-type paths go straight to Django. + +This is the official AdventureLog Caddy design (https://adventurelog.app/docs/install/caddy.html), +adapted for our remote Caddy VM (proxying to LAN ports 8015/8016) plus our standard security headers: ```caddy go.jgitta.com { - # GET/HEAD/OPTIONS /api/* → Django directly (port 8016) - # Browser sends session cookie; no CSRF needed for reads. - # Accept: application/json header forces JSON instead of DRF HTML. - @api_read { method GET HEAD OPTIONS; path /api/* } - handle @api_read { - reverse_proxy http://192.168.88.27:8016 { - header_up Accept "application/json" - } + import headers_base + encode zstd gzip + header { + X-Frame-Options "SAMEORIGIN" + Permissions-Policy "geolocation=(self), microphone=(), camera=()" } - # All other requests (pages + POST/PUT/DELETE /api/*) → SvelteKit (port 8015) - # SvelteKit proxy fetches fresh CSRF token server-to-server and handles auth. - @frontend { not path /media* /admin* /static* /accounts* } - handle @frontend { reverse_proxy http://192.168.88.27:8015 } + # Backend (Django): media, admin, static files, legacy account pages + @backend path /media* /admin* /static* /accounts* + handle @backend { + reverse_proxy http://192.168.88.27:8016 { import proxy_timeouts } + } - # Django: media, admin, static, auth endpoints - handle { reverse_proxy http://192.168.88.27:8016 } + # Everything else (pages, /api/*, /auth/*) -> SvelteKit frontend, + # which proxies API/auth to Django server-side with session + CSRF. + handle { + reverse_proxy http://192.168.88.27:8015 { import proxy_timeouts } + } } ``` -**Why this matters:** POST/PUT/DELETE must go through SvelteKit (port 8015 → gunicorn :8000). These requests will NOT appear in the Django nginx access log (`/var/log/nginx/access.log` inside the backend container). Only GET requests via Caddy appear there. +**`geolocation=(self)`** (inline, not the `web_secure` snippet) is required so the "Use My Location" +button works. + +### History — why the old split routing was removed +Earlier the config split `/api/*` by HTTP method: GET/HEAD/OPTIONS went **directly to Django** and +only writes went through the frontend. That was a workaround for a geocode 401 seen during initial +setup. After a frontend container auto-update (`:latest`) on 2026-07-02, the frontend stopped +exposing a Django session to the browser, so the "GET direct to Django" path could no longer +authenticate — every authenticated read (including **geocoding / map search**) returned +`401 "Authentication credentials were not provided."` Reverting to the official all-`/api`-through- +frontend routing fixed it. Lesson: don't hand-split `/api`; let the frontend proxy it. --- ## Portainer Environment Variables -All 17 env vars are stored in Portainer's environment section (not hardcoded in compose). Key ones: +All env vars are stored in Portainer's environment section (not hardcoded in compose). Key ones: | Variable | Value | |----------|-------| +| PUBLIC_SERVER_URL | http://server:8000 | | ORIGIN | https://go.jgitta.com | | PUBLIC_URL | https://go.jgitta.com | | CSRF_TRUSTED_ORIGINS | https://go.jgitta.com | | FRONTEND_URL | https://go.jgitta.com | -| PUBLIC_SERVER_URL | http://server:8000 | | FRONTEND_PORT | 8015 | | BACKEND_PORT | 8016 | +| BODY_SIZE_LIMIT | Infinity | | POSTGRES_DB | adventurelog | | POSTGRES_USER | adventure | | DEBUG | False | | DISABLE_REGISTRATION | False | -Secrets (password, secret key, admin credentials) are in Portainer only — not in this file. +Secrets (DB password, SECRET_KEY, admin credentials) are in Portainer only — not in this file. +Note: `DJANGO_ADMIN_PASSWORD` is only the *bootstrap* password used at first container start; the +live admin password has since been changed in-app. + +Optional/recommended (not currently set): `ENABLE_RATE_LIMITS=True` (docs recommend for production). --- ## Bugs Fixed During Setup -### 1. CSRF errors on login -Django's `CSRF_TRUSTED_ORIGINS` must match the public URL exactly. Fixed by setting it in env. - -### 2. GET /api/* returning DRF HTML instead of JSON -Caddy was doing a broken URI rewrite (`{1}` not expanding). Fixed by removing the rewrite and adding `header_up Accept "application/json"`. - -### 3. Geocode search returning 401 -GET requests were going through SvelteKit's proxy, which strips the session cookie on redirect. Fixed by routing GET /api/* directly to Django (see Caddy config above). - -### 4. Category creation returning 500 -AdventureLog has an unhandled `IntegrityError` when a duplicate category slug is created — it returns 500 instead of 400. Workaround: delete duplicate categories via Django shell if this occurs. - -### 5. "Use My Location" button blocked -Caddy's `web_secure` snippet sets `Permissions-Policy: geolocation=()`. Changed the `go.jgitta.com` block to use inline headers with `geolocation=(self)` instead. +1. **CSRF errors on login** — `CSRF_TRUSTED_ORIGINS` must match the public URL exactly. Set in env. +2. **GET /api/* returning DRF HTML instead of JSON** — old Caddy did a broken URI rewrite; removed. +3. **Geocode search 401** — see "Caddy Routing" history above. Final fix: route all `/api/*` + through the SvelteKit frontend (official design), not directly to Django. +4. **Category creation 500** — AdventureLog returns 500 (not 400) on duplicate category slug + (unhandled IntegrityError). Workaround: delete the duplicate via Django shell. +5. **"Use My Location" blocked** — set `Permissions-Policy: geolocation=(self)` on the site block. --- @@ -99,6 +115,8 @@ Caddy's `web_secure` snippet sets `Permissions-Policy: geolocation=()`. Changed --- ## Known Limitations / Not Configured -- No Google Maps API key — geocoding uses OpenStreetMap/Nominatim (works fine) -- No Strava integration -- B2/Kopia backup not configured for this stack +- Images run on `:latest` with Watchtower auto-update — an update on 2026-07-02 changed frontend + auth behavior and broke the old routing. Consider pinning image versions. +- No Google Maps API key — geocoding uses OpenStreetMap/Nominatim (works fine). +- No Strava integration. +- B2/Kopia backup not configured for this stack.