implement town middleware, first town route, clean up uservalue access

This commit is contained in:
Sky Johnson 2025-08-11 10:47:13 -05:00
parent 574bde5f28
commit 8eb869a971
12 changed files with 324 additions and 404 deletions

View File

@ -8,13 +8,6 @@ import (
// Manager is the global singleton instance
var Manager *AuthManager
// User is a simplified User struct for auth purposes
type User struct {
ID int
Username string
Email string
}
// AuthManager is a wrapper for the session store to add
// authentication tools over the store itself
type AuthManager struct {
@ -30,7 +23,7 @@ func Init(sessionsFilePath string) {
// Authenticate checks for the usernaname or email, then verifies the plain password
// against the stored hash.
func (am *AuthManager) Authenticate(usernameOrEmail, plainPassword string) (*User, error) {
func (am *AuthManager) Authenticate(usernameOrEmail, plainPassword string) (*users.User, error) {
var user *users.User
var err error
@ -51,14 +44,10 @@ func (am *AuthManager) Authenticate(usernameOrEmail, plainPassword string) (*Use
return nil, ErrInvalidCredentials
}
return &User{
ID: user.ID,
Username: user.Username,
Email: user.Email,
}, nil
return user, nil
}
func (am *AuthManager) CreateSession(user *User) *Session {
func (am *AuthManager) CreateSession(user *users.User) *Session {
return am.store.Create(user.ID, user.Username, user.Email)
}

View File

@ -14,6 +14,7 @@ import (
"dk/internal/database"
"dk/internal/password"
"dk/internal/users"
)
const dbPath = "dk.db"
@ -483,15 +484,14 @@ func populateData() error {
}
func createDemoUser() error {
hashedPassword, err := password.Hash("Demo123!")
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
user := users.New()
user.Username = "demo"
user.Email = "demo@demo.com"
user.Password = password.Hash("Demo123!")
user.ClassID = 1
user.Auth = 4
stmt := `INSERT INTO users (username, password, email, verified, class_id, auth)
VALUES (?, ?, ?, 1, 1, 4)`
if err := database.Exec(stmt, "demo", hashedPassword, "demo@demo.com"); err != nil {
if err := user.Insert(); err != nil {
return fmt.Errorf("failed to create demo user: %w", err)
}

View File

@ -3,67 +3,72 @@ package middleware
import (
"dk/internal/auth"
"dk/internal/router"
"dk/internal/users"
"github.com/valyala/fasthttp"
)
const (
UserKey = "user"
SessionKey = "session"
)
// Auth creates an authentication middleware
func Auth(authManager *auth.AuthManager) router.Middleware {
return func(next router.Handler) router.Handler {
return func(ctx router.Ctx, params []string) {
sessionID := auth.GetSessionCookie(ctx)
if sessionID != "" {
if session, exists := authManager.GetSession(sessionID); exists {
// Update session activity
authManager.UpdateSession(sessionID)
// Store session and user info in context
ctx.SetUserValue(SessionKey, session)
ctx.SetUserValue(UserKey, &auth.User{
ID: session.UserID,
Username: session.Username,
Email: session.Email,
})
// Refresh the cookie
auth.SetSessionCookie(ctx, sessionID)
// Get the full user object
user, err := users.Find(session.UserID)
if err == nil && user != nil {
// Store session and user info in context
ctx.SetUserValue("session", session)
ctx.SetUserValue("user", user)
// Refresh the cookie
auth.SetSessionCookie(ctx, sessionID)
}
}
}
next(ctx, params)
}
}
}
// RequireAuth enforces authentication - redirects to login if not authenticated
func RequireAuth(loginPath string) router.Middleware {
// RequireAuth enforces authentication - redirect defaults to "/login"
func RequireAuth(paths ...string) router.Middleware {
redirect := "/login"
if len(paths) > 0 && paths[0] != "" {
redirect = paths[0]
}
return func(next router.Handler) router.Handler {
return func(ctx router.Ctx, params []string) {
if !IsAuthenticated(ctx) {
ctx.Redirect(loginPath, fasthttp.StatusFound)
ctx.Redirect(redirect, fasthttp.StatusFound)
return
}
next(ctx, params)
}
}
}
// RequireGuest enforces no authentication - redirects to dashboard if authenticated
func RequireGuest(dashboardPath string) router.Middleware {
// RequireGuest enforces no authentication - redirect defaults to "/"
func RequireGuest(paths ...string) router.Middleware {
redirect := "/"
if len(paths) > 0 && paths[0] != "" {
redirect = paths[0]
}
return func(next router.Handler) router.Handler {
return func(ctx router.Ctx, params []string) {
if IsAuthenticated(ctx) {
ctx.Redirect(dashboardPath, fasthttp.StatusFound)
ctx.Redirect(redirect, fasthttp.StatusFound)
return
}
next(ctx, params)
}
}
@ -71,13 +76,13 @@ func RequireGuest(dashboardPath string) router.Middleware {
// IsAuthenticated checks if the current request has a valid session
func IsAuthenticated(ctx router.Ctx) bool {
_, exists := ctx.UserValue(UserKey).(*auth.User)
_, exists := ctx.UserValue("user").(*users.User)
return exists
}
// GetCurrentUser returns the current authenticated user, or nil if not authenticated
func GetCurrentUser(ctx router.Ctx) *auth.User {
if user, ok := ctx.UserValue(UserKey).(*auth.User); ok {
func GetCurrentUser(ctx router.Ctx) *users.User {
if user, ok := ctx.UserValue("user").(*users.User); ok {
return user
}
return nil
@ -85,20 +90,20 @@ func GetCurrentUser(ctx router.Ctx) *auth.User {
// GetCurrentSession returns the current session, or nil if not authenticated
func GetCurrentSession(ctx router.Ctx) *auth.Session {
if session, ok := ctx.UserValue(SessionKey).(*auth.Session); ok {
if session, ok := ctx.UserValue("session").(*auth.Session); ok {
return session
}
return nil
}
// Login creates a session and sets the cookie
func Login(ctx router.Ctx, authManager *auth.AuthManager, user *auth.User) {
func Login(ctx router.Ctx, authManager *auth.AuthManager, user *users.User) {
session := authManager.CreateSession(user)
auth.SetSessionCookie(ctx, session.ID)
// Set in context for immediate use
ctx.SetUserValue(SessionKey, session)
ctx.SetUserValue(UserKey, user)
ctx.SetUserValue("session", session)
ctx.SetUserValue("user", user)
}
// Logout destroys the session and clears the cookie
@ -107,10 +112,10 @@ func Logout(ctx router.Ctx, authManager *auth.AuthManager) {
if sessionID != "" {
authManager.DeleteSession(sessionID)
}
auth.DeleteSessionCookie(ctx)
// Clear from context
ctx.SetUserValue(SessionKey, nil)
ctx.SetUserValue(UserKey, nil)
}
ctx.SetUserValue("session", nil)
ctx.SetUserValue("user", nil)
}

View File

@ -0,0 +1,39 @@
package middleware
import (
"dk/internal/router"
"dk/internal/towns"
"dk/internal/users"
"github.com/valyala/fasthttp"
)
// RequireTown ensures the user is in town at valid coordinates
func RequireTown() router.Middleware {
return func(next router.Handler) router.Handler {
return func(ctx router.Ctx, params []string) {
user, ok := ctx.UserValue("user").(*users.User)
if !ok || user == nil {
ctx.SetStatusCode(fasthttp.StatusUnauthorized)
ctx.SetBodyString("Not authenticated")
return
}
if user.Currently != "In Town" {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetBodyString("You must be in town")
return
}
town, err := towns.ByCoords(user.X, user.Y)
if err != nil || town == nil || town.ID == 0 {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetBodyString("Invalid town location")
return
}
ctx.SetUserValue("town", town)
next(ctx, params)
}
}
}

View File

@ -18,22 +18,19 @@ const (
)
// Hash creates an argon2id hash of the password
func Hash(password string) (string, error) {
func Hash(password string) string {
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return "", err
}
rand.Read(salt)
hash := argon2.IDKey([]byte(password), salt, time, memory, threads, keyLen)
// Encode in the format: $argon2id$v=19$m=65536,t=1,p=4$<salt>$<hash>
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version, memory, time, threads, b64Salt, b64Hash)
return encoded, nil
return encoded
}
// Verify checks if a password matches the hash
@ -80,4 +77,4 @@ func Verify(password, encodedHash string) (bool, error) {
}
return false, nil
}
}

View File

@ -23,9 +23,9 @@ func RegisterAuthRoutes(r *router.Router) {
guestGroup := r.Group("")
guestGroup.Use(middleware.RequireGuest("/"))
guestGroup.Get("/login", showLogin())
guestGroup.Get("/login", showLogin)
guestGroup.Post("/login", processLogin())
guestGroup.Get("/register", showRegister())
guestGroup.Get("/register", showRegister)
guestGroup.Post("/register", processRegister())
// Authenticated routes
@ -36,60 +36,53 @@ func RegisterAuthRoutes(r *router.Router) {
}
// showLogin displays the login form
func showLogin() router.Handler {
return func(ctx router.Ctx, params []string) {
loginTmpl, err := template.Cache.Load("auth/login.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
func showLogin(ctx router.Ctx, params []string) {
loginTmpl, err := template.Cache.Load("auth/login.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
loginFormData := map[string]any{
"csrf_token": csrf.GetToken(ctx, auth.Manager),
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
"error_message": "",
}
loginFormData := map[string]any{
"csrf_token": csrf.GetToken(ctx, auth.Manager),
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
"error_message": "",
}
loginContent := loginTmpl.RenderNamed(loginFormData)
loginContent := loginTmpl.RenderNamed(loginFormData)
pageData := components.NewPageData("Login - Dragon Knight", loginContent)
if err := components.RenderPage(ctx, pageData, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
pageData := components.NewPageData("Login - Dragon Knight", loginContent)
if err := components.RenderPage(ctx, pageData, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
}
// processLogin handles login form submission
func processLogin() router.Handler {
return func(ctx router.Ctx, params []string) {
// Validate CSRF token
if !csrf.ValidateFormToken(ctx, auth.Manager) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.WriteString("CSRF validation failed")
return
}
// Get form values
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
userPassword := string(ctx.PostArgs().Peek("password"))
// Validate input
if email == "" || userPassword == "" {
showLoginError(ctx, "Email and password are required")
return
}
// Authenticate user
user, err := auth.Manager.Authenticate(email, userPassword)
if err != nil {
showLoginError(ctx, "Invalid email or password")
return
}
// Create session and login
middleware.Login(ctx, auth.Manager, user)
// Transfer CSRF token from cookie to session for authenticated user
@ -99,104 +92,78 @@ func processLogin() router.Handler {
}
}
// Redirect to dashboard
ctx.Redirect("/dashboard", fasthttp.StatusFound)
ctx.Redirect("/", fasthttp.StatusFound)
}
}
// showRegister displays the registration form
func showRegister() router.Handler {
return func(ctx router.Ctx, params []string) {
registerTmpl, err := template.Cache.Load("auth/register.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
func showRegister(ctx router.Ctx, _ []string) {
registerTmpl, err := template.Cache.Load("auth/register.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
registerFormData := map[string]any{
"csrf_token": csrf.GetToken(ctx, auth.Manager),
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
"error_message": "",
"username": "",
"email": "",
}
registerContent := registerTmpl.RenderNamed(map[string]any{
"csrf_token": csrf.GetToken(ctx, auth.Manager),
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
"error_message": "",
"username": "",
"email": "",
})
registerContent := registerTmpl.RenderNamed(registerFormData)
pageData := components.NewPageData("Register - Dragon Knight", registerContent)
if err := components.RenderPage(ctx, pageData, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
pageData := components.NewPageData("Register - Dragon Knight", registerContent)
if err := components.RenderPage(ctx, pageData, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
}
// processRegister handles registration form submission
func processRegister() router.Handler {
return func(ctx router.Ctx, params []string) {
// Validate CSRF token
if !csrf.ValidateFormToken(ctx, auth.Manager) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.WriteString("CSRF validation failed")
return
}
// Get form values
username := strings.TrimSpace(string(ctx.PostArgs().Peek("username")))
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
userPassword := string(ctx.PostArgs().Peek("password"))
confirmPassword := string(ctx.PostArgs().Peek("confirm_password"))
// Validate input
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
showRegisterError(ctx, err.Error(), username, email)
return
}
// Check if username already exists
if _, err := users.GetByUsername(username); err == nil {
showRegisterError(ctx, "Username already exists", username, email)
return
}
// Check if email already exists
if _, err := users.GetByEmail(email); err == nil {
showRegisterError(ctx, "Email already registered", username, email)
return
}
// Hash password
hashedPassword, err := password.Hash(userPassword)
if err != nil {
showRegisterError(ctx, "Failed to process password", username, email)
return
}
user := users.New()
user.Username = username
user.Email = email
user.Password = password.Hash(userPassword)
user.ClassID = 1
user.Auth = 1
// Create user (this is a simplified approach - in a real app you'd use a proper user creation function)
user := &users.User{
Username: username,
Email: email,
Password: hashedPassword,
Verified: 1, // Auto-verify for now
Auth: 1, // Enabled
}
// Insert into database
if err := createUser(user); err != nil {
if err := user.Insert(); err != nil {
showRegisterError(ctx, "Failed to create account", username, email)
return
}
// Auto-login after registration
authUser := &auth.User{
ID: user.ID,
Username: user.Username,
Email: user.Email,
}
middleware.Login(ctx, auth.Manager, authUser)
middleware.Login(ctx, auth.Manager, user)
// Transfer CSRF token from cookie to session for authenticated user
if cookieToken := csrf.GetTokenFromCookie(ctx); cookieToken != "" {

37
internal/routes/index.go Normal file
View File

@ -0,0 +1,37 @@
package routes
import (
"dk/internal/middleware"
"dk/internal/router"
"dk/internal/template/components"
"dk/internal/users"
"fmt"
"github.com/valyala/fasthttp"
)
func Index(ctx router.Ctx, _ []string) {
currentUser := middleware.GetCurrentUser(ctx)
var username string
if currentUser != nil {
username = currentUser.Username
user, _ := users.Find(currentUser.ID)
if user.Currently == "In Town" {
ctx.Redirect("/town", 303)
}
} else {
username = "Guest"
}
pageData := components.NewPageData(
"Dragon Knight",
fmt.Sprintf("Hello %s!", username),
)
if err := components.RenderPage(ctx, pageData, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
}

39
internal/routes/town.go Normal file
View File

@ -0,0 +1,39 @@
package routes
import (
"dk/internal/middleware"
"dk/internal/router"
"dk/internal/template"
"dk/internal/template/components"
"fmt"
"github.com/valyala/fasthttp"
)
func RegisterTownRoutes(r *router.Router) {
group := r.Group("/town")
group.Use(middleware.RequireAuth())
group.Use(middleware.RequireTown())
group.Get("/", showTown)
}
func showTown(ctx router.Ctx, _ []string) {
tmpl, err := template.Cache.Load("town/town.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
content := tmpl.RenderNamed(map[string]any{
"town": ctx.UserValue("town"),
})
pageData := components.NewPageData("Town - Dragon Knight", content)
if err := components.RenderPage(ctx, pageData, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
}

View File

@ -14,7 +14,6 @@ import (
"dk/internal/router"
"dk/internal/routes"
"dk/internal/template"
"dk/internal/template/components"
"github.com/valyala/fasthttp"
)
@ -40,29 +39,9 @@ func Start(port string) error {
r.Use(middleware.Auth(auth.Manager))
r.Use(middleware.CSRF(auth.Manager))
// Setup route handlers
r.Get("/", routes.Index)
routes.RegisterAuthRoutes(r)
r.Get("/", func(ctx router.Ctx, params []string) {
currentUser := middleware.GetCurrentUser(ctx)
var username string
if currentUser != nil {
username = currentUser.Username
} else {
username = "Guest"
}
pageData := components.NewPageData(
"Dragon Knight",
fmt.Sprintf("Hello %s!", username),
)
if err := components.RenderPage(ctx, pageData, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
})
routes.RegisterTownRoutes(r)
// Use current working directory for static files
assetsDir := filepath.Join(cwd, "assets")

View File

@ -1,232 +0,0 @@
package users
import (
"fmt"
"time"
"dk/internal/database"
"zombiezen.com/go/sqlite"
)
// Builder provides a fluent interface for creating users
type Builder struct {
user *User
}
// NewBuilder creates a new user builder with default values
func NewBuilder() *Builder {
now := time.Now().Unix()
return &Builder{
user: &User{
Verified: 0, // Default unverified
Token: "", // Empty verification token
Registered: now, // Current time
LastOnline: now, // Current time
Auth: 0, // Default no special permissions
X: 0, // Default starting position
Y: 0, // Default starting position
ClassID: 1, // Default to class 1
Currently: "In Town", // Default status
Fighting: 0, // Default not fighting
HP: 15, // Default starting HP
MP: 0, // Default starting MP
TP: 10, // Default starting TP
MaxHP: 15, // Default starting max HP
MaxMP: 0, // Default starting max MP
MaxTP: 10, // Default starting max TP
Level: 1, // Default starting level
Gold: 100, // Default starting gold
Exp: 0, // Default starting exp
Strength: 5, // Default starting strength
Dexterity: 5, // Default starting dexterity
Attack: 5, // Default starting attack
Defense: 5, // Default starting defense
Spells: "", // No spells initially
Towns: "", // No towns visited initially
},
}
}
// WithUsername sets the username
func (b *Builder) WithUsername(username string) *Builder {
b.user.Username = username
return b
}
// WithPassword sets the password
func (b *Builder) WithPassword(password string) *Builder {
b.user.Password = password
return b
}
// WithEmail sets the email address
func (b *Builder) WithEmail(email string) *Builder {
b.user.Email = email
return b
}
// WithVerified sets the verification status
func (b *Builder) WithVerified(verified bool) *Builder {
if verified {
b.user.Verified = 1
} else {
b.user.Verified = 0
}
return b
}
// WithToken sets the verification token
func (b *Builder) WithToken(token string) *Builder {
b.user.Token = token
return b
}
// WithAuth sets the authorization level
func (b *Builder) WithAuth(auth int) *Builder {
b.user.Auth = auth
return b
}
// AsAdmin sets the user as admin (auth = 4)
func (b *Builder) AsAdmin() *Builder {
b.user.Auth = 4
return b
}
// AsModerator sets the user as moderator (auth = 2)
func (b *Builder) AsModerator() *Builder {
b.user.Auth = 2
return b
}
// WithClassID sets the character class ID
func (b *Builder) WithClassID(classID int) *Builder {
b.user.ClassID = classID
return b
}
// WithPosition sets the starting coordinates
func (b *Builder) WithPosition(x, y int) *Builder {
b.user.X = x
b.user.Y = y
return b
}
// WithLevel sets the starting level
func (b *Builder) WithLevel(level int) *Builder {
b.user.Level = level
return b
}
// WithGold sets the starting gold amount
func (b *Builder) WithGold(gold int) *Builder {
b.user.Gold = gold
return b
}
// WithStats sets the core character stats
func (b *Builder) WithStats(strength, dexterity, attack, defense int) *Builder {
b.user.Strength = strength
b.user.Dexterity = dexterity
b.user.Attack = attack
b.user.Defense = defense
return b
}
// WithHP sets current and maximum HP
func (b *Builder) WithHP(hp, maxHP int) *Builder {
b.user.HP = hp
b.user.MaxHP = maxHP
return b
}
// WithMP sets current and maximum MP
func (b *Builder) WithMP(mp, maxMP int) *Builder {
b.user.MP = mp
b.user.MaxMP = maxMP
return b
}
// WithTP sets current and maximum TP
func (b *Builder) WithTP(tp, maxTP int) *Builder {
b.user.TP = tp
b.user.MaxTP = maxTP
return b
}
// WithCurrently sets the current status message
func (b *Builder) WithCurrently(currently string) *Builder {
b.user.Currently = currently
return b
}
// WithRegistered sets the registration timestamp
func (b *Builder) WithRegistered(registered int64) *Builder {
b.user.Registered = registered
return b
}
// WithRegisteredTime sets the registration timestamp from time.Time
func (b *Builder) WithRegisteredTime(t time.Time) *Builder {
b.user.Registered = t.Unix()
return b
}
// WithSpells sets the user's known spells
func (b *Builder) WithSpells(spells []string) *Builder {
b.user.SetSpellIDs(spells)
return b
}
// WithTowns sets the user's visited towns
func (b *Builder) WithTowns(towns []string) *Builder {
b.user.SetTownIDs(towns)
return b
}
// Create saves the user to the database and returns the created user with ID
func (b *Builder) Create() (*User, error) {
// Use a transaction to ensure we can get the ID
var user *User
err := database.Transaction(func(tx *database.Tx) error {
query := `INSERT INTO users (username, password, email, verified, token, registered, last_online, auth,
x, y, class_id, currently, fighting, monster_id, monster_hp, monster_sleep, monster_immune,
uber_damage, uber_defense, hp, mp, tp, max_hp, max_mp, max_tp, level, gold, exp,
gold_bonus, exp_bonus, strength, dexterity, attack, defense, weapon_id, armor_id, shield_id,
slot_1_id, slot_2_id, slot_3_id, weapon_name, armor_name, shield_name,
slot_1_name, slot_2_name, slot_3_name, drop_code, spells, towns)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
if err := tx.Exec(query, b.user.Username, b.user.Password, b.user.Email, b.user.Verified, b.user.Token,
b.user.Registered, b.user.LastOnline, b.user.Auth, b.user.X, b.user.Y, b.user.ClassID, b.user.Currently,
b.user.Fighting, b.user.MonsterID, b.user.MonsterHP, b.user.MonsterSleep, b.user.MonsterImmune,
b.user.UberDamage, b.user.UberDefense, b.user.HP, b.user.MP, b.user.TP, b.user.MaxHP, b.user.MaxMP, b.user.MaxTP,
b.user.Level, b.user.Gold, b.user.Exp, b.user.GoldBonus, b.user.ExpBonus, b.user.Strength, b.user.Dexterity,
b.user.Attack, b.user.Defense, b.user.WeaponID, b.user.ArmorID, b.user.ShieldID, b.user.Slot1ID,
b.user.Slot2ID, b.user.Slot3ID, b.user.WeaponName, b.user.ArmorName, b.user.ShieldName,
b.user.Slot1Name, b.user.Slot2Name, b.user.Slot3Name, b.user.DropCode, b.user.Spells, b.user.Towns); err != nil {
return fmt.Errorf("failed to insert user: %w", err)
}
// Get the last insert ID
var id int
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
id = stmt.ColumnInt(0)
return nil
})
if err != nil {
return fmt.Errorf("failed to get insert ID: %w", err)
}
b.user.ID = id
user = b.user
return nil
})
if err != nil {
return nil, err
}
return user, nil
}

View File

@ -65,6 +65,38 @@ type User struct {
Towns string `db:"towns" json:"towns"`
}
// New creates a new User with sensible defaults
func New() *User {
now := time.Now().Unix()
return &User{
Verified: 0, // Default unverified
Token: "", // Empty verification token
Registered: now, // Current time
LastOnline: now, // Current time
Auth: 0, // Default no special permissions
X: 0, // Default starting position
Y: 0, // Default starting position
ClassID: 1, // Default to class 1
Currently: "In Town", // Default status
Fighting: 0, // Default not fighting
HP: 15, // Default starting HP
MP: 0, // Default starting MP
TP: 10, // Default starting TP
MaxHP: 15, // Default starting max HP
MaxMP: 0, // Default starting max MP
MaxTP: 10, // Default starting max TP
Level: 1, // Default starting level
Gold: 100, // Default starting gold
Exp: 0, // Default starting exp
Strength: 5, // Default starting strength
Dexterity: 5, // Default starting dexterity
Attack: 5, // Default starting attack
Defense: 5, // Default starting defense
Spells: "", // No spells initially
Towns: "", // No towns visited initially
}
}
var userScanner = scanner.New[User]()
// userColumns returns the column list for user queries
@ -229,6 +261,50 @@ func (u *User) Save() error {
u.Slot1Name, u.Slot2Name, u.Slot3Name, u.DropCode, u.Spells, u.Towns, u.ID)
}
// Insert saves a new user to the database and sets the ID
func (u *User) Insert() error {
if u.ID != 0 {
return fmt.Errorf("user already has ID %d, use Save() to update", u.ID)
}
// Use a transaction to ensure we can get the ID
err := database.Transaction(func(tx *database.Tx) error {
query := `INSERT INTO users (username, password, email, verified, token, registered, last_online, auth,
x, y, class_id, currently, fighting, monster_id, monster_hp, monster_sleep, monster_immune,
uber_damage, uber_defense, hp, mp, tp, max_hp, max_mp, max_tp, level, gold, exp,
gold_bonus, exp_bonus, strength, dexterity, attack, defense, weapon_id, armor_id, shield_id,
slot_1_id, slot_2_id, slot_3_id, weapon_name, armor_name, shield_name,
slot_1_name, slot_2_name, slot_3_name, drop_code, spells, towns)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
if err := tx.Exec(query, u.Username, u.Password, u.Email, u.Verified, u.Token,
u.Registered, u.LastOnline, u.Auth, u.X, u.Y, u.ClassID, u.Currently,
u.Fighting, u.MonsterID, u.MonsterHP, u.MonsterSleep, u.MonsterImmune,
u.UberDamage, u.UberDefense, u.HP, u.MP, u.TP, u.MaxHP, u.MaxMP, u.MaxTP,
u.Level, u.Gold, u.Exp, u.GoldBonus, u.ExpBonus, u.Strength, u.Dexterity,
u.Attack, u.Defense, u.WeaponID, u.ArmorID, u.ShieldID, u.Slot1ID,
u.Slot2ID, u.Slot3ID, u.WeaponName, u.ArmorName, u.ShieldName,
u.Slot1Name, u.Slot2Name, u.Slot3Name, u.DropCode, u.Spells, u.Towns); err != nil {
return fmt.Errorf("failed to insert user: %w", err)
}
// Get the last insert ID
var id int
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
id = stmt.ColumnInt(0)
return nil
})
if err != nil {
return fmt.Errorf("failed to get insert ID: %w", err)
}
u.ID = id
return nil
})
return err
}
// Delete removes the user from the database
func (u *User) Delete() error {
if u.ID == 0 {

24
templates/town/town.html Normal file
View File

@ -0,0 +1,24 @@
<div class="town">
<div class="options">
<div class="title"><img src="/assets/images/town_{town.ID}.gif" alt="Welcome to {town.Name}" title="Welcome to {town.Name}"></div>
<b>Town Options:</b><br>
<ul hx-boost="true" hx-target="#middle">
<li><a href="/inn">Rest at the Inn</a></li>
<li><a href="/shop">Browse the Shop</a></li>
<li><a href="/maps">Buy Maps</a></li>
</ul>
</div>
<div class="news">
{news}
</div>
<div class="whos-online">
{whosonline}
</div>
<div class="babblebox">
<div class="title">Babblebox</div>
@TODO
</div>
</div>