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
134 lines
4.1 KiB
Go
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
|
|
}
|
|
}
|