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.
177 lines
5.3 KiB
Go
177 lines
5.3 KiB
Go
package lobby
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/ryanhamamura/games/connect4"
|
|
"github.com/ryanhamamura/games/db/repository"
|
|
lobbycomponents "github.com/ryanhamamura/games/features/lobby/components"
|
|
"github.com/ryanhamamura/games/features/lobby/pages"
|
|
appsessions "github.com/ryanhamamura/games/sessions"
|
|
"github.com/ryanhamamura/games/snake"
|
|
|
|
"github.com/alexedwards/scs/v2"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/starfederation/datastar-go/datastar"
|
|
)
|
|
|
|
// HandleLobbyPage renders the main lobby page with active games for logged-in users.
|
|
func HandleLobbyPage(queries *repository.Queries, sessions *scs.SessionManager, snakeStore *snake.SnakeStore) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
userID := sessions.GetString(r.Context(), appsessions.KeyUserID)
|
|
username := sessions.GetString(r.Context(), "username")
|
|
isLoggedIn := userID != ""
|
|
|
|
var userGames []lobbycomponents.GameListItem
|
|
if isLoggedIn {
|
|
ctx := context.Background()
|
|
games, err := queries.GetUserActiveGames(ctx, &userID)
|
|
if err == nil {
|
|
for _, g := range games {
|
|
isMyTurn := g.Status == 1 && g.CurrentTurn == g.MyColor
|
|
opponentName := ""
|
|
if g.OpponentNickname != nil {
|
|
opponentName = *g.OpponentNickname
|
|
}
|
|
var lastPlayed time.Time
|
|
if g.UpdatedAt != nil {
|
|
lastPlayed = *g.UpdatedAt
|
|
}
|
|
userGames = append(userGames, lobbycomponents.GameListItem{
|
|
ID: g.ID,
|
|
Status: int(g.Status),
|
|
OpponentName: opponentName,
|
|
IsMyTurn: isMyTurn,
|
|
LastPlayed: lastPlayed,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
var activeSnakeGames []pages.SnakeGameListItem
|
|
for _, g := range snakeStore.ActiveGames() {
|
|
statusLabel := "Waiting"
|
|
if g.Status == snake.StatusCountdown {
|
|
statusLabel = "Starting soon"
|
|
}
|
|
activeSnakeGames = append(activeSnakeGames, pages.SnakeGameListItem{
|
|
ID: g.ID,
|
|
Width: g.State.Width,
|
|
Height: g.State.Height,
|
|
PlayerCount: g.PlayerCount(),
|
|
StatusLabel: statusLabel,
|
|
})
|
|
}
|
|
|
|
data := pages.LobbyData{
|
|
IsLoggedIn: isLoggedIn,
|
|
Username: username,
|
|
UserGames: userGames,
|
|
ActiveSnakeGames: activeSnakeGames,
|
|
}
|
|
|
|
if err := pages.LobbyPage(data).Render(r.Context(), w); err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
// HandleCreateGame reads the nickname signal, creates a connect4 game, and redirects via SSE.
|
|
func HandleCreateGame(store *connect4.Store, sessions *scs.SessionManager) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
type Signals struct {
|
|
Nickname string `json:"nickname"`
|
|
}
|
|
signals := &Signals{}
|
|
if err := datastar.ReadSignals(r, signals); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if signals.Nickname == "" {
|
|
return
|
|
}
|
|
|
|
sessions.Put(r.Context(), appsessions.KeyNickname, signals.Nickname)
|
|
|
|
gi := store.Create()
|
|
sse := datastar.NewSSE(w, r)
|
|
sse.ExecuteScript(fmt.Sprintf("window.location.href='/games/%s'", gi.ID())) //nolint:errcheck
|
|
}
|
|
}
|
|
|
|
// HandleDeleteGame deletes a connect4 game and redirects to the lobby.
|
|
func HandleDeleteGame(store *connect4.Store, sessions *scs.SessionManager) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
gameID := chi.URLParam(r, "id")
|
|
if gameID == "" {
|
|
http.Error(w, "missing game id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
store.Delete(gameID) //nolint:errcheck
|
|
|
|
sse := datastar.NewSSE(w, r)
|
|
sse.ExecuteScript("window.location.href='/'") //nolint:errcheck
|
|
}
|
|
}
|
|
|
|
// HandleCreateSnakeGame reads nickname, grid preset, speed, and mode from the request,
|
|
// creates a snake game, and redirects via SSE.
|
|
func HandleCreateSnakeGame(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
type Signals struct {
|
|
Nickname string `json:"nickname"`
|
|
SelectedSpeed int `json:"selectedSpeed"`
|
|
}
|
|
signals := &Signals{}
|
|
if err := datastar.ReadSignals(r, signals); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if signals.Nickname == "" {
|
|
return
|
|
}
|
|
|
|
sessions.Put(r.Context(), appsessions.KeyNickname, signals.Nickname)
|
|
|
|
mode := snake.ModeMultiplayer
|
|
if r.URL.Query().Get("mode") == "solo" {
|
|
mode = snake.ModeSinglePlayer
|
|
}
|
|
|
|
presetIdx, _ := strconv.Atoi(r.URL.Query().Get("preset"))
|
|
if presetIdx < 0 || presetIdx >= len(snake.GridPresets) {
|
|
presetIdx = 0
|
|
}
|
|
preset := snake.GridPresets[presetIdx]
|
|
|
|
speed := snake.DefaultSpeed
|
|
if signals.SelectedSpeed >= 0 && signals.SelectedSpeed < len(snake.SpeedPresets) {
|
|
speed = snake.SpeedPresets[signals.SelectedSpeed].Speed
|
|
}
|
|
|
|
si := snakeStore.Create(preset.Width, preset.Height, mode, speed)
|
|
|
|
sse := datastar.NewSSE(w, r)
|
|
sse.ExecuteScript(fmt.Sprintf("window.location.href='/snake/%s'", si.ID())) //nolint:errcheck
|
|
}
|
|
}
|
|
|
|
// HandleLogout clears the session and redirects to the lobby.
|
|
func HandleLogout(sessions *scs.SessionManager) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if err := sessions.Destroy(r.Context()); err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
}
|