diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 0f67f1f..53662fd 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -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) } diff --git a/internal/install/install.go b/internal/install/install.go index 01fef3b..8d39528 100644 --- a/internal/install/install.go +++ b/internal/install/install.go @@ -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) } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 1ab0321..9e401f8 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -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) -} \ No newline at end of file + ctx.SetUserValue("session", nil) + ctx.SetUserValue("user", nil) +} diff --git a/internal/middleware/town.go b/internal/middleware/town.go new file mode 100644 index 0000000..062a692 --- /dev/null +++ b/internal/middleware/town.go @@ -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) + } + } +} diff --git a/internal/password/password.go b/internal/password/password.go index fbda652..7e2ebb9 100644 --- a/internal/password/password.go +++ b/internal/password/password.go @@ -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$$ 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 -} \ No newline at end of file +} diff --git a/internal/routes/auth.go b/internal/routes/auth.go index e5039d3..13846af 100644 --- a/internal/routes/auth.go +++ b/internal/routes/auth.go @@ -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 != "" { diff --git a/internal/routes/index.go b/internal/routes/index.go new file mode 100644 index 0000000..c0a933d --- /dev/null +++ b/internal/routes/index.go @@ -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 + } +} diff --git a/internal/routes/town.go b/internal/routes/town.go new file mode 100644 index 0000000..6a2d243 --- /dev/null +++ b/internal/routes/town.go @@ -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 + } +} diff --git a/internal/server/server.go b/internal/server/server.go index 6176134..2c3eb92 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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") diff --git a/internal/users/builder.go b/internal/users/builder.go deleted file mode 100644 index c789ac5..0000000 --- a/internal/users/builder.go +++ /dev/null @@ -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 -} diff --git a/internal/users/users.go b/internal/users/users.go index f565cc1..1a52183 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -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 { diff --git a/templates/town/town.html b/templates/town/town.html new file mode 100644 index 0000000..4081349 --- /dev/null +++ b/templates/town/town.html @@ -0,0 +1,24 @@ +
+
+
Welcome to {town.Name}
+ Town Options:
+ +
+ +
+ {news} +
+ +
+ {whosonline} +
+ +
+
Babblebox
+ @TODO +
+