3 Commits

Author SHA1 Message Date
umbra audio
e35d7a144f Add Apache-2.0 LICENSE, NOTICE, and SPDX headers 2026-10-18 19:01:05 -07:00
umbra audio
17f8e81fb7 Verify the mail transport instead of trusting its configuration
Gmail rejects an account password for SMTP and wants an app password
instead, which it says in a reply nobody reads. Configured with
smtp.gmail.com:587, a port, and a password, the deployment looked healthy:
`_mail_is_deliverable` asked whether the transport was smtp and a host was
set, both true, so /api/health reported mail_deliverable=true and the
control panel allowed account creation.

Every account created that way would have been stranded in `invited` with a
confirmation link that was never delivered and cannot be re-sent, and the
administrator would have seen a success response each time. That is a data
problem created by a configuration problem, invisible until somebody tried
to sign in.

So the transports report whether they can actually deliver:

- `Mailer.verify()` on all three, sharing the connection path with `send` so
  a probe cannot succeed by a route real delivery would not take.
- SmtpMailer authenticates and disconnects, and logs the server's own
  rejection code, because "wrong password" and "server unreachable" are
  different problems and a bare False throws that away. The reply contains
  no credential.
- health reports the verified state, not the configured one.

And enrollment refuses when mail cannot be delivered, in the service
rather than the browser. The control panel already disabled its own
buttons, but a client-side check is a courtesy, not a control: anything
reaching the process would still have created the account. Exempt is the
operator path, which attests the address instead of mailing it, because
guarding that would deadlock first-run setup behind SMTP configuration,
and development, where the logging transport is the point and the flow
would otherwise be untestable.

Verified against smtp.gmail.com on both 587 (STARTTLS) and 465 (implicit
TLS): reachable, TLS established, AUTH offered, and 535 5.7.8 "Username and
Password not accepted" on both. The network and the server are right; the
credential is not.

286 tests pass without a database, 295 with one.
2026-09-27 06:49:23 -07:00
umbra audio
0461dff9d2 Use the email address as the login identifier and require confirmation
Sign-in now takes the email address rather than a username. This needed no
migration: users.email is already citext NOT NULL UNIQUE in the control plane,
so it is a case-insensitively unique login key on its own. username survives as
an optional display handle for audit lines and is never a login path, which
removes the redundancy of asking an administrator for two identifiers.

An account created by an administrator now starts in the control plane's
invited state and cannot sign in until a single-use link is consumed. The token
maps onto bootstrap_tokens, which was already shaped for exactly this and had
been unused, so that needed no migration either. Modelling invited as a real
state also retires the adapter's earlier fail-closed refusal of that value,
which existed because invited was previously unrepresentable.

Three decisions worth stating plainly.

The "email not verified" message is sent only after the password has been
verified. Reporting it earlier would turn the login form into an oracle for
which addresses have accounts, which is the one thing the uniform
authentication error exists to prevent. A test asserts that a wrong password and
an unknown account are byte-identical, and it caught a real bug: the check was
first written before the password comparison, and the generic "not active"
branch swallowed invited accounts. Both are now ordered deliberately.

The operator bootstrap path does not require verification. The operator is
attesting to the address themselves, which is the standing an administrator
already has, and requiring a delivered email there would deadlock first-time
setup on SMTP configuration that does not exist yet.

The logging mail transport is refused in production. A deployment that logged
links instead of sending them would appear to work while every new account sat
permanently unverified.

The development transport writes the link with the token redacted, because a log
is where live credentials end up. That means a human cannot complete the flow by
reading the log; the test harness captures the message in memory instead. This
is a deliberate limitation rather than an oversight, and it is documented.

Also: production now requires a mail transport and an absolute verification base
URL alongside the existing DSN gate, because an account that cannot be verified
cannot be used at all.

269 tests pass, 3 skipped. Verified end to end against a real uvicorn server:
bootstrap login, organization creation, an invited account with its one-time
password, and a handle rejected as a login path.
2026-09-26 21:21:01 -07:00