Email infrastructure + forgot-password reset #5

Closed
opened 2026-07-29 22:33:44 +00:00 by Cordy · 1 comment
Owner

The account system needs outbound email — it's the prerequisite for password reset AND for org invites (next issue), and later for delivering keys after purchase.

Email sender — minimal SMTP client (stdlib net/smtp or a thin wrapper), configured via env: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, MAIL_FROM (e.g. no-reply@swisscairn.ch). All templated mails (reset, invite) share one plain-text template style matching the brand voice.

Forgot password

  • POST /v1/password-reset {email} → always 204 (no account-existence oracle). If the account exists: mint a single-use token (32B random, 1h expiry, stored server-side), email a link https://www.swisscairn.ch/reset.html?token=….
  • POST /v1/password-reset/confirm {token, newPassword} → validate token (constant-time, unexpired, unused), set password, burn token, revoke existing sessions.
  • Rate-limit requests per IP and per email (reuse the existing limiter).
  • Site half: "Forgot password?" link on the sign-in tab + a small reset.html page.

Decision needed at greenlight (Nikola): the SMTP provider + credentials. Infomaniak (already hosting the VPS/domain) offers SMTP with the domain — likely path of least resistance; an API-based sender (e.g. a transactional mail service) is the alternative. Credentials go into /etc/cairn-license/env, never git.

Acceptance: reset flow works end-to-end against the real SMTP (received mail → link → new password → old sessions dead); unknown email indistinguishable from known; expired/reused tokens rejected.

The account system needs outbound email — it's the prerequisite for password reset AND for org invites (next issue), and later for delivering keys after purchase. **Email sender** — minimal SMTP client (stdlib `net/smtp` or a thin wrapper), configured via env: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `MAIL_FROM` (e.g. `no-reply@swisscairn.ch`). All templated mails (reset, invite) share one plain-text template style matching the brand voice. **Forgot password** - `POST /v1/password-reset` `{email}` → always 204 (no account-existence oracle). If the account exists: mint a single-use token (32B random, 1h expiry, stored server-side), email a link `https://www.swisscairn.ch/reset.html?token=…`. - `POST /v1/password-reset/confirm` `{token, newPassword}` → validate token (constant-time, unexpired, unused), set password, burn token, revoke existing sessions. - Rate-limit requests per IP and per email (reuse the existing limiter). - Site half: "Forgot password?" link on the sign-in tab + a small `reset.html` page. **Decision needed at greenlight (Nikola):** the SMTP provider + credentials. Infomaniak (already hosting the VPS/domain) offers SMTP with the domain — likely path of least resistance; an API-based sender (e.g. a transactional mail service) is the alternative. Credentials go into `/etc/cairn-license/env`, never git. **Acceptance:** reset flow works end-to-end against the real SMTP (received mail → link → new password → old sessions dead); unknown email indistinguishable from known; expired/reused tokens rejected.
Author
Owner

Shipped and deployed (PR #8).

  • email.go: stdlib SMTP mailer from /etc/cairn-license/env (SMTP_HOST/PORT/USER/PASS/FROM; Infomaniak mailbox no-reply@swisscairn.ch configured by Nikola, SPF/DKIM/DMARC verified). Implicit TLS on 465, STARTTLS elsewhere; all sends async.
  • POST /v1/password-reset always 204 (no existence oracle), 1-hour single-use tokens; POST /v1/password-reset/confirm burns the token, sets the password, revokes all sessions. Rate-limited.
  • Site: "Forgot password?" on the sign-in tab + reset.html.

Unit-tested end to end (oracle silence, single use, session revocation, old-password death). Remaining acceptance: one live mail through the real Infomaniak SMTP — pending Nikola's smoke test.

Shipped and deployed (PR #8). - `email.go`: stdlib SMTP mailer from `/etc/cairn-license/env` (`SMTP_HOST/PORT/USER/PASS/FROM`; Infomaniak mailbox `no-reply@swisscairn.ch` configured by Nikola, SPF/DKIM/DMARC verified). Implicit TLS on 465, STARTTLS elsewhere; all sends async. - `POST /v1/password-reset` always 204 (no existence oracle), 1-hour single-use tokens; `POST /v1/password-reset/confirm` burns the token, sets the password, revokes all sessions. Rate-limited. - Site: "Forgot password?" on the sign-in tab + `reset.html`. Unit-tested end to end (oracle silence, single use, session revocation, old-password death). Remaining acceptance: one live mail through the real Infomaniak SMTP — pending Nikola's smoke test.
Cordy closed this issue 2026-07-30 00:15:05 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/cairn-license-server#5
No description provided.