package routes import ( "fmt" "slices" "strings" "dk/internal/components" "dk/internal/models/classes" "dk/internal/models/users" sushi "git.sharkk.net/Sharkk/Sushi" "git.sharkk.net/Sharkk/Sushi/auth" "git.sharkk.net/Sharkk/Sushi/password" ) // RegisterAuthRoutes sets up authentication routes func RegisterAuthRoutes(app *sushi.App) { // Public routes (no auth required) app.Get("/login", showLogin) app.Post("/login", processLogin) app.Get("/register", showRegister) app.Post("/register", processRegister) // Protected routes authed := app.Group("") authed.Use(auth.RequireAuth("/login")) authed.Post("/logout", processLogout) } // showLogin displays the login form func showLogin(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() var id string if formData, exists := sess.Get("form_data"); exists { if data, ok := formData.(map[string]string); ok { id = data["id"] } } sess.Delete("form_data") components.RenderPage(ctx, "Log In", "auth/login.html", map[string]any{ "id": id, }) } // processLogin handles login form submission func processLogin(ctx sushi.Ctx) { email := strings.TrimSpace(string(ctx.PostArgs().Peek("id"))) userPassword := string(ctx.PostArgs().Peek("password")) if email == "" || userPassword == "" { setFlashAndFormData(ctx, "Email and password are required", map[string]string{"id": email}) ctx.Redirect("/login") return } user, err := authenticate(email, userPassword) if err != nil { setFlashAndFormData(ctx, "Invalid email or password", map[string]string{"id": email}) ctx.Redirect("/login") return } // Update last online time when logging in user.UpdateLastOnline() ctx.Login(user.ID, user) // Set success message sess := ctx.GetCurrentSession() sess.SetFlash("success", fmt.Sprintf("Welcome back, %s!", user.Username)) ctx.Redirect("/") } // showRegister displays the registration form func showRegister(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() var username, email string if formData, exists := sess.Get("form_data"); exists { if data, ok := formData.(map[string]string); ok { username = data["username"] email = data["email"] } } sess.Delete("form_data") allClasses, _ := classes.All() slices.Reverse(allClasses) components.RenderPage(ctx, "Register", "auth/register.html", map[string]any{ "username": username, "email": email, "error_message": sess.GetFlashMessage("error"), "classes": allClasses, }) } // processRegister handles registration form submission func processRegister(ctx sushi.Ctx) { username := strings.TrimSpace(ctx.Form("username").String()) email := strings.TrimSpace(ctx.Form("email").String()) userPassword := ctx.Form("password").String() confirmPassword := ctx.Form("confirm_password").String() classID := ctx.Form("class").Int() formData := map[string]string{ "username": username, "email": email, } if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil { setFlashAndFormData(ctx, err.Error(), formData) ctx.Redirect("/register") return } // Check if username already exists if _, err := users.ByUsername(username); err == nil { setFlashAndFormData(ctx, "Username already exists", formData) ctx.Redirect("/register") return } // Check if email already exists if _, err := users.ByEmail(email); err == nil { setFlashAndFormData(ctx, "Email already registered", formData) ctx.Redirect("/register") return } // Ensure class ID exists (should always, unless someone modified the form) class, err := classes.Find(classID) if err != nil { setFlashAndFormData(ctx, "Invalid class selected", formData) ctx.Redirect("/register") return } // Create new user user := users.New() user.Username = username user.Email = email user.Password = password.HashPassword(userPassword) user.Auth = 1 user.ClassID = classID user.HP, user.MaxHP = class.BaseHP, class.BaseHP user.MP, user.MaxMP = class.BaseMP, class.BaseMP user.Strength = class.BaseSTR user.Dexterity = class.BaseDEX // Validate before inserting if err := user.Validate(); err != nil { setFlashAndFormData(ctx, fmt.Sprintf("Invalid user data: %s", err.Error()), formData) ctx.Redirect("/register") return } if err := user.Insert(); err != nil { setFlashAndFormData(ctx, "Failed to create account", formData) ctx.Redirect("/register") return } // Grant level 1 spells for their class if err := user.LearnNewSpells(); err != nil { // Don't fail registration if spells fail, just log it fmt.Printf("Failed to grant initial spells to user %d: %v\n", user.ID, err) } // Auto-login after registration ctx.Login(user.ID, user) // Set success message sess := ctx.GetCurrentSession() sess.SetFlash("success", fmt.Sprintf("Greetings, %s!", user.Username)) ctx.Redirect("/") } // processLogout handles logout func processLogout(ctx sushi.Ctx) { ctx.Logout() ctx.Redirect("/") } // Helper functions func validateRegistration(username, email, password, confirmPassword string) error { if username == "" { return fmt.Errorf("username is required") } if len(username) < 3 { return fmt.Errorf("username must be at least 3 characters") } if email == "" { return fmt.Errorf("email is required") } if !strings.Contains(email, "@") { return fmt.Errorf("invalid email address") } if password == "" { return fmt.Errorf("password is required") } if len(password) < 6 { return fmt.Errorf("password must be at least 6 characters") } if password != confirmPassword { return fmt.Errorf("passwords do not match") } return nil } func setFlashAndFormData(ctx sushi.Ctx, message string, formData map[string]string) { sess := ctx.GetCurrentSession() sess.SetFlash("error", message) sess.Set("form_data", formData) } func authenticate(usernameOrEmail, plainPassword string) (*users.User, error) { var user *users.User var err error // Try username first user, err = users.ByUsername(usernameOrEmail) if err != nil { // If username not found, try email user, err = users.ByEmail(usernameOrEmail) if err != nil { return nil, fmt.Errorf("user not found") } } isValid, err := password.VerifyPassword(plainPassword, user.Password) if err != nil { return nil, fmt.Errorf("password verification error: %w", err) } if !isValid { return nil, fmt.Errorf("invalid password") } return user, nil }