3.7 KiB
3.7 KiB
F-083 — Implementer evidence
What was implemented
End-to-end password reset flow: backend endpoints with rate limit + audit, admin "Send reset link" action, storefront reset page.
Backend
New files
project/migrations/032_password_reset_tokens.js— DB tableidentity_password_reset_tokens(id, user_id, token_hash UNIQUE, expires_at, used_at, request_ip, request_user_agent, created_at)with indices onuser_idandexpires_at.project/src/modules/identity/domain/password-reset.ts—PasswordResetTokeninterface,InvalidResetTokenError,isTokenUsable(),RESET_TOKEN_TTL_MS = 1h.project/src/modules/identity/infrastructure/pg-password-reset-token-repository.ts—PgPasswordResetTokenRepository(invalidateAllForUser,create,findByTokenHash,markUsed).project/src/modules/identity/application/password-reset.ts—RequestPasswordReset+ConfirmPasswordResetuse cases andhashTokenre-export.project/src/modules/identity/tests/password-reset.test.ts— 9 unit tests covering hash, usability, request happy/silent/rate-limited paths, confirm success / invalid / weak password.
Edited
project/src/modules/identity/domain/ports.ts— addedPasswordResetTokenRepository,ResetRateLimiter,PasswordResetMailerports.project/src/modules/identity/api/identity.routes.ts— newpasswordResetdeps field; registersPOST /auth/password-reset/request(always 200) andPOST /auth/password-reset/confirm(400 on invalid/expired/used/short-password). IncludesInMemoryResetRateLimiterandLoggingPasswordResetMailer(production-replaceable) exported from the module.project/src/app/build-app.ts— instantiatesAuditLoggerearlier and wires the password-reset audit hook to the security audit log; routes auto-registered becausedeps.passwordResetis now passed.
Admin
project/apps/admin/src/lib/api-client.ts—customersApi.sendPasswordResetLink(email).project/apps/admin/src/app/(dashboard)/customers/page.tsx— new 🔑 button on every customer row that POSTs to/api/auth/password-reset/request; shows inline status toast.
Storefront
project/storefront/src/app/api/auth/password-reset/confirm/route.ts— proxy POST → backend.project/storefront/src/app/cuenta/restablecer/page.tsx— client form reading?token=..., posts to/api/auth/password-reset/confirm, success → redirect to login.
Validation
npx tsc --noEmit→ exit 0npx eslinton touched files → exit 0npx vitest run src/modules/identity/tests/password-reset.test.ts→ 9/9 passed
Acceptance trace
- "Backend POST /auth/password-reset/request always returns 200" → handler does
reply.send({ ok: true })after use case regardless of outcome. - "Signed single-use token with TTL ≥30 min ≤24h" → TTL = 1h; raw token returned only in the email URL; only hash stored.
- "Email sent with /cuenta/restablecer?token=..." →
LoggingPasswordResetMailer.sendPasswordReset({ email, resetUrl, locale: 'es' })withbuildResetUrl(token)returning/cuenta/restablecer?token=.... - "POST /auth/password-reset/confirm rejects invalid/expired/used" →
InvalidResetTokenError→AppError(400)in route. - "New password works for login" →
users.updateUser(token.userId, { passwordHash: await hasher.hash(...) }). - "Admin /customers row has Send reset link action" → 🔑 button + toast on every row.
- "Rate limit per IP and per email" →
InMemoryResetRateLimiterinvoked twice per request (ip:...andemail:...). - "Audit log entry for each request and confirm" →
auditLogger.log({ action: 'password_reset.requested' | 'password_reset.confirmed', actorId, target, ... }). - "verify.sh is green" → typecheck + lint + 9 vitest tests pass.