From 27e819ecba41b2c4fa995d840582a8abc2861b34 Mon Sep 17 00:00:00 2001 From: Ruidy Date: Sat, 20 Sep 2025 13:31:17 +0200 Subject: [PATCH] refactor: standardize page handlers and templates --- AGENTS.md | 2 +- internal/server/handler_dashboard.go | 4 +- internal/server/handler_login.go | 66 ++++++++++++++++++ internal/server/handler_public.go | 14 ---- .../{handler_auth.go => handler_signup.go} | 68 +++---------------- internal/server/middleware.go | 4 +- internal/server/render.go | 13 ---- internal/server/routes.go | 8 +-- internal/server/server.go | 4 +- internal/server/views.go | 14 +++- web/templates/{in.html => dashboard.html} | 0 web/templates/{index.html => login.html} | 0 12 files changed, 101 insertions(+), 96 deletions(-) create mode 100644 internal/server/handler_login.go delete mode 100644 internal/server/handler_public.go rename internal/server/{handler_auth.go => handler_signup.go} (54%) delete mode 100644 internal/server/render.go rename web/templates/{in.html => dashboard.html} (100%) rename web/templates/{index.html => login.html} (100%) diff --git a/AGENTS.md b/AGENTS.md index c6f2c2f..d3312d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/internal/server/handler_dashboard.go b/internal/server/handler_dashboard.go index c3e4b4f..24f629f 100644 --- a/internal/server/handler_dashboard.go +++ b/internal/server/handler_dashboard.go @@ -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)) } } diff --git a/internal/server/handler_login.go b/internal/server/handler_login.go new file mode 100644 index 0000000..a7a6130 --- /dev/null +++ b/internal/server/handler_login.go @@ -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)) +} diff --git a/internal/server/handler_public.go b/internal/server/handler_public.go deleted file mode 100644 index 08359c9..0000000 --- a/internal/server/handler_public.go +++ /dev/null @@ -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)) - } -} diff --git a/internal/server/handler_auth.go b/internal/server/handler_signup.go similarity index 54% rename from internal/server/handler_auth.go rename to internal/server/handler_signup.go index 77949e4..a7ebab7 100644 --- a/internal/server/handler_auth.go +++ b/internal/server/handler_signup.go @@ -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) + } +} diff --git a/internal/server/middleware.go b/internal/server/middleware.go index ccfe88f..800bf91 100644 --- a/internal/server/middleware.go +++ b/internal/server/middleware.go @@ -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) } diff --git a/internal/server/render.go b/internal/server/render.go deleted file mode 100644 index 592952a..0000000 --- a/internal/server/render.go +++ /dev/null @@ -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) - } -} diff --git a/internal/server/routes.go b/internal/server/routes.go index 0897fca..dbce1cf 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -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. diff --git a/internal/server/server.go b/internal/server/server.go index 48616a4..d31fae9 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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", ) diff --git a/internal/server/views.go b/internal/server/views.go index d015175..4e3392e 100644 --- a/internal/server/views.go +++ b/internal/server/views.go @@ -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} } diff --git a/web/templates/in.html b/web/templates/dashboard.html similarity index 100% rename from web/templates/in.html rename to web/templates/dashboard.html diff --git a/web/templates/index.html b/web/templates/login.html similarity index 100% rename from web/templates/index.html rename to web/templates/login.html