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 ## 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 ## Testing Guidelines

View file

@ -10,7 +10,7 @@ import (
const dashboardTimeDisplayLayout = "02 Jan 2006 15:04 MST" 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) { return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context()) state := sessionFromContext(r.Context())
@ -37,6 +37,6 @@ func (s *Server) dashboardHandler() http.HandlerFunc {
createdAtISO := account.CreatedAt.Format(time.RFC3339) createdAtISO := account.CreatedAt.Format(time.RFC3339)
createdAtDisplay := account.CreatedAt.Format(dashboardTimeDisplayLayout) 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." 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) { return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context()) state := sessionFromContext(r.Context())
if state.Authenticated { if state.Authenticated {
http.Redirect(w, r, "/in", http.StatusSeeOther) http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return return
} }
@ -27,60 +27,7 @@ func (s *Server) signupHandler() http.HandlerFunc {
} }
} }
func (s *Server) loginHandler() http.HandlerFunc { func (s *Server) signupHandler() 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 {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
state := sessionFromContext(r.Context()) state := sessionFromContext(r.Context())
@ -107,7 +54,7 @@ func (s *Server) registerHandler() http.HandlerFunc {
if err := s.sessions.Save(w, state); err != nil { if err := s.sessions.Save(w, state); err != nil {
log.Printf("session: save failed: %v", err) 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): case errors.Is(err, auth.ErrInvalidInput):
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
s.render(w, "signup.html", newSignupData(email.String(), credentialRequiredMsg, state.CSRFToken)) 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" "net/http"
) )
type sessionContextKey struct{}
func (s *Server) sessionMiddleware(next http.Handler) http.Handler { func (s *Server) sessionMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
state := s.sessions.Load(r) 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 { func withSession(ctx context.Context, state SessionState) context.Context {
return context.WithValue(ctx, sessionContextKey{}, state) 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) { func (s *Server) registerRoutes(r chi.Router) {
r.Get("/", s.indexHandler()) r.Get("/", s.loginPageHandler())
r.Post("/login", s.loginHandler()) r.Post("/login", s.loginHandler())
r.Post("/logout", s.logoutHandler()) r.Post("/logout", s.logoutHandler())
r.Get("/signup", s.signupHandler()) r.Get("/signup", s.signupPageHandler())
r.Post("/signup", s.registerHandler()) r.Post("/signup", s.signupHandler())
r.Get("/in", s.dashboardHandler()) r.Get("/dashboard", s.dashboardPageHandler())
} }
// Router returns the configured HTTP router. // Router returns the configured HTTP router.

View file

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

View file

@ -1,5 +1,17 @@
package server 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. // PageData contains fields shared by the templates for now.
type PageData struct { type PageData struct {
Email string Email string
@ -10,7 +22,7 @@ type PageData struct {
CreatedAtISO string CreatedAtISO string
} }
func newIndexData(email, errMsg, token string) PageData { func newLoginData(email, errMsg, token string) PageData {
return PageData{Email: email, Error: errMsg, CSRFToken: token} return PageData{Email: email, Error: errMsg, CSRFToken: token}
} }