* feat(dev): add LOCAL_DEV_MODE for one-command local dev
When LOCAL_DEV_MODE=1, every protected route auto-authenticates as a seeded
admin user (default dev@localhost) — no login screen, no Google OAuth config,
no magic-link roundtrip. Startup logs a loud warning to make misuse obvious.
Also fixes two preexisting bugs in the magic-link flow that surfaced while
wiring up the dev fallback:
- /auth/email/verify only accepted POST, but the URL embedded in emails is
a GET link — clicking from any mail client returned 405. Added a GET
variant that consumes the token, sets the auth cookie, and redirects to
/dashboard.
- Token expiry check compared an offset-aware datetime.now(timezone.utc)
against an offset-naive value from DuckDB, raising TypeError on every
valid link. Normalize the stored timestamp to UTC before subtracting.
Dev-only fallback (scoped strictly to LOCAL_DEV_MODE to keep test and
production behavior identical): send-link logs the magic link to stderr
and returns it as dev_link in the JSON response when no SMTP is configured.
Usage:
./scripts/run-local-dev.sh
open http://localhost:8000 # lands on /dashboard as admin
* fix(dev): URL-encode magic-link email + avoid /login redirect loop
Two issues surfaced by Devin review on PR #32.
1. _build_magic_link interpolated email into the URL unescaped. For addresses
with '+' (e.g. user+tag@gmail.com) Starlette's query parser decoded '+'
as a space on the GET /verify side, so repo.get_by_email returned None
and every click yielded 401 "Invalid link". quote(email, safe='') fixes
both the email transport and the dev_link fallback.
2. /login in LOCAL_DEV_MODE unconditionally redirected to /dashboard. If
dev-user seeding failed at startup (main.py wraps seed in try/except),
/dashboard 401'd, the HTML redirect handler bounced to /login, and the
loop repeated until the browser aborted. Now /login checks the dev user
actually exists before short-circuiting; otherwise it falls through to
the normal login form so the missing seed is visible.