Code

Login, logout, the protected dashboard

Now bring it alive. Five files as tabs, dependency order: dashboard.templauth.templauth.gohandlers.gorouter.go (the protected page, the login form, the handlers, then the wiring). Four are new or modified from lesson 4.

1. internal/web/dashboard.templ (new) — the members-only page. It just greets nav.Email for now; the documents phase fills it in.

2. internal/web/auth.templ (modify) — Register gains the nav parameter, and a Login template joins it (email + password, one generic error slot).

3. internal/handlers/auth.go (modify) — the heart of the lesson:

func (h *Handlers) nav(r *http.Request) web.Nav { /* UserFrom → {LoggedIn, Email} */ }

func (h *Handlers) Login(w, r) {
	user, err := h.Queries.GetUserByEmail(...)         // unknown email → invalid()
	if !auth.CheckPassword(user.PasswordHash, pw) { invalid(); return }  // wrong pw → invalid()
	h.Sessions.Create(r.Context(), w, user.ID)          // same call register makes
	http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}

func (h *Handlers) Logout(w, r) {
	h.Sessions.Destroy(r.Context(), w, r)               // ← the one DELETE
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

nav() is new; Register now threads h.nav(r) and redirects to /dashboard; Dashboard renders the protected page. invalid() is the single generic error.

4. internal/handlers/handlers.go (modify) — Home gains one branch: a signed-in visitor is redirected to /dashboard instead of seeing the public page.

5. internal/handlers/router.go (modify) — three changes: r.Use(h.LoadUser) (annotate every request), the login/logout routes, and the authenticated group:

r.Group(func(pr chi.Router) {
	pr.Use(h.RequireAuth)
	pr.Get("/dashboard", h.Dashboard)
})

Verify. templ generate && go run ./cmd/signflow, then:

  1. Protected route, logged out. Visit http://localhost:8080/dashboard → you're bounced to /login. RequireAuth works.

  2. Log in. Use the account from lesson 4. You land on the dashboard, and the header now shows your email and a Log out button — LoadUser resolved your cookie, nav rendered the state.

  3. Log out. Click it. Back to the public home page, header shows Log in / Register. And server-side:

    $ psql signflow -c "SELECT count(*) FROM sessions;"
     count
    -------
         0          ← the row is gone. That's revocation a JWT can't do.
    
  4. Generic error. On /login, try a wrong password, then a nonexistent email. Both say "Invalid email or password." — identical, so neither reveals whether the account exists.

Gotcha (redirect after POST). Notice login/logout/register all end in http.StatusSeeOther (303) redirects, never rendering directly on the POST. That's the POST/redirect/GET pattern: it means a browser refresh re-requests the dashboard (a GET), not a re-submission of the login form. Without it, "reload" would replay the POST — a classic double-submit bug.