Login, logout, the protected dashboard
Now bring it alive. Five files as tabs, dependency order: dashboard.templ → auth.templ → auth.go → handlers.go → router.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:
-
Protected route, logged out. Visit
http://localhost:8080/dashboard→ you're bounced to/login.RequireAuthworks. -
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 —
LoadUserresolved your cookie,navrendered the state. -
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. -
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.