Release 0.7.0: email/OTC sign-in (Gitea OAuth retained as fallback)
Replaces the Gitea OAuth gesture as the primary human-auth path (roadmap item #5, SPEC §6.2). Users sign in by entering their email, receiving a six-digit code via the existing SMTP layer, and entering the code on a two-step /login surface. The Gitea OAuth callback remains functional during migration — the new UI links to it as a fallback for users with active OAuth sessions or older invite paths — and is scheduled for removal in a future release once OTC adoption is universal. Existing users are linked by email on first OTC sign- in (gitea_id preserved); new users are provisioned with NULL gitea_id and rely on email as the identity key. The migration introduces backend/migrations/012_otc.sql (otc_codes table + users schema rebuild for nullable gitea_id and a partial unique index on email), two new endpoints (POST /auth/otc/request, POST /auth/otc/verify), bcrypt as a new backend dependency for code hashing, and 11 new tests in test_otc_vertical.py covering the happy path, expired and consumed and wrong codes, the per-email rate limit, the allowlist gate, the OAuth-era link path, fresh provisioning, and prior-code invalidation on re-request. No new secrets are required — the existing SECRET_KEY signs sessions and bcrypt's per-row salt covers the code hashes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+135
@@ -23,6 +23,141 @@ skip versions are the composition of each intervening adjacent
|
||||
release's steps in order — no A-to-B path is pre-computed beyond
|
||||
that.
|
||||
|
||||
## 0.7.0 — 2026-05-28
|
||||
|
||||
**Minor — schema migration required; new auth path is additive.**
|
||||
This release lands email + one-time-code sign-in (roadmap item #5,
|
||||
SPEC §6.2) as the primary human-auth gesture. Users sign in by
|
||||
typing their email, receiving a six-digit code via email, and
|
||||
entering it. The Gitea OAuth callback (`/auth/callback`) remains
|
||||
functional during migration — the new UI no longer points at it
|
||||
primarily, but a "Sign in with Gitea (fallback)" link survives on
|
||||
the new login surface so users with active OAuth sessions or older
|
||||
invite paths still have a way in. A future release retires the
|
||||
OAuth path entirely once every active user has signed in at least
|
||||
once via OTC.
|
||||
|
||||
The migration path for existing users: first OTC sign-in matches by
|
||||
`users.email` (case-insensitive) to the OAuth-era row and reuses
|
||||
that row's `id` and `gitea_id`. New users provisioned via OTC carry
|
||||
`gitea_id = NULL` and `gitea_login = NULL`. The `gitea_id` linker
|
||||
remains the canonical handle for grandfathered users; `email`
|
||||
becomes the identity key for everything provisioned after v0.7.0.
|
||||
|
||||
The Gitea **bot** user + token are still required (server-side git
|
||||
operations — repo reads, PR creation — still flow through it). Only
|
||||
the operator-facing sign-in surface moves.
|
||||
|
||||
### Upgrade steps (from 0.6.0)
|
||||
|
||||
1. **MUST** restart the backend so migration `012_otc.sql` runs.
|
||||
The migration rebuilds the `users` table (SQLite cannot ALTER
|
||||
COLUMN); existing rows pass through unchanged, but the new
|
||||
schema relaxes `gitea_id` / `gitea_login` to nullable (with
|
||||
partial unique indexes that ignore NULL) and adds a partial
|
||||
unique index on `email`. A new `otc_codes` table is created.
|
||||
2. **MUST** confirm the SMTP overlay is set (`SMTP_HOST`,
|
||||
`SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_STARTTLS`,
|
||||
`EMAIL_FROM`, `EMAIL_FROM_NAME`). The OHM overlay already
|
||||
carries these as of v0.5.0; deployments without them fall back
|
||||
to logging the code to stdout (dev-only path — production
|
||||
visitors will not receive their codes).
|
||||
3. **SHOULD** announce the new email-based sign-in to existing
|
||||
users. Wording suggestion: "You can now sign in by entering
|
||||
your email and a one-time code we'll send you. Your old
|
||||
account is linked automatically the first time you sign in."
|
||||
4. **MAY** keep the existing OAuth callback as a fallback path.
|
||||
The new login UI surfaces a small "Sign in with Gitea
|
||||
(fallback)" link beneath the primary email/code form; a
|
||||
deployment that prefers to hide it can override the Login
|
||||
component in a future framework release that exposes the link
|
||||
behind a feature flag. For v0.7.0, the link is hard-coded.
|
||||
|
||||
### New environment variables (all optional with defaults)
|
||||
|
||||
- `OTC_TTL_MINUTES` (default `10`) — how long a one-time code is
|
||||
valid after issuance. Re-requesting invalidates the prior code
|
||||
immediately regardless of TTL.
|
||||
- `OTC_REQUEST_COOLDOWN_SECONDS` (default `60`) — per-email cooldown
|
||||
between successive `/auth/otc/request` calls. The endpoint returns
|
||||
HTTP 429 when the cooldown blocks a request (the loud-failure
|
||||
shape; the abuse path is visible rather than swallowed).
|
||||
|
||||
No new secrets are required. The existing `SECRET_KEY` continues to
|
||||
sign session cookies; OTC codes are bcrypt-hashed at rest using a
|
||||
per-row salt the library generates.
|
||||
|
||||
### Added
|
||||
|
||||
- **`POST /auth/otc/request`** — body `{email}`. Generates a six-digit
|
||||
code, stores its bcrypt hash with an expiry, and dispatches a plain
|
||||
text email via the existing SMTP layer. Returns HTTP 200 (`{ok:true}`)
|
||||
uniformly so allowlist state is not leaked. Returns HTTP 429 when
|
||||
the per-email cooldown blocks the request.
|
||||
- **`POST /auth/otc/verify`** — body `{email, code}`. Validates the
|
||||
bcrypt hash against the most-recent unconsumed non-expired row, marks
|
||||
the row consumed, provisions or links the `users` row by email, and
|
||||
stores the session cookie. Returns HTTP 200 on success, HTTP 400 on
|
||||
any failure (expired, consumed, wrong, unknown).
|
||||
- **`backend/migrations/012_otc.sql`** — creates `otc_codes` and
|
||||
rebuilds `users` with nullable `gitea_id` / `gitea_login` plus a
|
||||
partial unique index on `email`.
|
||||
- **`backend/app/otc.py`** — the OTC request/verify state machine and
|
||||
the `provision_or_link_user` linker.
|
||||
- **`backend/app/email_otc.py`** — outbound OTC mail composition. Reuses
|
||||
the SMTP plumbing from `email.py` (`EmailConfig.from_env()`) and the
|
||||
test buffer (`_SENT`) but writes its own envelope (no unsubscribe
|
||||
footer, no quiet-hours hold — OTC mail carries a credential and
|
||||
ignores notification preferences).
|
||||
- **`frontend/src/components/Login.jsx`** — two-step sign-in surface
|
||||
at `/login`. Step 1: enter email → request code. Step 2: enter
|
||||
six-digit code → verify. Cmd/Ctrl+Enter on the code field submits.
|
||||
The previous header "Sign in" link and the `Welcome` component's
|
||||
inline link now route to `/login` instead of jumping straight to
|
||||
the Gitea OAuth dance.
|
||||
- **SPEC `§6.1` / `§6.2` / `§14.1` / `§17` / `§19.2`** corrections per
|
||||
§19.3 rule-2 — see below.
|
||||
- **`backend/tests/test_otc_vertical.py`** — 11 new tests covering
|
||||
the happy path, expired/consumed/wrong codes, the per-email rate
|
||||
limit (and its per-email isolation), the allowlist gate, the
|
||||
migration link to OAuth-era users, fresh provisioning, and the
|
||||
prior-code-invalidation behavior on re-request.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`backend/app/auth.py#current_user`** — coerces NULL `gitea_id` and
|
||||
NULL `gitea_login` to `0` / `""` so the `SessionUser` shape stays
|
||||
stable for OTC-only users. The DB remains the source of truth for
|
||||
"is this user OAuth-linked" (`gitea_id IS NOT NULL`).
|
||||
- **`backend/requirements.txt`** — adds `bcrypt>=4.2` for OTC code
|
||||
hashing. Pure-Python wheels are available on every platform the
|
||||
deployment matrix targets; `pip install -r backend/requirements.txt`
|
||||
picks it up.
|
||||
- **`frontend/src/App.jsx`** — adds the `/login` route and replaces
|
||||
the header "Sign in" `<a href="/auth/login">` with `<Link to="/login">`.
|
||||
The `Welcome` component's inline sign-in link follows suit.
|
||||
- **`frontend/src/components/Landing.jsx`** — the `/welcome` page's
|
||||
primary action moves from "Sign in with Gitea" to "Sign in" pointing
|
||||
at `/login`.
|
||||
|
||||
### Deferred to later releases
|
||||
|
||||
Per the v0.7.0 scope discipline (the foundation for items #6, #8, #9,
|
||||
#10), several adjacent capabilities are intentionally not in this
|
||||
release and surface as §19.2 candidates:
|
||||
|
||||
- **First-OTC profile capture** (first name, last name, "why") — item
|
||||
#6, expected v0.8.0.
|
||||
- **Open beta-access request flow** replacing the allowlist gate —
|
||||
also item #6, v0.8.0.
|
||||
- **Passcodes** (a long-term reauth token alternative) — item #8,
|
||||
expected v0.10.0.
|
||||
- **Device-trust 30-day skip** — item #9, expected v0.11.0.
|
||||
- **Cloudflare Turnstile** on `/auth/otc/request` — item #10,
|
||||
expected v0.12.0.
|
||||
- **Removing the Gitea OAuth `/auth/callback` route entirely** — a
|
||||
later release after every active user has signed in via OTC.
|
||||
|
||||
## 0.5.0 — 2026-05-27
|
||||
|
||||
**Minor — no operator action required.** This release wires the
|
||||
|
||||
Reference in New Issue
Block a user