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.
This commit is contained in:
Ryan Hamamura
2026-03-02 19:16:09 -10:00
parent 063b03ce25
commit 7eadfbbb0c
3 changed files with 67 additions and 79 deletions

View File

@@ -18,10 +18,10 @@ import (
"github.com/ryanhamamura/c4/features/c4game/components" "github.com/ryanhamamura/c4/features/c4game/components"
"github.com/ryanhamamura/c4/features/c4game/pages" "github.com/ryanhamamura/c4/features/c4game/pages"
"github.com/ryanhamamura/c4/game" "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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -31,29 +31,20 @@ func HandleGamePage(store *game.GameStore, sessions *scs.SessionManager, queries
return return
} }
playerID := player.ID(sessions.GetString(r.Context(), "player_id")) playerID := sessions.GetPlayerID(sm, r)
if playerID == "" { userID := sessions.GetUserID(sm, r)
playerID = player.ID(player.GenerateID(8)) nickname := sessions.GetNickname(sm, r)
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")
// Auto-join if player has a nickname but isn't in the game yet // Auto-join if player has a nickname but isn't in the game yet
if nickname != "" && gi.GetPlayerColor(playerID) == 0 { if nickname != "" && gi.GetPlayerColor(playerID) == 0 {
player := &game.Player{ p := &game.Player{
ID: playerID, ID: playerID,
Nickname: nickname, Nickname: nickname,
} }
if userID != "" { if userID != "" {
player.UserID = &userID p.UserID = &userID
} }
gi.Join(&game.PlayerSession{Player: player}) gi.Join(&game.PlayerSession{Player: p})
} }
myColor := gi.GetPlayerColor(playerID) 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -96,12 +87,7 @@ func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sessions *scs.Sessio
return return
} }
playerID := player.ID(sessions.GetString(r.Context(), "player_id")) playerID := sessions.GetPlayerID(sm, r)
userID := sessions.GetString(r.Context(), "user_id")
if userID != "" {
playerID = player.ID(userID)
}
myColor := gi.GetPlayerColor(playerID) myColor := gi.GetPlayerColor(playerID)
sse := datastar.NewSSE(w, r, datastar.WithCompression( 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -186,12 +172,7 @@ func HandleDropPiece(store *game.GameStore, sessions *scs.SessionManager) http.H
return return
} }
playerID := player.ID(sessions.GetString(r.Context(), "player_id")) playerID := sessions.GetPlayerID(sm, r)
userID := sessions.GetString(r.Context(), "user_id")
if userID != "" {
playerID = player.ID(userID)
}
myColor := gi.GetPlayerColor(playerID) myColor := gi.GetPlayerColor(playerID)
if myColor == 0 { if myColor == 0 {
http.Error(w, "not in game", http.StatusForbidden) 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -230,12 +211,7 @@ func HandleSendChat(store *game.GameStore, nc *nats.Conn, sessions *scs.SessionM
return return
} }
playerID := player.ID(sessions.GetString(r.Context(), "player_id")) playerID := sessions.GetPlayerID(sm, r)
userID := sessions.GetString(r.Context(), "user_id")
if userID != "" {
playerID = player.ID(userID)
}
myColor := gi.GetPlayerColor(playerID) myColor := gi.GetPlayerColor(playerID)
if myColor == 0 { if myColor == 0 {
datastar.NewSSE(w, r) 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -297,23 +273,20 @@ func HandleSetNickname(store *game.GameStore, sessions *scs.SessionManager) http
return return
} }
sessions.Put(r.Context(), "nickname", signals.Nickname) sm.Put(r.Context(), "nickname", signals.Nickname)
playerID := player.ID(sessions.GetString(r.Context(), "player_id")) playerID := sessions.GetPlayerID(sm, r)
userID := sessions.GetString(r.Context(), "user_id") userID := sessions.GetUserID(sm, r)
if userID != "" {
playerID = player.ID(userID)
}
if gi.GetPlayerColor(playerID) == 0 { if gi.GetPlayerColor(playerID) == 0 {
player := &game.Player{ p := &game.Player{
ID: playerID, ID: playerID,
Nickname: signals.Nickname, Nickname: signals.Nickname,
} }
if userID != "" { 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) 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")

View File

@@ -13,24 +13,11 @@ import (
"github.com/ryanhamamura/c4/features/snakegame/components" "github.com/ryanhamamura/c4/features/snakegame/components"
"github.com/ryanhamamura/c4/features/snakegame/pages" "github.com/ryanhamamura/c4/features/snakegame/pages"
"github.com/ryanhamamura/c4/player" "github.com/ryanhamamura/c4/sessions"
"github.com/ryanhamamura/c4/snake" "github.com/ryanhamamura/c4/snake"
) )
func getPlayerID(sessions *scs.SessionManager, r *http.Request) player.ID { func HandleSnakePage(snakeStore *snake.SnakeStore, sm *scs.SessionManager) http.HandlerFunc {
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 {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
si, ok := snakeStore.Get(gameID) si, ok := snakeStore.Get(gameID)
@@ -39,9 +26,9 @@ func HandleSnakePage(snakeStore *snake.SnakeStore, sessions *scs.SessionManager)
return return
} }
playerID := getPlayerID(sessions, r) playerID := sessions.GetPlayerID(sm, r)
nickname := sessions.GetString(r.Context(), "nickname") nickname := sessions.GetNickname(sm, r)
userID := sessions.GetString(r.Context(), "user_id") userID := sessions.GetUserID(sm, r)
// Auto-join if nickname exists and not already in game // Auto-join if nickname exists and not already in game
if nickname != "" && si.GetPlayerSlot(playerID) < 0 { 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
si, ok := snakeStore.Get(gameID) si, ok := snakeStore.Get(gameID)
@@ -88,7 +75,7 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sessions *sc
return return
} }
playerID := getPlayerID(sessions, r) playerID := sessions.GetPlayerID(sm, r)
mySlot := si.GetPlayerSlot(playerID) mySlot := si.GetPlayerSlot(playerID)
sse := datastar.NewSSE(w, r, datastar.WithCompression( 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
si, ok := snakeStore.Get(gameID) si, ok := snakeStore.Get(gameID)
@@ -196,7 +183,7 @@ func HandleSetDirection(snakeStore *snake.SnakeStore, sessions *scs.SessionManag
return return
} }
playerID := getPlayerID(sessions, r) playerID := sessions.GetPlayerID(sm, r)
slot := si.GetPlayerSlot(playerID) slot := si.GetPlayerSlot(playerID)
if slot < 0 { if slot < 0 {
http.Error(w, "not in game", http.StatusForbidden) http.Error(w, "not in game", http.StatusForbidden)
@@ -219,7 +206,7 @@ type chatSignals struct {
ChatMsg string `json:"chatMsg"` 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
si, ok := snakeStore.Get(gameID) si, ok := snakeStore.Get(gameID)
@@ -238,7 +225,7 @@ func HandleSendChat(snakeStore *snake.SnakeStore, nc *nats.Conn, sessions *scs.S
return return
} }
playerID := getPlayerID(sessions, r) playerID := sessions.GetPlayerID(sm, r)
slot := si.GetPlayerSlot(playerID) slot := si.GetPlayerSlot(playerID)
if slot < 0 { if slot < 0 {
http.Error(w, "not in game", http.StatusForbidden) http.Error(w, "not in game", http.StatusForbidden)
@@ -266,7 +253,7 @@ type nicknameSignals struct {
Nickname string `json:"nickname"` 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
si, ok := snakeStore.Get(gameID) si, ok := snakeStore.Get(gameID)
@@ -285,10 +272,10 @@ func HandleSetNickname(snakeStore *snake.SnakeStore, sessions *scs.SessionManage
return return
} }
sessions.Put(r.Context(), "nickname", signals.Nickname) sm.Put(r.Context(), "nickname", signals.Nickname)
playerID := getPlayerID(sessions, r) playerID := sessions.GetPlayerID(sm, r)
userID := sessions.GetString(r.Context(), "user_id") userID := sessions.GetUserID(sm, r)
if si.GetPlayerSlot(playerID) < 0 { if si.GetPlayerSlot(playerID) < 0 {
player := &snake.Player{ 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) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
si, ok := snakeStore.Get(gameID) si, ok := snakeStore.Get(gameID)

View File

@@ -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 package sessions
import ( import (
@@ -7,6 +8,8 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/ryanhamamura/c4/player"
"github.com/alexedwards/scs/sqlite3store" "github.com/alexedwards/scs/sqlite3store"
"github.com/alexedwards/scs/v2" "github.com/alexedwards/scs/v2"
) )
@@ -30,3 +33,28 @@ func SetupSessionManager(db *sql.DB) (*scs.SessionManager, func()) {
return sessionManager, cleanup 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")
}