From 7eadfbbb0c075e31540605d5c5e19bf46b4bb746 Mon Sep 17 00:00:00 2001 From: Ryan Hamamura <58859899+ryanhamamura@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:16:09 -1000 Subject: [PATCH] refactor: extract session helpers for player identity resolution Add GetPlayerID, GetUserID, GetNickname to the sessions package. Remove the inline player-ID-from-session pattern duplicated across every handler in c4game and snakegame, and the local getPlayerID helper in snakegame. --- features/c4game/handlers.go | 71 +++++++++++----------------------- features/snakegame/handlers.go | 45 ++++++++------------- sessions/sessions.go | 30 +++++++++++++- 3 files changed, 67 insertions(+), 79 deletions(-) diff --git a/features/c4game/handlers.go b/features/c4game/handlers.go index 8b8b251..c47551b 100644 --- a/features/c4game/handlers.go +++ b/features/c4game/handlers.go @@ -18,10 +18,10 @@ import ( "github.com/ryanhamamura/c4/features/c4game/components" "github.com/ryanhamamura/c4/features/c4game/pages" "github.com/ryanhamamura/c4/game" - "github.com/ryanhamamura/c4/player" + "github.com/ryanhamamura/c4/sessions" ) -func HandleGamePage(store *game.GameStore, sessions *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { +func HandleGamePage(store *game.GameStore, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") @@ -31,29 +31,20 @@ func HandleGamePage(store *game.GameStore, sessions *scs.SessionManager, queries return } - playerID := player.ID(sessions.GetString(r.Context(), "player_id")) - if playerID == "" { - playerID = player.ID(player.GenerateID(8)) - sessions.Put(r.Context(), "player_id", string(playerID)) - } - - userID := sessions.GetString(r.Context(), "user_id") - if userID != "" { - playerID = player.ID(userID) - } - - nickname := sessions.GetString(r.Context(), "nickname") + playerID := sessions.GetPlayerID(sm, r) + userID := sessions.GetUserID(sm, r) + nickname := sessions.GetNickname(sm, r) // Auto-join if player has a nickname but isn't in the game yet if nickname != "" && gi.GetPlayerColor(playerID) == 0 { - player := &game.Player{ + p := &game.Player{ ID: playerID, Nickname: nickname, } if userID != "" { - player.UserID = &userID + p.UserID = &userID } - gi.Join(&game.PlayerSession{Player: player}) + gi.Join(&game.PlayerSession{Player: p}) } myColor := gi.GetPlayerColor(playerID) @@ -86,7 +77,7 @@ func HandleGamePage(store *game.GameStore, sessions *scs.SessionManager, queries } } -func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sessions *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { +func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") @@ -96,12 +87,7 @@ func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sessions *scs.Sessio return } - playerID := player.ID(sessions.GetString(r.Context(), "player_id")) - userID := sessions.GetString(r.Context(), "user_id") - if userID != "" { - playerID = player.ID(userID) - } - + playerID := sessions.GetPlayerID(sm, r) myColor := gi.GetPlayerColor(playerID) sse := datastar.NewSSE(w, r, datastar.WithCompression( @@ -169,7 +155,7 @@ func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sessions *scs.Sessio } } -func HandleDropPiece(store *game.GameStore, sessions *scs.SessionManager) http.HandlerFunc { +func HandleDropPiece(store *game.GameStore, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") @@ -186,12 +172,7 @@ func HandleDropPiece(store *game.GameStore, sessions *scs.SessionManager) http.H return } - playerID := player.ID(sessions.GetString(r.Context(), "player_id")) - userID := sessions.GetString(r.Context(), "user_id") - if userID != "" { - playerID = player.ID(userID) - } - + playerID := sessions.GetPlayerID(sm, r) myColor := gi.GetPlayerColor(playerID) if myColor == 0 { http.Error(w, "not in game", http.StatusForbidden) @@ -206,7 +187,7 @@ func HandleDropPiece(store *game.GameStore, sessions *scs.SessionManager) http.H } } -func HandleSendChat(store *game.GameStore, nc *nats.Conn, sessions *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { +func HandleSendChat(store *game.GameStore, nc *nats.Conn, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") @@ -230,12 +211,7 @@ func HandleSendChat(store *game.GameStore, nc *nats.Conn, sessions *scs.SessionM return } - playerID := player.ID(sessions.GetString(r.Context(), "player_id")) - userID := sessions.GetString(r.Context(), "user_id") - if userID != "" { - playerID = player.ID(userID) - } - + playerID := sessions.GetPlayerID(sm, r) myColor := gi.GetPlayerColor(playerID) if myColor == 0 { datastar.NewSSE(w, r) @@ -272,7 +248,7 @@ func HandleSendChat(store *game.GameStore, nc *nats.Conn, sessions *scs.SessionM } } -func HandleSetNickname(store *game.GameStore, sessions *scs.SessionManager) http.HandlerFunc { +func HandleSetNickname(store *game.GameStore, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") @@ -297,23 +273,20 @@ func HandleSetNickname(store *game.GameStore, sessions *scs.SessionManager) http return } - sessions.Put(r.Context(), "nickname", signals.Nickname) + sm.Put(r.Context(), "nickname", signals.Nickname) - playerID := player.ID(sessions.GetString(r.Context(), "player_id")) - userID := sessions.GetString(r.Context(), "user_id") - if userID != "" { - playerID = player.ID(userID) - } + playerID := sessions.GetPlayerID(sm, r) + userID := sessions.GetUserID(sm, r) if gi.GetPlayerColor(playerID) == 0 { - player := &game.Player{ + p := &game.Player{ ID: playerID, Nickname: signals.Nickname, } if userID != "" { - player.UserID = &userID + p.UserID = &userID } - gi.Join(&game.PlayerSession{Player: player}) + gi.Join(&game.PlayerSession{Player: p}) } sse := datastar.NewSSE(w, r) @@ -321,7 +294,7 @@ func HandleSetNickname(store *game.GameStore, sessions *scs.SessionManager) http } } -func HandleRematch(store *game.GameStore, sessions *scs.SessionManager) http.HandlerFunc { +func HandleRematch(store *game.GameStore, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") diff --git a/features/snakegame/handlers.go b/features/snakegame/handlers.go index 3fa0143..b72fcc1 100644 --- a/features/snakegame/handlers.go +++ b/features/snakegame/handlers.go @@ -13,24 +13,11 @@ import ( "github.com/ryanhamamura/c4/features/snakegame/components" "github.com/ryanhamamura/c4/features/snakegame/pages" - "github.com/ryanhamamura/c4/player" + "github.com/ryanhamamura/c4/sessions" "github.com/ryanhamamura/c4/snake" ) -func getPlayerID(sessions *scs.SessionManager, r *http.Request) player.ID { - pid := sessions.GetString(r.Context(), "player_id") - if pid == "" { - pid = player.GenerateID(8) - sessions.Put(r.Context(), "player_id", pid) - } - userID := sessions.GetString(r.Context(), "user_id") - if userID != "" { - return player.ID(userID) - } - return player.ID(pid) -} - -func HandleSnakePage(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) http.HandlerFunc { +func HandleSnakePage(snakeStore *snake.SnakeStore, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") si, ok := snakeStore.Get(gameID) @@ -39,9 +26,9 @@ func HandleSnakePage(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) return } - playerID := getPlayerID(sessions, r) - nickname := sessions.GetString(r.Context(), "nickname") - userID := sessions.GetString(r.Context(), "user_id") + playerID := sessions.GetPlayerID(sm, r) + nickname := sessions.GetNickname(sm, r) + userID := sessions.GetUserID(sm, r) // Auto-join if nickname exists and not already in game if nickname != "" && si.GetPlayerSlot(playerID) < 0 { @@ -79,7 +66,7 @@ func HandleSnakePage(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) } } -func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sessions *scs.SessionManager) http.HandlerFunc { +func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") si, ok := snakeStore.Get(gameID) @@ -88,7 +75,7 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sessions *sc return } - playerID := getPlayerID(sessions, r) + playerID := sessions.GetPlayerID(sm, r) mySlot := si.GetPlayerSlot(playerID) sse := datastar.NewSSE(w, r, datastar.WithCompression( @@ -187,7 +174,7 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sessions *sc } } -func HandleSetDirection(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) http.HandlerFunc { +func HandleSetDirection(snakeStore *snake.SnakeStore, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") si, ok := snakeStore.Get(gameID) @@ -196,7 +183,7 @@ func HandleSetDirection(snakeStore *snake.SnakeStore, sessions *scs.SessionManag return } - playerID := getPlayerID(sessions, r) + playerID := sessions.GetPlayerID(sm, r) slot := si.GetPlayerSlot(playerID) if slot < 0 { http.Error(w, "not in game", http.StatusForbidden) @@ -219,7 +206,7 @@ type chatSignals struct { ChatMsg string `json:"chatMsg"` } -func HandleSendChat(snakeStore *snake.SnakeStore, nc *nats.Conn, sessions *scs.SessionManager) http.HandlerFunc { +func HandleSendChat(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") si, ok := snakeStore.Get(gameID) @@ -238,7 +225,7 @@ func HandleSendChat(snakeStore *snake.SnakeStore, nc *nats.Conn, sessions *scs.S return } - playerID := getPlayerID(sessions, r) + playerID := sessions.GetPlayerID(sm, r) slot := si.GetPlayerSlot(playerID) if slot < 0 { http.Error(w, "not in game", http.StatusForbidden) @@ -266,7 +253,7 @@ type nicknameSignals struct { Nickname string `json:"nickname"` } -func HandleSetNickname(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) http.HandlerFunc { +func HandleSetNickname(snakeStore *snake.SnakeStore, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") si, ok := snakeStore.Get(gameID) @@ -285,10 +272,10 @@ func HandleSetNickname(snakeStore *snake.SnakeStore, sessions *scs.SessionManage return } - sessions.Put(r.Context(), "nickname", signals.Nickname) + sm.Put(r.Context(), "nickname", signals.Nickname) - playerID := getPlayerID(sessions, r) - userID := sessions.GetString(r.Context(), "user_id") + playerID := sessions.GetPlayerID(sm, r) + userID := sessions.GetUserID(sm, r) if si.GetPlayerSlot(playerID) < 0 { player := &snake.Player{ @@ -306,7 +293,7 @@ func HandleSetNickname(snakeStore *snake.SnakeStore, sessions *scs.SessionManage } } -func HandleRematch(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) http.HandlerFunc { +func HandleRematch(snakeStore *snake.SnakeStore, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { gameID := chi.URLParam(r, "id") si, ok := snakeStore.Get(gameID) diff --git a/sessions/sessions.go b/sessions/sessions.go index 5b52bb4..c4e50d6 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -1,4 +1,5 @@ -// Package sessions configures the SCS session manager backed by SQLite. +// Package sessions configures the SCS session manager and provides +// helpers for resolving player identity from the session. package sessions import ( @@ -7,6 +8,8 @@ import ( "net/http" "time" + "github.com/ryanhamamura/c4/player" + "github.com/alexedwards/scs/sqlite3store" "github.com/alexedwards/scs/v2" ) @@ -30,3 +33,28 @@ func SetupSessionManager(db *sql.DB) (*scs.SessionManager, func()) { return sessionManager, cleanup } + +// GetPlayerID returns the current player's identity from the session. +// Authenticated users get their user UUID; guests get a random ID that +// is generated and persisted on first access. +func GetPlayerID(sm *scs.SessionManager, r *http.Request) player.ID { + pid := sm.GetString(r.Context(), "player_id") + if pid == "" { + pid = player.GenerateID(8) + sm.Put(r.Context(), "player_id", pid) + } + if userID := sm.GetString(r.Context(), "user_id"); userID != "" { + return player.ID(userID) + } + return player.ID(pid) +} + +// GetUserID returns the authenticated user's UUID, or empty string for guests. +func GetUserID(sm *scs.SessionManager, r *http.Request) string { + return sm.GetString(r.Context(), "user_id") +} + +// GetNickname returns the player's display name from the session. +func GetNickname(sm *scs.SessionManager, r *http.Request) string { + return sm.GetString(r.Context(), "nickname") +}