refactor: standardize page handlers and templates

This commit is contained in:
Ruidy 2025-09-20 13:31:17 +02:00
parent d642716fd0
commit 27e819ecba
No known key found for this signature in database
GPG key ID: 705C24D202990805
12 changed files with 101 additions and 96 deletions

View file

@ -20,7 +20,7 @@ Implement email/password authentication with secure password hashing, CSRF prote
## Coding Style & Naming Conventions
Trust `gofmt`; avoid manual formatting. Use CamelCase for exported Go identifiers and snake_case for embedded assets. Keep handlers slim, factor shared logic into helpers, and add concise comments only when intent needs clarification. Promote named constants/variables over magic numbers or strings. Template IDs and Alpine component names should reflect their role (e.g., `login_form`).
Trust `gofmt`; avoid manual formatting. Use CamelCase for exported Go identifiers and snake_case for embedded assets. Keep handlers slim, factor shared logic into helpers, and add concise comments only when intent needs clarification. Promote named constants/variables over magic numbers or strings. Template IDs and Alpine component names should reflect their role (e.g., `login_form`). Name handlers that render full pages with a `PageHandler` suffix and reserve the plain `Handler` suffix for non-page actions.
## Testing Guidelines

View file

@ -10,7 +10,7 @@ import (
const dashboardTimeDisplayLayout = "02 Jan 2006 15:04 MST"
func (s *Server) dashboardHandler() http.HandlerFunc {
func (s *Server) dashboardPageHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context())
@ -37,6 +37,6 @@ func (s *Server) dashboardHandler() http.HandlerFunc {
createdAtISO := account.CreatedAt.Format(time.RFC3339)
createdAtDisplay := account.CreatedAt.Format(dashboardTimeDisplayLayout)
s.render(w, "in.html", newDashboardData(state.Email, state.CSRFToken, createdAtDisplay, createdAtISO))
s.render(w, "dashboard.html", newDashboardData(state.Email, state.CSRFToken, createdAtDisplay, createdAtISO))
}
}

View file

@ -0,0 +1,66 @@
package server
import (
"errors"
"log"
"net/http"
"github.com/rjnemo/auth/internal/service/auth"
)
func (s *Server) loginPageHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context())
if state.Authenticated {
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return
}
s.render(w, "login.html", newLoginData(state.Email, "", state.CSRFToken))
}
}
func (s *Server) loginHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context())
if err := r.ParseForm(); err != nil {
http.Error(w, "invalid form submission", http.StatusBadRequest)
return
}
emailInput := r.FormValue("email")
password := r.FormValue("password")
email, err := auth.NewUserEmail(emailInput)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
s.render(w, "login.html", newLoginData("", credentialRequiredMsg, state.CSRFToken))
return
}
account, err := s.authService.Authenticate(r.Context(), email, password)
switch {
case err == nil:
state.Authenticated = true
state.Email = account.Email.String()
if err := s.sessions.Save(w, state); err != nil {
log.Printf("session: save failed: %v", err)
}
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
case errors.Is(err, auth.ErrInvalidInput):
w.WriteHeader(http.StatusBadRequest)
s.render(w, "login.html", newLoginData(email.String(), credentialRequiredMsg, state.CSRFToken))
case errors.Is(err, auth.ErrInvalidCredentials):
s.renderLoginFailure(w, email, state.CSRFToken)
default:
log.Printf("auth: authenticate failed: %v", err)
http.Error(w, "unexpected error", http.StatusInternalServerError)
}
}
}
func (s *Server) renderLoginFailure(w http.ResponseWriter, email auth.UserEmail, token string) {
w.WriteHeader(http.StatusUnauthorized)
s.render(w, "login.html", newLoginData(email.String(), invalidCredentialsMsg, token))
}

View file

@ -1,14 +0,0 @@
package server
import "net/http"
func (s *Server) indexHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context())
if state.Authenticated {
http.Redirect(w, r, "/in", http.StatusSeeOther)
return
}
s.render(w, "index.html", newIndexData(state.Email, "", state.CSRFToken))
}
}

View file

@ -14,12 +14,12 @@ const (
duplicateEmailMsg = "An account with that email already exists."
)
func (s *Server) signupHandler() http.HandlerFunc {
func (s *Server) signupPageHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context())
if state.Authenticated {
http.Redirect(w, r, "/in", http.StatusSeeOther)
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return
}
@ -27,60 +27,7 @@ func (s *Server) signupHandler() http.HandlerFunc {
}
}
func (s *Server) loginHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context())
if err := r.ParseForm(); err != nil {
http.Error(w, "invalid form submission", http.StatusBadRequest)
return
}
emailInput := r.FormValue("email")
password := r.FormValue("password")
email, err := auth.NewUserEmail(emailInput)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
s.render(w, "index.html", newIndexData("", credentialRequiredMsg, state.CSRFToken))
return
}
account, err := s.authService.Authenticate(r.Context(), email, password)
switch {
case err == nil:
state.Authenticated = true
state.Email = account.Email.String()
if err := s.sessions.Save(w, state); err != nil {
log.Printf("session: save failed: %v", err)
}
http.Redirect(w, r, "/in", http.StatusSeeOther)
case errors.Is(err, auth.ErrInvalidInput):
w.WriteHeader(http.StatusBadRequest)
s.render(w, "index.html", newIndexData(email.String(), credentialRequiredMsg, state.CSRFToken))
case errors.Is(err, auth.ErrInvalidCredentials):
s.renderLoginFailure(w, email, state.CSRFToken)
default:
log.Printf("auth: authenticate failed: %v", err)
http.Error(w, "unexpected error", http.StatusInternalServerError)
}
}
}
func (s *Server) logoutHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
s.sessions.Clear(w)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
func (s *Server) renderLoginFailure(w http.ResponseWriter, email auth.UserEmail, token string) {
w.WriteHeader(http.StatusUnauthorized)
s.render(w, "index.html", newIndexData(email.String(), invalidCredentialsMsg, token))
}
func (s *Server) registerHandler() http.HandlerFunc {
func (s *Server) signupHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context())
@ -107,7 +54,7 @@ func (s *Server) registerHandler() http.HandlerFunc {
if err := s.sessions.Save(w, state); err != nil {
log.Printf("session: save failed: %v", err)
}
http.Redirect(w, r, "/in", http.StatusSeeOther)
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
case errors.Is(err, auth.ErrInvalidInput):
w.WriteHeader(http.StatusBadRequest)
s.render(w, "signup.html", newSignupData(email.String(), credentialRequiredMsg, state.CSRFToken))
@ -120,3 +67,10 @@ func (s *Server) registerHandler() http.HandlerFunc {
}
}
}
func (s *Server) logoutHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
s.sessions.Clear(w)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}

View file

@ -7,6 +7,8 @@ import (
"net/http"
)
type sessionContextKey struct{}
func (s *Server) sessionMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
state := s.sessions.Load(r)
@ -57,8 +59,6 @@ func (s *Server) csrfMiddleware(next http.Handler) http.Handler {
})
}
type sessionContextKey struct{}
func withSession(ctx context.Context, state SessionState) context.Context {
return context.WithValue(ctx, sessionContextKey{}, state)
}

View file

@ -1,13 +0,0 @@
package server
import (
"log"
"net/http"
)
func (s *Server) render(w http.ResponseWriter, name string, data any) {
if err := s.templates.ExecuteTemplate(w, name, data); err != nil {
log.Printf("render %s: %v", name, err)
http.Error(w, "template render failed", http.StatusInternalServerError)
}
}

View file

@ -8,12 +8,12 @@ import (
)
func (s *Server) registerRoutes(r chi.Router) {
r.Get("/", s.indexHandler())
r.Get("/", s.loginPageHandler())
r.Post("/login", s.loginHandler())
r.Post("/logout", s.logoutHandler())
r.Get("/signup", s.signupHandler())
r.Post("/signup", s.registerHandler())
r.Get("/in", s.dashboardHandler())
r.Get("/signup", s.signupPageHandler())
r.Post("/signup", s.signupHandler())
r.Get("/dashboard", s.dashboardPageHandler())
}
// Router returns the configured HTTP router.

View file

@ -27,8 +27,8 @@ type Server struct {
func New() (*Server, error) {
tmpl, err := template.ParseFS(
web.Templates,
"templates/index.html",
"templates/in.html",
"templates/login.html",
"templates/dashboard.html",
"templates/signup.html",
"templates/unauthorized.html",
)

View file

@ -1,5 +1,17 @@
package server
import (
"log"
"net/http"
)
func (s *Server) render(w http.ResponseWriter, name string, data any) {
if err := s.templates.ExecuteTemplate(w, name, data); err != nil {
log.Printf("render %s: %v", name, err)
http.Error(w, "template render failed", http.StatusInternalServerError)
}
}
// PageData contains fields shared by the templates for now.
type PageData struct {
Email string
@ -10,7 +22,7 @@ type PageData struct {
CreatedAtISO string
}
func newIndexData(email, errMsg, token string) PageData {
func newLoginData(email, errMsg, token string) PageData {
return PageData{Email: email, Error: errMsg, CSRFToken: token}
}