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
This commit is contained in:
133
features/auth/handlers.go
Normal file
133
features/auth/handlers.go
Normal file
@@ -0,0 +1,133 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
89
features/auth/pages/login_templ.go
Normal file
89
features/auth/pages/login_templ.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
package pages
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/ryanhamamura/c4/features/common/layouts"
|
||||
"github.com/starfederation/datastar-go/datastar"
|
||||
)
|
||||
|
||||
func LoginPage() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
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=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/api/auth/login"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/login.templ`, Line: 34, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/api/auth/login"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/login.templ`, Line: 40, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layouts.Base("Login").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
89
features/auth/pages/register_templ.go
Normal file
89
features/auth/pages/register_templ.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
package pages
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/ryanhamamura/c4/features/common/layouts"
|
||||
"github.com/starfederation/datastar-go/datastar"
|
||||
)
|
||||
|
||||
func RegisterPage() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
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=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/api/auth/register"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/register.templ`, Line: 43, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(datastar.PostSSE("/api/auth/register"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/auth/pages/register.templ`, Line: 49, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layouts.Base("Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
16
features/auth/routes.go
Normal file
16
features/auth/routes.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
)
|
||||
|
||||
func SetupRoutes(router chi.Router, queries *repository.Queries, sessions *scs.SessionManager) error {
|
||||
router.Get("/login", HandleLoginPage())
|
||||
router.Get("/register", HandleRegisterPage())
|
||||
router.Post("/api/auth/login", HandleLogin(queries, sessions))
|
||||
router.Post("/api/auth/register", HandleRegister(queries, sessions))
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user