1.1.6 Password reset flow + email abstraction (dev console provider)
Estimate: 22m · Depends on: 1.1.2, 1.1.3
Implement the backend half of password reset (the token table, the "request reset" handler that sends an email with a one-time link, and the "set new password" handler that consumes the token) and introduce a small email-provider abstraction (lib/email.ts) so the rest of the codebase never depends on a specific mailer. The UI half lives in 1.1.5; this subtask provides the endpoints those pages call plus the email abstraction those endpoints (and the email-verification flow) use.
Why an abstraction, not a direct Resend/Postmark/SES call: Per the planner-as-consumer principle (MOTIR.md "Current state"), production email-provider choice is planner work — each Motir-planned project's planner decides which provider to use in pre-plan and adds a mandatory Story to wire it. The starter (and motir-core itself for v1) ships only the abstraction + a dev console-logging provider that prints reset links to stdout. Production wiring is deferred. This keeps the starter dependency-free of any specific email vendor while making the wiring point explicit (lib/email.ts's sendEmail() export).
Why split from 1.1.5: Reset is a security-sensitive flow (timing attacks, token reuse, account enumeration) that benefits from focused review independent of the UI. Splitting also lets it run in parallel with 1.1.5 since they share no files.
What you'll do: Add a PasswordResetToken table (token-hash, user-id, expires-at, used-at). Create /lib/email.ts exporting sendEmail({ to, subject, html, text? }) as the canonical interface; provide a DevConsoleEmailProvider implementation that logs to stdout with a clear [EMAIL] marker; wire the production-provider hook as a single env-var switch (EMAIL_PROVIDER=console default; future values resend, postmark, etc.). Create Server Actions requestReset(email) and confirmReset(token, newPassword) in /app/(auth)/reset-password/actions.ts. Tokens expire in 1 hour and are single-use.
Acceptance criteria
PasswordResetTokentable with required fields + indexes onuserIdandexpiresAt.- Tokens stored as hashes (don't store the raw token; hash on insert, compare hashes on lookup).
/lib/email.tsexports a typedsendEmail({ to, subject, html, text? }) → Promise<void>interface and aDevConsoleEmailProviderimplementation. Selected viaEMAIL_PROVIDERenv var (default:console); unknown values throw a clear startup error.- Console provider prints
[EMAIL] To: ... Subject: ... Body: ...with the full reset link visible, so dev/test flows can extract it from stdout. requestReset(email): rate-limit to 3 requests/hour/email; do NOT reveal whether the email exists in the response; callssendEmailwith the reset link only if a user is found (silent no-op otherwise).confirmReset(token, newPassword): validates token, checks expiry, checks unused, updates password via the repo from 1.1.3, marks token used, returns success or typed error..env.exampledocumentsEMAIL_PROVIDERwith the comment "consolefor dev; production provider wiring is planner work — see your project's pre-plan email-provider Story."- Tests: happy path; expired token; used token; unknown email (silently succeeds — no email sent); rate-limit triggers;
sendEmailwith the console provider writes the expected marker to stdout.
Context refs
/lib/users/repo.ts— user repository (from 1.1.3)/lib/auth/passwords.ts— hashing helper (from 1.1.3)/prisma/schema.prisma— current schema- MOTIR.md — planner-as-consumer principle; email-provider choice is planner work
- OWASP password-reset cheat-sheet (URL, fetched at prompt-gen)