Integrate email notifications into signup and login (#38)

This commit is contained in:
Stephan D committed 2026-09-12 15:07:00 +02:00
1 parent 326d58f705
commit ad5a061164
55 files changed
+1859 -3464

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
# Signup and login email integration
Task [#38](https://git.sendico.io/tech/sxchnge/issues/38), branch `auth-38`.
Primary module: auth; secondary modules: notification templates and Flutter auth.
This change stacks on notifications PR #37, which stacks on BFF PR #35.
Merge #35, then #37, then this contribution, retargeting each dependent PR to main.
## Configuration
Load the `auth.activation` and `auth.login` revision 1 templates from
`backend/notifications/templates.example.json` into the notification catalog.
Both templates have English and Russian variants; product account locale selects
Russian for `ru` variants and English otherwise. Publish subsequent template
changes under a new revision rather than mutating deployed content.
Configure `api.notifications` in the product YAML:
- `address`: internal notifications gRPC address.
- `token_env`: environment variable containing the configured caller's token
(at least 32 bytes); example `SXCHNGE_NOTIFICATIONS_TOKEN`.
- `ca_file`: trusted PEM CA bundle; omit to use system roots.
- `server_name`: optional TLS certificate hostname override, never a TLS bypass.
- `activation_url`: absolute frontend `/verify-email` URL. HTTPS is required,
except HTTP on localhost/127.0.0.1 for development. No user-supplied redirect is
accepted. The token is appended as a query parameter.
The caller token must map to the product caller in the notification service's
configuration. Keep Mongo, notification caller, TLS and provider credentials out
of Git. JWT signing configuration must contain at least 32 bytes of private key
material; HMAC-derived issuance keys depend on this key. Rotation changes replay
keys and existing pending JWT validity. Omission of `api.notifications` preserves
the bootstrap-only legacy routes; it does not enable signup or code completion.
The example config enables the integration and therefore requires credentials.
The notification service must be reachable over TLS, with its Mongo/NATS/email
provider configured. Product startup creates a lazy gRPC channel; dependency
outages appear as HTTP 503 when attempting delivery. A successful Send means
persisted queue acceptance, not inbox delivery or a provider receipt.
Start Flutter with:
```sh
flutter run --dart-define=SXCHNGE_API_URL=https://api.example/api/v1 \
--dart-define=SXCHNGE_CLIENT_ID=your-provisioned-public-client
```
Provision that client using the existing local application-client configuration
workflow. Its ID must exist in the BFF's client store and allow the user's IP.
Do not put a confidential client secret in Flutter. Match BFF CORS to the exact
frontend origin. Browser requests use credentials, the existing CSRF headers,
and Secure/HttpOnly/SameSite=Strict refresh cookies; deploy frontend and API on
HTTPS origins within the same site. Native adapters retain credentials in memory.
No API URL keeps the existing explicit preview services. With an API URL, auth
is real, the preview shortcut is hidden, and workspace routes require the local
confirmed session. Wallet, identity and factor-settings backend adapters remain
outside this change; those screens still use their existing preview data. The
real factor adapter refuses unsupported money/settings operations.
## HTTP contract
All paths have prefix `/api/v1/accounts`. Responses use the existing `data`
envelope. Credentials, codes and activation tokens are sent in POST bodies.
| Route | Request | Successful result |
| --- | --- | --- |
| POST `/signup` | `name`, `email`, `password`, `locale`, `idempotencyKey` | 202 with `userId`, after activation email queue acceptance |
| POST `/verification/send` | `email`, `idempotencyKey` | 202; generic acceptance also applies to absent/already-active accounts |
| POST `/verification/confirm` | `token` | 202 with `verified: true` after activation commits |
| POST `/login` | `login: {login, password}`, `clientId`, `deviceId` | 202 with `userId`, `pendingToken: {token, expiration}`, masked `target`, `expiresAt`, `resendAt` |
| POST `/login/resend` | `pendingToken`, `idempotencyKey` | 202 with the challenge timing and same pending credential |
| POST `/login/verify` | `pendingToken`, six-digit `code` | 200 with account and existing access/refresh response |
The pending JWT binds the customer, client and device. The verification record
binds its code to a hash of that JWT. Password login does not create a session.
The API-login client-secret endpoint retains its pre-existing separate behavior.
Unverified customers cannot log in; Flutter routes them to verification recovery.
Passwords preserve whitespace, matching signup hashing. Activation is single-use
and commits the account transition with token consumption in one transaction.
Login codes expire within 10 minutes and cannot outlive their pending JWT.
Resending uses a one-minute cooldown, invalidates the previous code in that
challenge, and allows five deliveries per destination per hour. Incorrect codes
consume the existing five-attempt budget. Restart login after a pending credential
expires or a challenge locks. Signup/resend request limits also apply per email
(10/hour) and IP (30/hour), using shared atomic fixed-window counters.
New idempotency keys are server-secret-derived before token generation. Automatic
verification keys now use 32 cryptographically random bytes. Empty-target queries
match both omitted and empty-string stored fields, fixing Mongo cooldown,
invalidation and replay semantics without rewriting existing documents.
Consumed or locked records cannot be replayed to redeliver an unusable code.
Token/hash/salt/key previews are excluded from verification logs.
## Recovery and delivery limits
A resend retries its same idempotency key after a failed/ambiguous RPC. Token
replay reconstructs the same code/link and stored expiry, and notification Send
deduplicates the same payload. A new accepted resend gets a new key. A failed
initial signup leaves the account pending; retry the same credentials or request
another activation email after cooldown. Initial signup and password-login
retries can create replacement messages; they are not distributed exactly-once
operations. Tokens are stored before the RPC, so a process crash between those
steps requires user recovery; there is no product-side delivery outbox.
Code consumption precedes session creation. If session creation or its HTTP
response fails, restart password login; the code is not reusable. The adapter
retains a confirmed session in memory and has no refresh/restore-on-reload flow
in this contribution. Backend authorization remains authoritative for future
protected APIs. Notification provider failures after queue acceptance are visible
in the notification service; no inbox/delivery-receipt status is claimed here.
No live emails were sent during implementation tests.
## Measurement and UX review
Existing `InstrumentedAuthServices`, `JourneyRecorder`, and factor providers track
signup, initial email acceptance, confirmation, login challenge verification,
completion, failure, retry, and navigation abandonment. Requests retain the
existing journey correlation and timeout rules in `docs/ux/auth-journeys.md`.
The initial confirmation-page send consumes the signup queue receipt once to
avoid sending a duplicate email. Success is based on backend acceptance or
verification, with `simulated: false`; it never means an inbox receipt.
Backend metrics `sxchnge_auth_flow_requests_total{operation,outcome}` and
`sxchnge_auth_flow_duration_seconds{operation}` expose accepted, rejected, limited
and unavailable HTTP steps without email, tokens or account labels. Combine
login-verify acceptance with login requests to inspect completion and dependency
failures. Counts are HTTP attempts, so retries are not distinct users. The current
frontend analytics sink is still the existing injected/default preview sink;
a production sink and cross-device confirmation correlation remain deployment
work. Do not interpret default in-memory analytics as retained production data.
UX checks cover unchanged mobile/keyboard form composition, preserved input after
errors, loading and cooldown feedback, invalid/expired/locked code recovery,
unverified-account recovery, and English/Russian copy. Queue feedback is phrased
conditionally because absent/already-active emails deliberately receive the same
acceptance response. Auth credentials are absent from provider state, analytics,
URLs (except the existing activation link) and local persistent storage. Existing
widget suites cover screen behavior; new adapter tests cover real response state
mapping, duplicate initial delivery, failures and one-use completion grants.
## Validation
Required validation: `make test` (including `make test-frontend`),
`make lint-backend`, Dart format checks for every changed Dart file,
and `flutter analyze` in `frontend/packages/sxchnge_auth` and `frontend/app`.
Results and the delivery PR are recorded in `docs/implementation.md` and task #38.