|
W30_live_verify — Forgot Password v4 (ticket #3273 / oms-forgt-passwd-v4)
|
|
Node: W30_live_verify (Web)
|
|
Date: 2026-09-08
|
|
|
|
SCOPE
|
|
Feature files under test (per sides.web.feature_files):
|
|
app/(auth)/forgot-password/page.tsx
|
|
app/(auth)/forgot-password/verify/page.tsx
|
|
app/(auth)/forgot-password/reset/page.tsx
|
|
components/auth/ForgotPasswordForm.tsx
|
|
components/auth/VerifyOtpForm.tsx
|
|
components/auth/ResetPasswordForm.tsx
|
|
components/auth/SignInForm.tsx
|
|
Server Actions under test (lib/actions/forgot-password.ts):
|
|
requestPasswordResetOtp, resendOtp, verifyResetOtp, resetPasswordViaOtp
|
|
ticket_spec.acceptance_criteria was an empty array; scope was taken from
|
|
requirement_text's numbered requirements + "Final Verification" checklist,
|
|
scoped strictly to the forgot-password flow (not a full app re-verification).
|
|
|
|
ENVIRONMENT SETUP (real Postgres, real Next.js dev server — no mocks)
|
|
1. Local Postgres 16 already running (oms_test / oms_test_pw @ localhost:5432,
|
|
per .env DATABASE_URL). `prisma migrate status` showed the DB's applied
|
|
migration history had DIVERGED from this checkout's prisma/migrations/:
|
|
- DB had `20260904000000_password_reset_flow` applied (created a
|
|
`PasswordResetFlow` table) which does not exist in this checkout's
|
|
migrations directory at all — leftover from an earlier iteration of
|
|
this ticket (branches feat/3254-... and feat/3269-... exist in git
|
|
reflog, i.e. this is v4 of the same ticket).
|
|
- This checkout's own `20260904090000_item_code` and
|
|
`20260904120000_password_reset_otp` (which creates the actual
|
|
`PasswordResetOtp` table the current code in lib/actions/forgot-password.ts
|
|
and prisma/schema.prisma reads/writes) had NOT been applied.
|
|
Ran `npx prisma migrate deploy` — applied both pending migrations cleanly,
|
|
no conflicts with the stray `PasswordResetFlow` table (untouched, unrelated).
|
|
Ran `npx prisma generate`.
|
|
2. Ran `SUPER_ADMIN_MOBILE="9840421770" npx tsx prisma/seed.ts` — .env had no
|
|
SUPER_ADMIN_MOBILE set, so the seeded superadmin user would otherwise carry
|
|
the intentionally-invalid "0000000" sentinel mobile (see .env.example /
|
|
prisma/seed.ts comments). Set a real 10-digit mobile so the forgot-password
|
|
flow has a real registered user to exercise end to end.
|
|
3. Started `npm run dev` (NODE_ENV=development implicit) on :3000. In this
|
|
mode lib/sms.ts's sendOtp() writes the plaintext OTP to server stdout
|
|
(`[OTP dev] <mobile>: <code>`) instead of calling a real SMS gateway — this
|
|
is documented in-code as a dev-only escape hatch. Captured real OTPs from
|
|
this stdout stream for each test run (never fabricated).
|
|
4. Drove the app with a real headless Chromium (Playwright, cached browser
|
|
binaries under ~/.cache/ms-playwright, driven via node scripts run from
|
|
/opt/drcarrot-deploy where the playwright package is available) against
|
|
http://localhost:3000 — real HTTP requests, real Server Actions, real
|
|
Postgres reads/writes. Full click-by-click transcript with timestamps:
|
|
docs/gate-evidence/live-verify-3273-transcript.txt
|
|
|
|
RESULTS — golden path & edge cases (all against real DB/server, see transcript)
|
|
PASS Sign In page has a working "Forgot Password?" link -> /forgot-password
|
|
PASS Golden path: mobile -> OTP (real, captured from stdout) -> verify ->
|
|
reset -> redirect to /login -> login with the NEW password succeeds
|
|
(lands on /orders, i.e. a real authenticated session)
|
|
PASS Old (pre-reset) password is rejected after a successful reset
|
|
PASS Unregistered mobile: stays on /forgot-password (no redirect to verify),
|
|
shows the generic "If a matching account is found..." anti-enumeration
|
|
message — matches lib/actions/forgot-password.ts's documented behavior
|
|
PASS Invalid OTP: real "Invalid code. Please check and try again." field
|
|
error, stays on /verify
|
|
PASS Expired OTP: forced expiry via direct DB UPDATE (expiresAt = now() -
|
|
1 min) on a real just-issued OTP row, then submitted the original code
|
|
-> real "This code has expired or has already been used." rejection
|
|
PASS Reuse of an already-consumed OTP: after a full first pass consumed the
|
|
OTP (usedAt set) and cleared fp_otp_token, re-visiting /verify directly
|
|
redirects to /forgot-password (cookie-guard in the page component)
|
|
PASS Resend Code: 30s countdown shown on load, button hidden until it
|
|
elapses (waited out the real 30s, not simulated); clicking Resend
|
|
issued a genuinely new OTP hash (verified: the pre-resend code is
|
|
rejected afterwards, the new one succeeds); PasswordResetOtp.attempts
|
|
incremented to 1 in the DB after one resend, confirming the
|
|
OTP_MAX_ATTEMPTS=5 rate-limit counter is live
|
|
PASS Password policy: a policy-violating password ("weak") leaves the
|
|
submit button disabled client-side
|
|
PASS Password mismatch: "Passwords do not match" inline error shown
|
|
PASS Password history (last-3): attempting to reuse the just-set password
|
|
is rejected with "You have used this password recently."
|
|
PASS A second, genuinely new, compliant password completes the reset
|
|
PASS Direct navigation to /forgot-password/verify or /forgot-password/reset
|
|
with no fp_otp_token/fp_reset_token cookie redirects back to
|
|
/forgot-password (guard in both page.tsx files)
|
|
PASS Refresh mid-flow on /verify (cookie still valid) does not break the
|
|
page; browser back/forward between step 1 and step 2 does not crash
|
|
and the verify page remains reachable while its cookie is alive
|
|
PASS Existing login/logout: a wrong-password login attempt for an unrelated
|
|
seeded user (staff_test) still correctly rejects — forgot-password
|
|
changes did not regress the existing session/auth path
|
|
|
|
CRITICAL FINDING — sendOtp() cannot deliver a real SMS outside local dev
|
|
lib/sms.ts's sendOtp() only writes the OTP to stdout when
|
|
`process.env.NODE_ENV === "development"`. In every other environment
|
|
(explicitly including production — scripts/build-prod.sh runs
|
|
`NODE_ENV=production npx next build`, and `next start`/`next build` both run
|
|
under NODE_ENV=production) it throws:
|
|
"SMS provider not configured. Set SMS_PROVIDER_API_KEY ... Mobile: 984***"
|
|
Verified directly (not just by reading the code):
|
|
$ NODE_ENV=production npx tsx -e "import('./lib/sms.ts').then(...)"
|
|
-> sendOtp threw as expected in production: SMS provider not configured...
|
|
requestPasswordResetOtp's catch block swallows this and returns the generic
|
|
"We could not send the verification code. Please try again." to the user.
|
|
Net effect: in any deployed (non-dev) environment, EVERY forgot-password
|
|
attempt for EVERY registered user fails at step 1 — no OTP is ever sent, no
|
|
user can ever complete this flow. The ticket's own requirement list states
|
|
"Implement both frontend and backend/API changes if the required
|
|
forgot-password/verification functionality does not already exist" and
|
|
"Verification code must be securely generated, stored, validated, and
|
|
expired on the backend" — generation/storage/validation/expiry are all
|
|
implemented and verified above, but delivery is not, and the code says so
|
|
explicitly (lib/sms.ts's own header comment flags the SMS provider as an
|
|
unresolved "clarifications_needed" item). This is not a pre-existing/
|
|
unrelated-flow issue — it is this ticket's own OTP-delivery mechanism, and
|
|
it is unconditionally broken for any real deployment.
|
|
|
|
SECONDARY FINDING — destination mobile number not shown on the Verify screen
|
|
Requirement (explicit, with the design's own example): "Show the destination
|
|
number the code was sent to (design shows 'Enter the 6-digit code sent to
|
|
+91 98404 21770')." Verified live: components/auth/VerifyOtpForm.tsx renders
|
|
"Enter the 6-digit code we sent to your registered mobile number." — no
|
|
digits of the actual number appear anywhere on the page (checked via a live
|
|
DOM text-content scan for the seeded number's last 4 digits: not found).
|
|
Functionally the flow still works (the OTP itself is correctly scoped to the
|
|
right user via the signed cookie), but the explicit, named requirement is
|
|
not met.
|
|
|
|
ENDPOINTS SUMMARY
|
|
declared: 4 Server Actions (requestPasswordResetOtp, resendOtp,
|
|
verifyResetOtp, resetPasswordViaOtp) in lib/actions/forgot-password.ts
|
|
tested: 4/4, each driven end-to-end at least once above with a real DB
|
|
permission_gated: 0 — all four are deliberately unauthenticated
|
|
(pre-login) actions by design; none carry or should carry a permission
|
|
check. permission_matrix is empty accordingly.
|
|
|
|
REPRODUCIBILITY
|
|
seed command: SUPER_ADMIN_MOBILE="9840421770" npx tsx prisma/seed.ts
|
|
(run after `npx prisma migrate deploy`)
|
|
seeded user used for all scenarios: username=superadmin, mobile=9840421770
|
|
(final password at end of this run: FinalPassw0rd!9 — the DB is left in
|
|
this end state; re-running the verification flow from scratch requires
|
|
re-seeding or resetting the password history/passwordHash for this user)
|
|
env: NODE_ENV=development, `npm run dev` on :3000, DATABASE_URL from this
|
|
checkout's own .env (postgresql://oms_test:oms_test_pw@localhost:5432/oms_test)
|