fix: convert auth flows from SSE to standard HTTP to fix session cookies
Datastar's NewSSE() flushes HTTP headers before SCS's session middleware can attach the Set-Cookie header, so the session cookie never reaches the browser after login/register/logout. Convert login, register, and logout to standard HTML forms with HTTP redirects, which lets SCS write cookies normally. Also fix return_url capture on the login page (was never being stored in the session). Add handler tests covering login, register, and logout flows.
This commit is contained in:
@@ -3,10 +3,10 @@ package auth
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/starfederation/datastar-go/datastar"
|
||||
|
||||
"github.com/ryanhamamura/games/auth"
|
||||
"github.com/ryanhamamura/games/db/repository"
|
||||
@@ -14,20 +14,15 @@ import (
|
||||
appsessions "github.com/ryanhamamura/games/sessions"
|
||||
)
|
||||
|
||||
type LoginSignals struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"` //nolint:gosec // form input, not stored
|
||||
}
|
||||
|
||||
type RegisterSignals struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"` //nolint:gosec // form input, not stored
|
||||
Confirm string `json:"confirm"`
|
||||
}
|
||||
|
||||
func HandleLoginPage() http.HandlerFunc {
|
||||
func HandleLoginPage(sessions *scs.SessionManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := pages.LoginPage().Render(r.Context(), w); err != nil {
|
||||
// Capture return_url so we can redirect back after login
|
||||
if returnURL := r.URL.Query().Get("return_url"); returnURL != "" {
|
||||
sessions.Put(r.Context(), "return_url", returnURL)
|
||||
}
|
||||
|
||||
errorMsg := r.URL.Query().Get("error")
|
||||
if err := pages.LoginPage(errorMsg).Render(r.Context(), w); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +30,8 @@ func HandleLoginPage() http.HandlerFunc {
|
||||
|
||||
func HandleRegisterPage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := pages.RegisterPage().Render(r.Context(), w); err != nil {
|
||||
errorMsg := r.URL.Query().Get("error")
|
||||
if err := pages.RegisterPage(errorMsg).Render(r.Context(), w); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -43,25 +39,20 @@ func HandleRegisterPage() http.HandlerFunc {
|
||||
|
||||
func HandleLogin(queries *repository.Queries, sessions *scs.SessionManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var signals LoginSignals
|
||||
if err := datastar.ReadSignals(r, &signals); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
user, err := queries.GetUserByUsername(r.Context(), signals.Username)
|
||||
user, err := queries.GetUserByUsername(r.Context(), username)
|
||||
if err == sql.ErrNoRows {
|
||||
sse.MarshalAndPatchSignals(map[string]any{"error": "Invalid username or password"}) //nolint:errcheck
|
||||
http.Redirect(w, r, "/login?error="+url.QueryEscape("Invalid username or password"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
sse.MarshalAndPatchSignals(map[string]any{"error": "An error occurred"}) //nolint:errcheck
|
||||
http.Redirect(w, r, "/login?error="+url.QueryEscape("An error occurred"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if !auth.CheckPassword(signals.Password, user.PasswordHash) {
|
||||
sse.MarshalAndPatchSignals(map[string]any{"error": "Invalid username or password"}) //nolint:errcheck
|
||||
if !auth.CheckPassword(password, user.PasswordHash) {
|
||||
http.Redirect(w, r, "/login?error="+url.QueryEscape("Invalid username or password"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -76,46 +67,42 @@ func HandleLogin(queries *repository.Queries, sessions *scs.SessionManager) http
|
||||
redirectURL = returnURL
|
||||
}
|
||||
|
||||
sse.Redirect(redirectURL) //nolint:errcheck
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRegister(queries *repository.Queries, sessions *scs.SessionManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var signals RegisterSignals
|
||||
if err := datastar.ReadSignals(r, &signals); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
confirm := r.FormValue("confirm")
|
||||
|
||||
if err := auth.ValidateUsername(username); err != nil {
|
||||
http.Redirect(w, r, "/register?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if err := auth.ValidatePassword(password); err != nil {
|
||||
http.Redirect(w, r, "/register?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if password != confirm {
|
||||
http.Redirect(w, r, "/register?error="+url.QueryEscape("Passwords do not match"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
if err := auth.ValidateUsername(signals.Username); err != nil {
|
||||
sse.MarshalAndPatchSignals(map[string]any{"error": err.Error()}) //nolint:errcheck
|
||||
return
|
||||
}
|
||||
if err := auth.ValidatePassword(signals.Password); err != nil {
|
||||
sse.MarshalAndPatchSignals(map[string]any{"error": err.Error()}) //nolint:errcheck
|
||||
return
|
||||
}
|
||||
if signals.Password != signals.Confirm {
|
||||
sse.MarshalAndPatchSignals(map[string]any{"error": "Passwords do not match"}) //nolint:errcheck
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := auth.HashPassword(signals.Password)
|
||||
hash, err := auth.HashPassword(password)
|
||||
if err != nil {
|
||||
sse.MarshalAndPatchSignals(map[string]any{"error": "An error occurred"}) //nolint:errcheck
|
||||
http.Redirect(w, r, "/register?error="+url.QueryEscape("An error occurred"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := queries.CreateUser(r.Context(), repository.CreateUserParams{
|
||||
ID: uuid.New().String(),
|
||||
Username: signals.Username,
|
||||
Username: username,
|
||||
PasswordHash: hash,
|
||||
})
|
||||
if err != nil {
|
||||
sse.MarshalAndPatchSignals(map[string]any{"error": "Username already taken"}) //nolint:errcheck
|
||||
http.Redirect(w, r, "/register?error="+url.QueryEscape("Username already taken"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -130,6 +117,6 @@ func HandleRegister(queries *repository.Queries, sessions *scs.SessionManager) h
|
||||
redirectURL = returnURL
|
||||
}
|
||||
|
||||
sse.Redirect(redirectURL) //nolint:errcheck
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user