Files
games/features/auth/handlers.go
Ryan Hamamura 8c3b3fc6ea
Some checks failed
CI / Deploy / test (pull_request) Successful in 28s
CI / Deploy / lint (pull_request) Failing after 42s
CI / Deploy / deploy (pull_request) Has been skipped
refactor: replace via framework with chi + templ + datastar
Migrate from the via meta-framework to direct dependencies:
- chi for routing, templ for HTML templates, datastar for SSE/reactivity
- Feature-sliced architecture (features/{auth,lobby,c4game,snakegame}/)
- Shared layouts and components (features/common/)
- Handler factory pattern (HandleX(deps) http.HandlerFunc)
- Embedded NATS server (nats/), SCS sessions (sessions/), chi router wiring (router/)
- Move ChatMessage domain type from ui package to game package
- Remove old ui/ package (gomponents-based via/h views)
- Remove via dependency from go.mod entirely
2026-03-02 12:16:25 -10:00

134 lines
4.1 KiB
Go

package auth
import (
"database/sql"
"net/http"
"github.com/alexedwards/scs/v2"
"github.com/google/uuid"
"github.com/ryanhamamura/c4/auth"
"github.com/ryanhamamura/c4/db/repository"
"github.com/ryanhamamura/c4/features/auth/pages"
"github.com/starfederation/datastar-go/datastar"
)
type LoginSignals struct {
Username string `json:"username"`
Password string `json:"password"`
}
type RegisterSignals struct {
Username string `json:"username"`
Password string `json:"password"`
Confirm string `json:"confirm"`
}
func HandleLoginPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := pages.LoginPage().Render(r.Context(), w); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}
}
func HandleRegisterPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := pages.RegisterPage().Render(r.Context(), w); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}
}
func HandleLogin(queries *repository.Queries, sessions *scs.SessionManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var signals LoginSignals
if err := datastar.ReadSignals(r, &signals); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sse := datastar.NewSSE(w, r)
user, err := queries.GetUserByUsername(r.Context(), signals.Username)
if err == sql.ErrNoRows {
sse.MarshalAndPatchSignals(map[string]any{"error": "Invalid username or password"}) //nolint:errcheck
return
}
if err != nil {
sse.MarshalAndPatchSignals(map[string]any{"error": "An error occurred"}) //nolint:errcheck
return
}
if !auth.CheckPassword(signals.Password, user.PasswordHash) {
sse.MarshalAndPatchSignals(map[string]any{"error": "Invalid username or password"}) //nolint:errcheck
return
}
sessions.RenewToken(r.Context()) //nolint:errcheck
sessions.Put(r.Context(), "user_id", user.ID)
sessions.Put(r.Context(), "username", user.Username)
sessions.Put(r.Context(), "nickname", user.Username)
redirectURL := "/"
if returnURL := sessions.GetString(r.Context(), "return_url"); returnURL != "" {
sessions.Put(r.Context(), "return_url", "")
redirectURL = returnURL
}
sse.Redirect(redirectURL) //nolint:errcheck
}
}
func HandleRegister(queries *repository.Queries, sessions *scs.SessionManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var signals RegisterSignals
if err := datastar.ReadSignals(r, &signals); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sse := datastar.NewSSE(w, r)
if err := auth.ValidateUsername(signals.Username); err != nil {
sse.MarshalAndPatchSignals(map[string]any{"error": err.Error()}) //nolint:errcheck
return
}
if err := auth.ValidatePassword(signals.Password); err != nil {
sse.MarshalAndPatchSignals(map[string]any{"error": err.Error()}) //nolint:errcheck
return
}
if signals.Password != signals.Confirm {
sse.MarshalAndPatchSignals(map[string]any{"error": "Passwords do not match"}) //nolint:errcheck
return
}
hash, err := auth.HashPassword(signals.Password)
if err != nil {
sse.MarshalAndPatchSignals(map[string]any{"error": "An error occurred"}) //nolint:errcheck
return
}
user, err := queries.CreateUser(r.Context(), repository.CreateUserParams{
ID: uuid.New().String(),
Username: signals.Username,
PasswordHash: hash,
})
if err != nil {
sse.MarshalAndPatchSignals(map[string]any{"error": "Username already taken"}) //nolint:errcheck
return
}
sessions.RenewToken(r.Context()) //nolint:errcheck
sessions.Put(r.Context(), "user_id", user.ID)
sessions.Put(r.Context(), "username", user.Username)
sessions.Put(r.Context(), "nickname", user.Username)
redirectURL := "/"
if returnURL := sessions.GetString(r.Context(), "return_url"); returnURL != "" {
sessions.Put(r.Context(), "return_url", "")
redirectURL = returnURL
}
sse.Redirect(redirectURL) //nolint:errcheck
}
}