Code

Forgot & reset, end to end

Now the flow itself. Four files as tabs: auth.templutil.goauth.gorouter.go. All are modifications.

1. internal/web/auth.templ (modify) — three templates join: Forgot (email form + the generic confirmation), ResetPassword (the new-password form), and ResetInvalid (expired/used/missing). Each carries the CSRF field, like every form since lesson 6.

2. internal/handlers/util.go (modify) — one helper, chiURLParam, to read the {token} from the URL.

3. internal/handlers/auth.go (modify) — the handlers. Forgot shows the same confirmation either way and only does real work if the account exists; sendResetLink creates the token and sends the email; Reset verifies, updates the password, then burns everything:

h.Queries.UpdateUserPassword(...)         // the new hash
h.Queries.MarkPasswordResetUsed(prt.ID)   // this token: spent
h.Queries.InvalidateUserPasswordResets(...) // any other reset tokens: spent
h.Queries.DeleteSessionsForUser(...)      // every session: gone

4. internal/handlers/router.go (modify) — the four routes: GET/POST /forgot, GET/POST /reset/{token}.

Verify — the whole flow, no API key. sqlc generate && templ generate && go run ./cmd/signflow, then:

  1. Log in on a second device (or private window) so the account has two sessions: SELECT count(*) FROM sessions → 2.

  2. Go to /loginForgot your password? → enter your email → submit. You see the generic confirmation. Now look at the server terminal:

    ┌──────────────────────────────────────────────────────────────
    │ 📧  DEV EMAIL (not actually sent)
    │ To:      you@example.com
    │ Subject: Reset your SignFlow password
    ├──────────────────────────────────────────────────────────────
    │ Someone requested a password reset for your SignFlow account.
    │
    │ Reset your password (link valid for 1 hour):
    │ http://localhost:8080/reset/Ck3p9x2...
    │
    │ If this wasn't you, you can safely ignore this email.
    └──────────────────────────────────────────────────────────────
    

    A clean, readable box — that's the fmt.Fprintf-to-stdout decision paying off. Copy the link.

  3. Open it, set a new password, submit → redirected to /login. Now the payoff:

    $ psql signflow -c "SELECT count(*) FROM sessions;"
     count
    -------
         0          ← BOTH sessions gone. A password change logged out every device.
    

    Log in with the new password — it works. The old one no longer does.

  4. Single-use. Reload the reset link you just used → the "This reset link is invalid" page. used_at is set, so GetValidPasswordReset returns nothing.

Gotcha (enumeration). Submit /forgot with an email that was never registered. You get the exact same confirmation — "if an account exists, we've sent a link." — and the terminal stays silent (no email, because sendResetLink no-ops on an unknown address). The attacker learns nothing about who has an account.