implement town middleware, first town route, clean up uservalue access
This commit is contained in:
parent
574bde5f28
commit
8eb869a971
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
39
internal/middleware/town.go
Normal file
39
internal/middleware/town.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
37
internal/routes/index.go
Normal 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
39
internal/routes/town.go
Normal 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
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
@ -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
24
templates/town/town.html
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user