fix: serve datastar locally and clean up session/route config

Replace CDN-hosted datastar beta.11 with local v1.0.0-RC.7 to fix
client-side expression incompatibilities with the Go SDK. Also fix
quoted CSS class keys in data-class expressions, harden session cookie
settings (named cookie, Secure flag), simplify SetupRoutes to not
return an error, and regenerate templ output.
This commit is contained in:
Ryan Hamamura
2026-03-02 14:34:39 -10:00
parent 5120eef776
commit 587f392b8b
12 changed files with 36 additions and 46 deletions

9
assets/js/datastar.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -46,33 +46,33 @@ func LoginPage() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<main class=\"max-w-sm mx-auto mt-8 text-center\" data-signals=\"{username: '', password: '', error: ''}\"><h1 class=\"text-3xl font-bold\">Login</h1><p class=\"mb-4\">Sign in to your account</p><div data-show=\"$error != ''\" class=\"alert alert-error mb-4\" data-text=\"$error\"></div><form><fieldset class=\"fieldset\"><label class=\"label\" for=\"username\">Username</label> <input class=\"input input-bordered w-full\" id=\"username\" type=\"text\" placeholder=\"Enter your username\" data-bind=\"username\" required autofocus> <label class=\"label\" for=\"password\">Password</label> <input class=\"input input-bordered w-full\" id=\"password\" type=\"password\" placeholder=\"Enter your password\" data-bind=\"password\" required data-on:keydown.key_enter=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<main class=\"max-w-sm mx-auto mt-8 text-center\" data-signals=\"{username: '', password: '', error: ''}\"><h1 class=\"text-3xl font-bold\">Login</h1><p class=\"mb-4\">Sign in to your account</p><div data-show=\"$error != ''\" class=\"alert alert-error mb-4\" data-text=\"$error\"></div><div><fieldset class=\"fieldset\"><label class=\"label\" for=\"username\">Username</label> <input class=\"input input-bordered w-full\" id=\"username\" type=\"text\" placeholder=\"Enter your username\" data-bind=\"username\" autofocus> <label class=\"label\" for=\"password\">Password</label> <input class=\"input input-bordered w-full\" id=\"password\" type=\"password\" placeholder=\"Enter your password\" data-bind=\"password\" data-on:keydown.key_enter=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/auth/login")) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/auth/login"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/login.templ`, Line: 34, Col: 65} return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/login.templ`, Line: 32, Col: 65}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"></fieldset><button class=\"btn btn-primary w-full\" type=\"button\" data-on:click=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"></fieldset><button class=\"btn btn-primary w-full\" data-on:click=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/auth/login")) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/auth/login"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/login.templ`, Line: 40, Col: 52} return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/login.templ`, Line: 37, Col: 52}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">Login</button></form><p>Don't have an account? <a class=\"link\" href=\"/register\">Register</a></p></main>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">Login</button></div><p>Don't have an account? <a class=\"link\" href=\"/register\">Register</a></p></main>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -46,33 +46,33 @@ func RegisterPage() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<main class=\"max-w-sm mx-auto mt-8 text-center\" data-signals=\"{username: '', password: '', confirm: '', error: ''}\"><h1 class=\"text-3xl font-bold\">Register</h1><p class=\"mb-4\">Create a new account</p><div data-show=\"$error != ''\" class=\"alert alert-error mb-4\" data-text=\"$error\"></div><form><fieldset class=\"fieldset\"><label class=\"label\" for=\"username\">Username</label> <input class=\"input input-bordered w-full\" id=\"username\" type=\"text\" placeholder=\"Choose a username\" data-bind=\"username\" required autofocus> <label class=\"label\" for=\"password\">Password</label> <input class=\"input input-bordered w-full\" id=\"password\" type=\"password\" placeholder=\"Choose a password (min 8 chars)\" data-bind=\"password\" required> <label class=\"label\" for=\"confirm\">Confirm Password</label> <input class=\"input input-bordered w-full\" id=\"confirm\" type=\"password\" placeholder=\"Confirm your password\" data-bind=\"confirm\" required data-on:keydown.key_enter=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<main class=\"max-w-sm mx-auto mt-8 text-center\" data-signals=\"{username: '', password: '', confirm: '', error: ''}\"><h1 class=\"text-3xl font-bold\">Register</h1><p class=\"mb-4\">Create a new account</p><div data-show=\"$error != ''\" class=\"alert alert-error mb-4\" data-text=\"$error\"></div><div><fieldset class=\"fieldset\"><label class=\"label\" for=\"username\">Username</label> <input class=\"input input-bordered w-full\" id=\"username\" type=\"text\" placeholder=\"Choose a username\" data-bind=\"username\" autofocus> <label class=\"label\" for=\"password\">Password</label> <input class=\"input input-bordered w-full\" id=\"password\" type=\"password\" placeholder=\"Choose a password (min 8 chars)\" data-bind=\"password\"> <label class=\"label\" for=\"confirm\">Confirm Password</label> <input class=\"input input-bordered w-full\" id=\"confirm\" type=\"password\" placeholder=\"Confirm your password\" data-bind=\"confirm\" data-on:keydown.key_enter=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/auth/register")) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/auth/register"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/register.templ`, Line: 43, Col: 68} return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/register.templ`, Line: 40, Col: 68}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"></fieldset><button class=\"btn btn-primary w-full\" type=\"button\" data-on:click=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"></fieldset><button class=\"btn btn-primary w-full\" data-on:click=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/auth/register")) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/auth/register"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/register.templ`, Line: 49, Col: 55} return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/register.templ`, Line: 45, Col: 55}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">Register</button></form><p>Already have an account? <a class=\"link\" href=\"/login\">Login</a></p></main>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">Register</button></div><p>Already have an account? <a class=\"link\" href=\"/login\">Login</a></p></main>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -8,11 +8,9 @@ import (
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
) )
func SetupRoutes(router chi.Router, queries *repository.Queries, sessions *scs.SessionManager) error { func SetupRoutes(router chi.Router, queries *repository.Queries, sessions *scs.SessionManager) {
router.Get("/login", HandleLoginPage()) router.Get("/login", HandleLoginPage())
router.Get("/register", HandleRegisterPage()) router.Get("/register", HandleRegisterPage())
router.Post("/auth/login", HandleLogin(queries, sessions)) router.Post("/auth/login", HandleLogin(queries, sessions))
router.Post("/auth/register", HandleRegister(queries, sessions)) router.Post("/auth/register", HandleRegister(queries, sessions))
return nil
} }

View File

@@ -16,7 +16,7 @@ func SetupRoutes(
nc *nats.Conn, nc *nats.Conn,
sessions *scs.SessionManager, sessions *scs.SessionManager,
queries *repository.Queries, queries *repository.Queries,
) error { ) {
router.Route("/games/{id}", func(r chi.Router) { router.Route("/games/{id}", func(r chi.Router) {
r.Get("/", HandleGamePage(store, sessions, queries)) r.Get("/", HandleGamePage(store, sessions, queries))
r.Get("/events", HandleGameEvents(store, nc, sessions, queries)) r.Get("/events", HandleGameEvents(store, nc, sessions, queries))
@@ -25,6 +25,4 @@ func SetupRoutes(
r.Post("/join", HandleSetNickname(store, sessions)) r.Post("/join", HandleSetNickname(store, sessions))
r.Post("/rematch", HandleRematch(store, sessions)) r.Post("/rematch", HandleRematch(store, sessions))
}) })
return nil
} }

View File

@@ -44,7 +44,7 @@ func Base(title string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0\"><script defer type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@starfederation/datastar@1.0.0-beta.11/dist/datastar.min.js\"></script><link href=\"/assets/css/output.css\" rel=\"stylesheet\" type=\"text/css\"></head><body class=\"flex flex-col h-screen\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0\"><script defer type=\"module\" src=\"/assets/js/datastar.js\"></script><link href=\"/assets/css/output.css\" rel=\"stylesheet\" type=\"text/css\"></head><body class=\"flex flex-col h-screen\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -100,7 +100,7 @@ func LobbyPage(data LobbyData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h1><div class=\"tabs tabs-box mb-6 justify-center\"><button class=\"tab\" type=\"button\" data-class=\"{tab-active: $activeTab==='connect4'}\" data-on:click=\"$activeTab='connect4'\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h1><div class=\"tabs tabs-box mb-6 justify-center\"><button class=\"tab\" type=\"button\" data-class=\"{'tab-active': $activeTab==='connect4'}\" data-on:click=\"$activeTab='connect4'\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -108,7 +108,7 @@ func LobbyPage(data LobbyData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</button> <button class=\"tab\" type=\"button\" data-class=\"{tab-active: $activeTab==='snake'}\" data-on:click=\"$activeTab='snake'\">~~~~</button></div><div data-show=\"$activeTab==='connect4'\"><p class=\"mb-4\">Start a new session</p><form><fieldset class=\"fieldset\"><label class=\"label\" for=\"nickname\">Your Nickname</label> <input class=\"input input-bordered w-full\" id=\"nickname\" type=\"text\" placeholder=\"Enter your nickname\" data-bind=\"nickname\" required data-on:keydown.enter=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</button> <button class=\"tab\" type=\"button\" data-class=\"{'tab-active': $activeTab==='snake'}\" data-on:click=\"$activeTab='snake'\">~~~~</button></div><div data-show=\"$activeTab==='connect4'\"><p class=\"mb-4\">Start a new session</p><form><fieldset class=\"fieldset\"><label class=\"label\" for=\"nickname\">Your Nickname</label> <input class=\"input input-bordered w-full\" id=\"nickname\" type=\"text\" placeholder=\"Enter your nickname\" data-bind=\"nickname\" required data-on:keydown.enter=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -152,9 +152,9 @@ func LobbyPage(data LobbyData) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("{btn-active: $selectedSpeed===%d}", i)) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("{'btn-active': $selectedSpeed===%d}", i))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/lobby/pages/lobby.templ`, Line: 113, Col: 72} return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/lobby/pages/lobby.templ`, Line: 113, Col: 74}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {

View File

@@ -16,13 +16,11 @@ func SetupRoutes(
sessions *scs.SessionManager, sessions *scs.SessionManager,
store *game.GameStore, store *game.GameStore,
snakeStore *snake.SnakeStore, snakeStore *snake.SnakeStore,
) error { ) {
router.Get("/", HandleLobbyPage(queries, sessions, snakeStore)) router.Get("/", HandleLobbyPage(queries, sessions, snakeStore))
router.Post("/games", HandleCreateGame(store, sessions)) router.Post("/games", HandleCreateGame(store, sessions))
router.Delete("/games/{id}", HandleDeleteGame(store, sessions)) router.Delete("/games/{id}", HandleDeleteGame(store, sessions))
router.Post("/snake", HandleCreateSnakeGame(snakeStore, sessions)) router.Post("/snake", HandleCreateSnakeGame(snakeStore, sessions))
router.Post("/logout", HandleLogout(sessions)) router.Post("/logout", HandleLogout(sessions))
return nil
} }

View File

@@ -9,7 +9,7 @@ import (
"github.com/ryanhamamura/c4/snake" "github.com/ryanhamamura/c4/snake"
) )
func SetupRoutes(router chi.Router, snakeStore *snake.SnakeStore, nc *nats.Conn, sessions *scs.SessionManager) error { func SetupRoutes(router chi.Router, snakeStore *snake.SnakeStore, nc *nats.Conn, sessions *scs.SessionManager) {
router.Route("/snake/{id}", func(r chi.Router) { router.Route("/snake/{id}", func(r chi.Router) {
r.Get("/", HandleSnakePage(snakeStore, sessions)) r.Get("/", HandleSnakePage(snakeStore, sessions))
r.Get("/events", HandleSnakeEvents(snakeStore, nc, sessions)) r.Get("/events", HandleSnakeEvents(snakeStore, nc, sessions))
@@ -18,6 +18,4 @@ func SetupRoutes(router chi.Router, snakeStore *snake.SnakeStore, nc *nats.Conn,
r.Post("/join", HandleSetNickname(snakeStore, sessions)) r.Post("/join", HandleSetNickname(snakeStore, sessions))
r.Post("/rematch", HandleRematch(snakeStore, sessions)) r.Post("/rematch", HandleRematch(snakeStore, sessions))
}) })
return nil
} }

View File

@@ -90,9 +90,7 @@ func run(ctx context.Context) error {
sessionManager.LoadAndSave, sessionManager.LoadAndSave,
) )
if err := router.SetupRoutes(r, queries, sessionManager, nc, store, snakeStore, assets); err != nil { router.SetupRoutes(r, queries, sessionManager, nc, store, snakeStore, assets)
return fmt.Errorf("setting up routes: %w", err)
}
// HTTP server // HTTP server
srv := &http.Server{ srv := &http.Server{

View File

@@ -30,7 +30,7 @@ func SetupRoutes(
store *game.GameStore, store *game.GameStore,
snakeStore *snake.SnakeStore, snakeStore *snake.SnakeStore,
assets embed.FS, assets embed.FS,
) error { ) {
// Static assets // Static assets
subFS, _ := fs.Sub(assets, "assets") subFS, _ := fs.Sub(assets, "assets")
router.Handle("/assets/*", http.StripPrefix("/assets/", http.FileServerFS(subFS))) router.Handle("/assets/*", http.StripPrefix("/assets/", http.FileServerFS(subFS)))
@@ -40,20 +40,10 @@ func SetupRoutes(
setupReload(router) setupReload(router)
} }
if err := auth.SetupRoutes(router, queries, sessions); err != nil { auth.SetupRoutes(router, queries, sessions)
return err lobby.SetupRoutes(router, queries, sessions, store, snakeStore)
} c4game.SetupRoutes(router, store, nc, sessions, queries)
if err := lobby.SetupRoutes(router, queries, sessions, store, snakeStore); err != nil { snakegame.SetupRoutes(router, snakeStore, nc, sessions)
return err
}
if err := c4game.SetupRoutes(router, store, nc, sessions, queries); err != nil {
return err
}
if err := snakegame.SetupRoutes(router, snakeStore, nc, sessions); err != nil {
return err
}
return nil
} }
func setupReload(router chi.Router) { func setupReload(router chi.Router) {

View File

@@ -20,9 +20,10 @@ func SetupSessionManager(db *sql.DB) (*scs.SessionManager, func()) {
sessionManager := scs.New() sessionManager := scs.New()
sessionManager.Store = store sessionManager.Store = store
sessionManager.Lifetime = 30 * 24 * time.Hour sessionManager.Lifetime = 30 * 24 * time.Hour
sessionManager.Cookie.Name = "c4_session"
sessionManager.Cookie.Path = "/" sessionManager.Cookie.Path = "/"
sessionManager.Cookie.HttpOnly = true sessionManager.Cookie.HttpOnly = true
sessionManager.Cookie.Secure = false sessionManager.Cookie.Secure = true
sessionManager.Cookie.SameSite = http.SameSiteLaxMode sessionManager.Cookie.SameSite = http.SameSiteLaxMode
slog.Info("session manager configured") slog.Info("session manager configured")