# Forgot Password Flow Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add self-service password reset so users can recover access via email link without admin help. **Architecture:** Three UI changes — a link on the login form, a forgot-password page (sends reset email via Supabase), and a reset-password page (sets new password using the recovery session). The existing `/api/auth/callback` route handles the email link's code exchange without modification. **Tech Stack:** Next.js 15 App Router, Supabase Auth (`@supabase/ssr`), Tailwind CSS ## Global Constraints - `basePath: '/ims'` — all internal links use paths without `/ims` prefix; Next.js prepends it automatically - `NEXT_PUBLIC_APP_URL` = `http://64.176.82.100/ims` in production (`.env.production`) — no trailing slash - Supabase client in client components: `createClient` from `@/lib/supabase/client` - Styling must match existing login form: `border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500`, error in `bg-red-50 border border-red-200 text-red-700 text-sm rounded px-3 py-2` - Password minimum: 8 characters (matches `change-password-form.tsx`) - No new API routes — all Supabase calls are client-side --- ### Task 1: Add "Forgot password?" link to login form **Files:** - Modify: `components/auth/login-form.tsx` **Interfaces:** - Produces: link to `/forgot-password` visible below the Sign in button - [ ] **Step 1: Add the link** In `components/auth/login-form.tsx`, add after the `

Forgot password?

``` - [ ] **Step 2: Verify visually** Run `npm run dev` and open `http://localhost:3000/ims/login`. Confirm "Forgot password?" link appears below the Sign in button and clicking it navigates to `/forgot-password` (404 is expected — page doesn't exist yet). - [ ] **Step 3: Commit** ```bash git add components/auth/login-form.tsx git commit -m "feat(auth): add forgot password link to login form" ``` --- ### Task 2: Create forgot-password page **Files:** - Create: `app/(auth)/forgot-password/page.tsx` **Interfaces:** - Consumes: `createClient` from `@/lib/supabase/client`, `NEXT_PUBLIC_APP_URL` env var - Produces: page at `/forgot-password` that sends a reset email and shows success state - [ ] **Step 1: Create the page file** Create `app/(auth)/forgot-password/page.tsx`: ```tsx 'use client' import { useState } from 'react' import Link from 'next/link' import { createClient } from '@/lib/supabase/client' export default function ForgotPasswordPage() { const [email, setEmail] = useState('') const [loading, setLoading] = useState(false) const [submitted, setSubmitted] = useState(false) const [error, setError] = useState(null) async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!email) return setLoading(true) setError(null) const supabase = createClient() const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? '' const redirectTo = `${appUrl}/api/auth/callback?next=/auth/reset-password` const { error: resetError } = await supabase.auth.resetPasswordForEmail(email, { redirectTo, }) setLoading(false) if (resetError) { setError('Something went wrong. Please try again.') return } // Always show success — never reveal whether email exists setSubmitted(true) } if (submitted) { return (

Check your email

If an account exists for {email}, a password reset link has been sent.

Back to sign in
) } return (

Reset password

Enter your email and we'll send a reset link.

{error && (

{error}

)} setEmail(e.target.value)} required autoComplete="email" className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />

Back to sign in

) } ``` - [ ] **Step 2: Verify page loads** With dev server running, open `http://localhost:3000/ims/forgot-password`. Confirm the form renders with email input and "Send reset link" button. Confirm "Back to sign in" navigates to `/ims/login`. - [ ] **Step 3: Commit** ```bash git add app/\(auth\)/forgot-password/page.tsx git commit -m "feat(auth): add forgot password page" ``` --- ### Task 3: Create reset-password page **Files:** - Create: `app/(auth)/reset-password/page.tsx` **Interfaces:** - Consumes: `createClient` from `@/lib/supabase/client`, valid Supabase recovery session in browser cookie - Produces: page at `/auth/reset-password` that sets a new password and redirects to `/login` Note: The path is `/auth/reset-password` (not `/reset-password`) because the auth callback `next` param is `/auth/reset-password` which resolves to `app/(auth)/reset-password/page.tsx` — the `(auth)` group folder matches the `/auth/` URL segment? No — route groups like `(auth)` do NOT add a URL segment. So `app/(auth)/reset-password/page.tsx` maps to `/reset-password`. **Correction:** The callback redirects to `/auth/reset-password` but the page lives at `app/(auth)/reset-password/page.tsx` which maps to `/reset-password`. These must match. Use `/reset-password` as the `next` param in the callback URL (update Task 2's `redirectTo`). Update `app/(auth)/forgot-password/page.tsx` line: ```tsx const redirectTo = `${appUrl}/api/auth/callback?next=/reset-password` ``` - [ ] **Step 1: Fix redirectTo in forgot-password page** In `app/(auth)/forgot-password/page.tsx`, change: ```tsx const redirectTo = `${appUrl}/api/auth/callback?next=/auth/reset-password` ``` to: ```tsx const redirectTo = `${appUrl}/api/auth/callback?next=/reset-password` ``` - [ ] **Step 2: Create the reset-password page** Create `app/(auth)/reset-password/page.tsx`: ```tsx 'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import { createClient } from '@/lib/supabase/client' export default function ResetPasswordPage() { const router = useRouter() const [password, setPassword] = useState('') const [confirm, setConfirm] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError(null) if (password.length < 8) { setError('Password must be at least 8 characters.') return } if (password !== confirm) { setError('Passwords do not match.') return } setLoading(true) const supabase = createClient() const { error: updateError } = await supabase.auth.updateUser({ password }) setLoading(false) if (updateError) { if (updateError.message.toLowerCase().includes('session') || updateError.message.toLowerCase().includes('expired')) { setError('Reset link has expired. Request a new one.') } else { setError(updateError.message) } return } router.push('/login?message=password_reset') } return (

Set new password

Choose a strong password.

{error && (

{error}{' '} {error.includes('expired') && ( Request new link )}

)} setPassword(e.target.value)} required minLength={8} autoComplete="new-password" className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> setConfirm(e.target.value)} required autoComplete="new-password" className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
) } ``` - [ ] **Step 3: Show success message on login page when redirected from reset** In `app/(auth)/login/page.tsx` (or wherever the login page renders), check for `?message=password_reset` and show a banner. Check what the login page currently looks like first: ```bash cat app/\(auth\)/login/page.tsx ``` If it just renders ``, update it to a server component that reads the search param and passes a success message: ```tsx import { LoginForm } from '@/components/auth/login-form' interface Props { searchParams: Promise<{ message?: string }> } export default async function LoginPage({ searchParams }: Props) { const { message } = await searchParams return (
{message === 'password_reset' && (
Password updated — sign in with your new password.
)}
) } ``` If the login page already has a wrapping layout, add the banner inside the existing structure instead. - [ ] **Step 4: Verify reset-password page loads** Open `http://localhost:3000/ims/reset-password`. Confirm the two password inputs and "Set password" button render. Without a recovery session, the Supabase call will fail — the error message should show. - [ ] **Step 5: Commit** ```bash git add app/\(auth\)/forgot-password/page.tsx app/\(auth\)/reset-password/page.tsx app/\(auth\)/login/page.tsx git commit -m "feat(auth): add reset-password page and login success banner" ``` --- ### Task 4: Verify Supabase redirect URL and deploy **Files:** None — configuration + deploy - [ ] **Step 1: Check Supabase redirect URL allowlist** Go to [https://supabase.com](https://supabase.com) → project `nkcfjbgappslicotwopl` → Authentication → URL Configuration → Redirect URLs. Ensure this URL is listed: ``` http://64.176.82.100/ims/api/auth/callback ``` If missing, click "Add URL" and add it. Without this, Supabase will reject the reset email's redirect as unauthorised. - [ ] **Step 2: Deploy to VPS** ```bash bash deploy.sh ``` Expected output ends with: ``` ==> Done. http://64.176.82.100/ims/ ``` - [ ] **Step 3: End-to-end test on production** 1. Go to `http://64.176.82.100/ims/login` — confirm "Forgot password?" link visible 2. Click it → confirm redirects to `http://64.176.82.100/ims/forgot-password` 3. Enter a real user's email → click "Send reset link" 4. Confirm success screen: "Check your email" 5. Open email → click the reset link 6. Confirm landing on `http://64.176.82.100/ims/reset-password` 7. Enter a new password (≥8 chars) + confirm → click "Set password" 8. Confirm redirect to `/login` with green success banner 9. Sign in with the new password — confirm it works