Files
marketplaces/docs/superpowers/specs/2026-08-15-email-phone-login-design.md
sdarbinyan e818dc6fc0
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
docs: add admin method toggle + error handling to email/phone login spec
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 18:52:52 +04:00

7.3 KiB

Email/phone customer login (OTP) — design

Status: Approved Date: 2026-08-15 Related backlog item: #4 (Telegram-only identification)

Problem

Customer storefront login/checkout requires Telegram today (src/app/services/auth.service.ts, TelegramSessionApiService) — shoppers without Telegram have no way to identify themselves. User asked for email/phone as an alternative.

Backend has zero email/phone/OTP/password infrastructure — only Telegram session polling exists (BACKEND-API-REFERENCE.md §2a). Building real authentication client-side is not possible; this is fundamentally a backend feature. Per user's explicit choice, this round produces the design + backend spec only — no client UI/code, since there is no real backend to build a working feature against yet (mirrors the currency-FX and admin-notifications backend-dependency pattern already documented this session).

Design

Mechanism: OTP code (email or SMS), chosen over magic link (email-only, extra click) and password (heaviest backend lift — storage, hashing, reset flow). Passwordless matches the feel of the existing Telegram QR flow.

A third, independent auth mechanism — coexists with Telegram QR (§2a) and admin Ed25519 (§2b, still unimplemented) exactly the way those two already coexist. Does not replace or modify either.

Proposed backend endpoints

POST /auth/otp/request
Body: { "identifier": "user@example.com" }   // or E.164 phone: "+79991234567"
Response: { "requestId": "...", "expiresAt": "2026-08-15T10:15:00Z" }
POST /auth/otp/verify
Body: { "requestId": "...", "code": "482913" }
Response (on success): {
  "sessionId": "...",
  "userId": 8823771,
  "username": null,
  "displayName": "user@example.com",
  "active": true,
  "expires": "2026-08-15T11:15:00Z"
}

The success response is shaped identically to the existing AuthSession model (src/app/models/auth.model.ts) — sessionId, userId, username, displayName, active, expires. This is deliberate: every downstream consumer (auth guards, session signals, cart/checkout) already works against AuthSession regardless of which mechanism produced it, so wiring this in later requires no changes to guards or session state — only a new "request/verify" UI flow that ends by populating the same session shape Telegram QR already produces.

Rate limiting / expiry (backend-enforced, not left implicit):

  • Resend cooldown: 60s between POST /auth/otp/request calls for the same identifier.
  • Code expiry: 10 minutes from issuance.
  • requestId allows up to 5 verify attempts before it's invalidated — consumed on success, on the 5th wrong attempt, or on expiry, whichever comes first. (Revised from an earlier single-use-per-attempt draft: burning the whole request on one typo is bad UX — a shopper should be able to correct a mistyped digit without waiting out a fresh 60s cooldown.)

Admin-configurable login methods

Admin can enable/disable each login method independently — Telegram QR, Email OTP, Phone OTP — via three checkboxes in Admin Settings, same section/pattern as the existing currency-rates and notification-interval settings (admin-settings-page.component.ts, LocalStorageService-persisted signal).

  • Default: all three enabled — a settings change must never silently lock shoppers out.
  • A new AuthMethodsService (or an extension of the existing settings service) exposes enabledMethods: Signal<('telegram' | 'email' | 'phone')[]>. The storefront login screen reads it and only renders buttons for enabled methods; if exactly one is enabled, skip the method-picker screen entirely and go straight to it.
  • Purely a client-side UI gate — the backend OTP endpoints stay unconditionally available; disabling "Email OTP" in admin just hides the button, it doesn't need a corresponding backend flag. (Same category of client-only gate as the existing adminAuthGuard/permission checks — real enforcement, if ever needed, would be a separate backend concern.)

Error handling

The codebase has an established error envelope (BACKEND-API-REFERENCE.md §5: error.code, error.message, error.status, error.details) explicitly flagged as "recommended for new endpoints, not wired anywhere yet." Since the OTP endpoints are new, this is the natural first real adopter — every response maps to a specific code, not just an HTTP status:

error.code HTTP status UX
VALIDATION_FAILED 422 Inline field error under the identifier input, sourced from error.details[0].message — same pattern the client already uses for local validation errors (cart.component.ts's email/phone inline errors), so a 422 slots into the existing inline-error UI without inventing a second display mechanism.
RATE_LIMITED 429 "Too many attempts — try again in Ns," countdown derived from error.details/Retry-After if present, otherwise a flat 60s. Resend button stays disabled until the countdown ends.
CODE_EXPIRED 410 "This code expired — request a new one." Auto-focuses/enables the resend action; does not silently re-send.
CODE_INVALID 401 "Wrong code, try again" — stays on the code-entry screen (does not consume the whole flow; see the 5-attempt allowance above). Shows the remaining-attempts count once ≤2 remain.
REQUEST_NOT_FOUND 404 requestId unknown/already invalidated (5 wrong attempts, expiry, or a stale reload) — "This login attempt is no longer valid, start again," returns to the identifier-entry step.
Anything else / network error / 5xx Generic fallback: "Something went wrong. Try again, or use a different login method" — the second half of that sentence is a real, populated action, not filler text: it surfaces whichever other methods are currently enabled per the admin toggle above (e.g. falls back to the Telegram QR button), not just a dead-end retry link.

Identifier validation: email vs. phone format is auto-detected client-side. Extract the validation logic already written inline in cart.component.ts (validateEmail/validatePhone, currently only used for post-purchase contact capture) into a shared utility rather than duplicating it when the client UI is eventually built — the same email/phone shape-checking applies to both use cases.

Future client UI (not built this round)

A "Login with email or phone" option next to the existing Telegram QR button: identifier entry → code entry → session established. Deferred until the backend endpoints above exist — no client code to write against a 404.

Backend doc update

New BACKEND-API-REFERENCE.md §2c ("Email/phone OTP login — customer (NOT IMPLEMENTED)"), following the same Gap/Ask format as the existing §12.x entries, documenting the two endpoints, the response-shape compatibility requirement, and the rate-limit/expiry asks above.

Out of scope

  • Admin backoffice login — user confirmed this round is customer-storefront only (item 4 was split into two potential specs during brainstorming; admin auth is a separate future spec if wanted).
  • Magic link and password mechanisms — considered, OTP chosen.
  • Any client-side UI or session-handling code — explicitly deferred; nothing to build against a non-existent backend.
  • Account merging (e.g. a shopper who later links Telegram + email to the same identity) — not raised, not designed.