Skip to content

Wire real password reset into the Builder auth screen (Refs #7698) - #798

Merged
developer-ainative merged 1 commit into
mainfrom
feature/7698-app-branded-password-reset
Sep 18, 2026
Merged

developer-ainative merged 1 commit into
mainfrom
feature/7698-app-branded-password-reset

Conversation

@developer-ainative

Copy link
Copy Markdown
Contributor

Problem

Reported live 2026-09-18: a founder clicked "Forgot password?" in Builder and hit a dead end. The handler in components/build/screens/Auth.tsx was a stub that never called anything:

if (mode === 'forgot' || mode === 'reset') { setError('Password reset is coming soon — contact support.'); return }

Change

Builder has no AINative password store of its own — core owns the users table, the reset token, and the reset email. So this is a proxy, not a local implementation (unlike app/api/auth/login/route.ts, which checks Builder's own DB).

app/api/auth/forgot-password/route.ts (new) — proxies to core, following the existing core-proxy pattern in app/api/build/register/route.ts (AINATIVE_API_URL base, 25s timeout, {ok} response shape):

  • default action → POST /api/v1/auth/forgot-password with {email, app: 'builder'} (core's new #7698 branding hook, so the email is Builder-branded and its link returns here)
  • action:'reset'POST /api/v1/auth/reset-password with core's real {token, new_password} contract

components/build/screens/Auth.tsx — forgot/reset modes call it for real. Confirmation panels mirror the existing verify-email panel's shape and styling rather than inventing new patterns. The forgot confirmation stays deliberately conditional ("if an account exists…") so this screen cannot become the account-enumeration oracle core carefully avoids being.

app/reset-password/page.tsx (new) + middleware.ts — see below.

The landing page: it did NOT already exist

Worth calling out explicitly, since it was flagged as possible follow-up scope.

  • The password-entry UI already existedAuth.tsx has a full mode==='reset' branch rendering a "New password" field, and 'reset' was already an allowed deep-link screen (added by Auth screens have no navigation back to the landing page #651).
  • The route did not. Core emails {frontend_url}/reset-password?token=…, which for Builder is now https://builder.ainative.studio/reset-password?token=… — a hard 404 on Builder today. Every reset link would have died there.

Because the SPA screen already existed, closing this was small and obvious from an existing precedent (app/refer/page.tsx): a thin redirect forwarding the token into /build?screen=reset&token=…. Built, not deferred. It's allowlisted anonymously in middleware.ts — a founder resetting a password is by definition logged out, so gating it behind auth would make every reset link dead.

Test evidence

20 new tests, genuine red → green (10 failed with the source stashed, 20 passed with it):

✓ __tests__/api/forgot-password-route.test.ts                (10 tests)
✓ __tests__/components/build/Auth-password-reset.test.tsx    (10 tests)

Route tests cover: app:'builder' actually being sent, email normalization, invalid email rejected without calling core, no account-existence leak, 429 surfaced plainly, 502 on core unreachable, the new_password (not password) field name, and expired-token errors surfaced rather than swallowed. Component tests cover: the stub message being gone, the neutral confirmation, local validation, token read from the URL, and error states not claiming false success.

No regressions in the 3 pre-existing Auth suites:

✓ Auth-my-companies-outage (4)  ✓ Auth-back-to-landing (13)  ✓ Auth-phone-otp (7)

Full suite and typecheck:

Test Files  403 passed | 6 skipped
     Tests  5660 passed | 50 skipped
npx tsc --noEmit  → clean

One unrelated flake, __tests__/lib/generation-persist.test.ts (a background-retry timing test), fails only under full-suite parallel load — it passes in isolation both with and without this change, and this PR touches none of that code path.

Companion PR

Core-side app branding parameter: AINative-Studio/core#7699. That must merge and deploy first — until it does, core ignores the unknown app field and falls back to AINative branding, so this PR degrades safely rather than breaking.

Refs #7698

Builder's "Forgot password?" button was a stub that never called anything:
  if (mode === 'forgot' || mode === 'reset') { setError('Password reset is
    coming soon — contact support.'); return }
so a founder clicking it hit a dead end (live: 2026-09-18).

Builder has no AINative password store of its own — core owns the users
table, the reset token and the reset email — so this is a proxy, not a
local implementation:

- app/api/auth/forgot-password/route.ts proxies to core
  POST /api/v1/auth/forgot-password with app:'builder' (core's new branding
  hook), and POST /api/v1/auth/reset-password for the final token exchange,
  following the existing core-proxy pattern in app/api/build/register.
- Auth.tsx's forgot/reset modes now call it for real, with neutral
  "check your email" and "password updated" confirmations matching the
  existing verify-email panel. The forgot confirmation stays conditional
  ("if an account exists") so it cannot become the account-enumeration
  oracle core carefully avoids being.
- app/reset-password/page.tsx is the landing page for the emailed link,
  which core builds as {frontend_url}/reset-password?token=… — without it
  every Builder reset link 404s. Thin redirect into the SPA's existing
  'reset' screen, same shape as /refer. Allowlisted anonymously in
  middleware.ts: a founder resetting a password is by definition logged out.

Tests: 20 new (red before the fix, green after) across the route and the
component. Full suite 5660 passed; tsc --noEmit clean.

Refs #7698
@developer-ainative
developer-ainative merged commit 24006dd into main Sep 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant