mirror of
https://github.com/rjNemo/auth
synced 2026-06-06 00:16:40 +00:00
refactor: standardize page handlers and templates
This commit is contained in:
parent
d642716fd0
commit
27e819ecba
12 changed files with 101 additions and 96 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
internal/server/handler_login.go
Normal file
66
internal/server/handler_login.go
Normal 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))
|
||||||
|
}
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue