Add user authentication and game persistence with SQLite

- User registration/login with bcrypt password hashing
- SQLite database with goose migrations and sqlc-generated queries
- Games and players persisted to database, resumable after restart
- Guest play still supported alongside authenticated users
- Auth UI components (login/register forms, auth header, guest banner)
This commit is contained in:
Ryan Hamamura
2026-01-14 16:59:40 -10:00
parent 03dcfdbf85
commit b264d8990b
18 changed files with 1121 additions and 5 deletions

127
ui/auth.go Normal file
View File

@@ -0,0 +1,127 @@
package ui
import (
"github.com/ryanhamamura/via/h"
)
func LoginView(usernameBind, passwordBind, loginKeyDown, loginClick h.H, errorMsg string) h.H {
var errorEl h.H
if errorMsg != "" {
errorEl = h.P(h.Class("error"), h.Text(errorMsg))
}
return h.Main(h.Class("container"),
h.Div(h.Class("lobby"),
h.H1(h.Text("Login")),
h.P(h.Text("Sign in to your account")),
errorEl,
h.Form(
h.FieldSet(
h.Label(h.Text("Username"), h.Attr("for", "username")),
h.Input(
h.ID("username"),
h.Type("text"),
h.Placeholder("Enter your username"),
usernameBind,
h.Attr("required"),
h.Attr("autofocus"),
),
h.Label(h.Text("Password"), h.Attr("for", "password")),
h.Input(
h.ID("password"),
h.Type("password"),
h.Placeholder("Enter your password"),
passwordBind,
h.Attr("required"),
loginKeyDown,
),
),
h.Button(
h.Type("button"),
h.Text("Login"),
loginClick,
),
),
h.P(
h.Text("Don't have an account? "),
h.A(h.Href("/register"), h.Text("Register")),
),
),
)
}
func RegisterView(usernameBind, passwordBind, confirmBind, registerKeyDown, registerClick h.H, errorMsg string) h.H {
var errorEl h.H
if errorMsg != "" {
errorEl = h.P(h.Class("error"), h.Text(errorMsg))
}
return h.Main(h.Class("container"),
h.Div(h.Class("lobby"),
h.H1(h.Text("Register")),
h.P(h.Text("Create a new account")),
errorEl,
h.Form(
h.FieldSet(
h.Label(h.Text("Username"), h.Attr("for", "username")),
h.Input(
h.ID("username"),
h.Type("text"),
h.Placeholder("Choose a username"),
usernameBind,
h.Attr("required"),
h.Attr("autofocus"),
),
h.Label(h.Text("Password"), h.Attr("for", "password")),
h.Input(
h.ID("password"),
h.Type("password"),
h.Placeholder("Choose a password (min 8 chars)"),
passwordBind,
h.Attr("required"),
),
h.Label(h.Text("Confirm Password"), h.Attr("for", "confirm")),
h.Input(
h.ID("confirm"),
h.Type("password"),
h.Placeholder("Confirm your password"),
confirmBind,
h.Attr("required"),
registerKeyDown,
),
),
h.Button(
h.Type("button"),
h.Text("Register"),
registerClick,
),
),
h.P(
h.Text("Already have an account? "),
h.A(h.Href("/login"), h.Text("Login")),
),
),
)
}
func AuthHeader(username string, logoutClick h.H) h.H {
return h.Div(h.Class("auth-header"),
h.Span(h.Text("Logged in as "), h.Strong(h.Text(username))),
h.Button(
h.Type("button"),
h.Class("secondary outline small"),
h.Text("Logout"),
logoutClick,
),
)
}
func GuestBanner() h.H {
return h.Div(h.Class("guest-banner"),
h.Text("Playing as guest. "),
h.A(h.Href("/login"), h.Text("Login")),
h.Text(" or "),
h.A(h.Href("/register"), h.Text("Register")),
h.Text(" to save your games."),
)
}