package routes import ( "dk/internal/components" "dk/internal/database" "dk/internal/helpers" "dk/internal/models/monsters" "dk/internal/models/news" "dk/internal/models/spells" "dk/internal/models/towns" "dk/internal/models/users" "fmt" "runtime" "strconv" "strings" "time" sushi "git.sharkk.net/Sharkk/Sushi" "git.sharkk.net/Sharkk/Sushi/auth" "git.sharkk.net/Sharkk/Sushi/password" ) func RegisterAdminRoutes(app *sushi.App) { group := app.Group("/admin") group.Use(auth.RequireAuth()) group.Use(func(ctx sushi.Ctx, next func()) { if ctx.GetCurrentUser().(*users.User).Auth < 4 { ctx.Redirect("/") return } next() }) group.Get("/", adminIndex) group.Get("/news", adminNewsForm) group.Post("/news", adminNewsCreate) group.Get("/users", adminUsersIndex) group.Get("/users/:id", adminUserEdit) group.Post("/users/:id", adminUserUpdate) group.Get("/towns", adminTownsIndex) group.Get("/towns/new", adminTownNew) group.Post("/towns/new", adminTownCreate) group.Get("/towns/:id", adminTownEdit) group.Post("/towns/:id", adminTownUpdate) group.Get("/spells", adminSpellsIndex) group.Get("/spells/new", adminSpellNew) group.Post("/spells/new", adminSpellCreate) group.Get("/spells/:id", adminSpellEdit) group.Post("/spells/:id", adminSpellUpdate) group.Get("/monsters", adminMonstersIndex) group.Get("/monsters/new", adminMonsterNew) group.Post("/monsters/new", adminMonsterCreate) group.Get("/monsters/:id", adminMonsterEdit) group.Post("/monsters/:id", adminMonsterUpdate) } func adminIndex(ctx sushi.Ctx) { var m runtime.MemStats runtime.ReadMemStats(&m) components.RenderAdminPage(ctx, "", "admin/home.html", map[string]any{ "alloc_mb": bToMb(m.Alloc), "total_alloc_mb": bToMb(m.TotalAlloc), "sys_mb": bToMb(m.Sys), "heap_alloc_mb": bToMb(m.HeapAlloc), "heap_sys_mb": bToMb(m.HeapSys), "heap_released_mb": bToMb(m.HeapReleased), "gc_cycles": m.NumGC, "gc_pause_total": m.PauseTotalNs / 1000000, // ms "goroutines": runtime.NumGoroutine(), "cpu_cores": runtime.NumCPU(), "go_version": runtime.Version(), }) } func adminNewsForm(ctx sushi.Ctx) { components.RenderAdminPage(ctx, "", "admin/news.html", map[string]any{}) } func adminNewsCreate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() content := strings.TrimSpace(ctx.Form("content").String()) if content == "" { sess.SetFlash("error", "Content cannot be empty") ctx.Redirect("/admin/news") return } user := ctx.GetCurrentUser().(*users.User) newsPost := &news.News{ Author: user.ID, Content: content, Posted: time.Now().Unix(), } if err := newsPost.Insert(); err != nil { sess.SetFlash("error", "Failed to create news post") ctx.Redirect("/admin/news") return } sess.SetFlash("success", "News post created successfully") ctx.Redirect("/admin") } func adminUsersIndex(ctx sushi.Ctx) { pagination := helpers.Pagination{ Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), PerPage: 30, } type UserData struct { ID int Username string Email string Level int Auth int ClassID int ClassName string HP int MaxHP int Registered int64 LastOnline int64 } var userList []*UserData err := database.Select(&userList, ` SELECT u.id, u.username, u.email, u.level, u.auth, u.class_id, COALESCE(c.name, 'Unknown') as class_name, u.hp, u.max_hp, u.registered, u.last_online FROM users u LEFT JOIN classes c ON u.class_id = c.id ORDER BY u.id ASC LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) if err != nil { fmt.Printf("Error getting user list for admin index: %s", err.Error()) userList = make([]*UserData, 0) } type CountResult struct{ Count int } var result CountResult database.Get(&result, "SELECT COUNT(*) as count FROM users") pagination.Total = result.Count components.RenderAdminPage(ctx, "User Management", "admin/users/index.html", map[string]any{ "users": userList, "currentPage": pagination.Page, "totalPages": pagination.TotalPages(), "hasNext": pagination.HasNext(), "hasPrev": pagination.HasPrev(), }) } func adminUserEdit(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() user, err := users.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("User %d not found", id)) ctx.Redirect("/admin/users") return } components.RenderAdminPage(ctx, fmt.Sprintf("Edit User: %s", user.Username), "admin/users/edit.html", map[string]any{ "user": user, }) } func adminUserUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() user, err := users.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("User %d not found", id)) ctx.Redirect("/admin/users") return } // Update fields username := strings.TrimSpace(ctx.Form("username").String()) email := strings.TrimSpace(ctx.Form("email").String()) level, _ := strconv.Atoi(ctx.Form("level").String()) auth, _ := strconv.Atoi(ctx.Form("auth").String()) hp, _ := strconv.Atoi(ctx.Form("hp").String()) maxHP, _ := strconv.Atoi(ctx.Form("max_hp").String()) newPassword := strings.TrimSpace(ctx.Form("new_password").String()) if username == "" || email == "" { sess.SetFlash("error", "Username and email are required") ctx.Redirect(fmt.Sprintf("/admin/users/%d", id)) return } user.Username = username user.Email = email user.Level = level user.Auth = auth user.HP = hp user.MaxHP = maxHP if newPassword != "" { user.Password = password.HashPassword(newPassword) } fields := map[string]any{ "username": user.Username, "email": user.Email, "level": user.Level, "auth": user.Auth, "hp": user.HP, "max_hp": user.MaxHP, } if newPassword != "" { fields["password"] = user.Password } if err := database.Update("users", fields, "id", id); err != nil { sess.SetFlash("error", "Failed to update user") ctx.Redirect(fmt.Sprintf("/admin/users/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("User %s updated successfully", user.Username)) ctx.Redirect("/admin/users") } func adminTownsIndex(ctx sushi.Ctx) { pagination := helpers.Pagination{ Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), PerPage: 30, } type TownData struct { ID int Name string X int Y int InnCost int MapCost int TPCost int ShopList string ShopItemCount int } var townList []*TownData err := database.Select(&townList, ` SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list, CASE WHEN shop_list = '' THEN 0 ELSE (LENGTH(shop_list) - LENGTH(REPLACE(shop_list, ',', '')) + 1) END as shop_item_count FROM towns ORDER BY id ASC LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) if err != nil { fmt.Printf("Error getting town list for admin index: %s", err.Error()) townList = make([]*TownData, 0) } type CountResult struct{ Count int } var result CountResult database.Get(&result, "SELECT COUNT(*) as count FROM towns") pagination.Total = result.Count components.RenderAdminPage(ctx, "Town Management", "admin/towns/index.html", map[string]any{ "towns": townList, "currentPage": pagination.Page, "totalPages": pagination.TotalPages(), "hasNext": pagination.HasNext(), "hasPrev": pagination.HasPrev(), }) } func adminTownNew(ctx sushi.Ctx) { town := towns.New() components.RenderAdminPage(ctx, "Add New Town", "admin/towns/edit.html", map[string]any{ "town": town, }) } func adminTownCreate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() town := towns.New() if err := populateTownFromForm(ctx, town); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/towns/new") return } if err := town.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/towns/new") return } if err := checkTownCoordinateConflict(town.X, town.Y, 0); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/towns/new") return } if err := town.Insert(); err != nil { sess.SetFlash("error", "Failed to create town") ctx.Redirect("/admin/towns/new") return } sess.SetFlash("success", fmt.Sprintf("Town %s created successfully", town.Name)) ctx.Redirect("/admin/towns") } func adminTownEdit(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() town, err := towns.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Town %d not found", id)) ctx.Redirect("/admin/towns") return } components.RenderAdminPage(ctx, fmt.Sprintf("Edit Town: %s", town.Name), "admin/towns/edit.html", map[string]any{ "town": town, }) } func adminTownUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() town, err := towns.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Town %d not found", id)) ctx.Redirect("/admin/towns") return } // Check if delete was requested if ctx.Form("delete").String() == "1" { if err := town.Delete(); err != nil { sess.SetFlash("error", "Failed to delete town") ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Town %s deleted successfully", town.Name)) ctx.Redirect("/admin/towns") return } if err := populateTownFromForm(ctx, town); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } if err := town.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } if err := checkTownCoordinateConflict(town.X, town.Y, town.ID); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } fields := map[string]any{ "name": town.Name, "x": town.X, "y": town.Y, "inn_cost": town.InnCost, "map_cost": town.MapCost, "tp_cost": town.TPCost, "shop_list": town.ShopList, } if err := database.Update("towns", fields, "id", id); err != nil { sess.SetFlash("error", "Failed to update town") ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Town %s updated successfully", town.Name)) ctx.Redirect("/admin/towns") } func populateTownFromForm(ctx sushi.Ctx, town *towns.Town) error { town.Name = strings.TrimSpace(ctx.Form("name").String()) town.X = ctx.Form("x").Int() town.Y = ctx.Form("y").Int() town.InnCost = ctx.Form("inn_cost").Int() town.MapCost = ctx.Form("map_cost").Int() town.TPCost = ctx.Form("tp_cost").Int() town.ShopList = strings.TrimSpace(ctx.Form("shop_list").String()) if town.Name == "" { return fmt.Errorf("town name is required") } return nil } func checkTownCoordinateConflict(x, y, excludeID int) error { existingTown, err := towns.ByCoords(x, y) if err != nil { return nil // No conflict if no town found or database error } if existingTown != nil && existingTown.ID != excludeID { return fmt.Errorf("a town already exists at coordinates (%d, %d): %s", x, y, existingTown.Name) } return nil } func adminSpellsIndex(ctx sushi.Ctx) { pagination := helpers.Pagination{ Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), PerPage: 30, } type SpellData struct { ID int Name string Type int TypeName string MP int Power int Icon string Lore string } var spellList []*SpellData err := database.Select(&spellList, ` SELECT id, name, type, mp, power, icon, lore, CASE type WHEN 0 THEN 'Heal' WHEN 1 THEN 'Damage' WHEN 2 THEN 'Sleep' WHEN 3 THEN 'Uber Attack' WHEN 4 THEN 'Uber Defense' ELSE 'Unknown' END as type_name FROM spells ORDER BY type ASC, mp ASC, id ASC LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) if err != nil { fmt.Printf("Error getting spell list for admin index: %s", err.Error()) spellList = make([]*SpellData, 0) } type CountResult struct{ Count int } var result CountResult database.Get(&result, "SELECT COUNT(*) as count FROM spells") pagination.Total = result.Count components.RenderAdminPage(ctx, "Spell Management", "admin/spells/index.html", map[string]any{ "spells": spellList, "currentPage": pagination.Page, "totalPages": pagination.TotalPages(), "hasNext": pagination.HasNext(), "hasPrev": pagination.HasPrev(), }) } func adminSpellNew(ctx sushi.Ctx) { spell := spells.New() components.RenderAdminPage(ctx, "Add New Spell", "admin/spells/edit.html", map[string]any{ "spell": spell, }) } func adminSpellCreate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() spell := spells.New() if err := populateSpellFromForm(ctx, spell); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/spells/new") return } if err := spell.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/spells/new") return } if err := checkSpellNameConflict(spell.Name, 0); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/spells/new") return } if err := spell.Insert(); err != nil { sess.SetFlash("error", "Failed to create spell") ctx.Redirect("/admin/spells/new") return } sess.SetFlash("success", fmt.Sprintf("Spell %s created successfully", spell.Name)) ctx.Redirect("/admin/spells") } func adminSpellEdit(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() spell, err := spells.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Spell %d not found", id)) ctx.Redirect("/admin/spells") return } components.RenderAdminPage(ctx, fmt.Sprintf("Edit Spell: %s", spell.Name), "admin/spells/edit.html", map[string]any{ "spell": spell, }) } func adminSpellUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() spell, err := spells.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Spell %d not found", id)) ctx.Redirect("/admin/spells") return } // Check if delete was requested if ctx.Form("delete").String() == "1" { if err := spell.Delete(); err != nil { sess.SetFlash("error", "Failed to delete spell") ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Spell %s deleted successfully", spell.Name)) ctx.Redirect("/admin/spells") return } if err := populateSpellFromForm(ctx, spell); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } if err := spell.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } if err := checkSpellNameConflict(spell.Name, spell.ID); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } fields := map[string]any{ "name": spell.Name, "type": spell.Type, "mp": spell.MP, "power": spell.Power, "icon": spell.Icon, "lore": spell.Lore, } if err := database.Update("spells", fields, "id", id); err != nil { sess.SetFlash("error", "Failed to update spell") ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Spell %s updated successfully", spell.Name)) ctx.Redirect("/admin/spells") } func populateSpellFromForm(ctx sushi.Ctx, spell *spells.Spell) error { spell.Name = strings.TrimSpace(ctx.Form("name").String()) spell.Type = ctx.Form("type").Int() spell.MP = ctx.Form("mp").Int() spell.Power = ctx.Form("power").Int() spell.Icon = strings.TrimSpace(ctx.Form("icon").String()) spell.Lore = strings.TrimSpace(ctx.Form("lore").String()) if spell.Name == "" { return fmt.Errorf("spell name is required") } return nil } func checkSpellNameConflict(name string, excludeID int) error { existingSpell, err := spells.ByName(name) if err != nil { return nil // No conflict if no spell found or database error } if existingSpell != nil && existingSpell.ID != excludeID { return fmt.Errorf("a spell with the name '%s' already exists", name) } return nil } func adminMonstersIndex(ctx sushi.Ctx) { pagination := helpers.Pagination{ Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), PerPage: 30, } type MonsterData struct { ID int Name string Level int MaxHP int MaxDmg int Armor int MaxExp int MaxGold int Immune int ImmunityName string } var monsterList []*MonsterData err := database.Select(&monsterList, ` SELECT id, name, level, max_hp, max_dmg, armor, max_exp, max_gold, immune, CASE immune WHEN 0 THEN 'None' WHEN 1 THEN 'Hurt Spells' WHEN 2 THEN 'Sleep Spells' ELSE 'Unknown' END as immunity_name FROM monsters ORDER BY level ASC, id ASC LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) if err != nil { fmt.Printf("Error getting monster list for admin index: %s", err.Error()) monsterList = make([]*MonsterData, 0) } type CountResult struct{ Count int } var result CountResult database.Get(&result, "SELECT COUNT(*) as count FROM monsters") pagination.Total = result.Count components.RenderAdminPage(ctx, "Monster Management", "admin/monsters/index.html", map[string]any{ "monsters": monsterList, "currentPage": pagination.Page, "totalPages": pagination.TotalPages(), "hasNext": pagination.HasNext(), "hasPrev": pagination.HasPrev(), }) } func adminMonsterNew(ctx sushi.Ctx) { monster := monsters.New() components.RenderAdminPage(ctx, "Add New Monster", "admin/monsters/edit.html", map[string]any{ "monster": monster, }) } func adminMonsterCreate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() monster := monsters.New() if err := populateMonsterFromForm(ctx, monster); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/monsters/new") return } if err := monster.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/monsters/new") return } if err := checkMonsterNameConflict(monster.Name, 0); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/monsters/new") return } if err := monster.Insert(); err != nil { sess.SetFlash("error", "Failed to create monster") ctx.Redirect("/admin/monsters/new") return } sess.SetFlash("success", fmt.Sprintf("Monster %s created successfully", monster.Name)) ctx.Redirect("/admin/monsters") } func adminMonsterEdit(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() monster, err := monsters.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Monster %d not found", id)) ctx.Redirect("/admin/monsters") return } components.RenderAdminPage(ctx, fmt.Sprintf("Edit Monster: %s", monster.Name), "admin/monsters/edit.html", map[string]any{ "monster": monster, }) } func adminMonsterUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() monster, err := monsters.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Monster %d not found", id)) ctx.Redirect("/admin/monsters") return } // Check if delete was requested if ctx.Form("delete").String() == "1" { if err := monster.Delete(); err != nil { sess.SetFlash("error", "Failed to delete monster") ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Monster %s deleted successfully", monster.Name)) ctx.Redirect("/admin/monsters") return } if err := populateMonsterFromForm(ctx, monster); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) return } if err := monster.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) return } if err := checkMonsterNameConflict(monster.Name, monster.ID); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) return } fields := map[string]any{ "name": monster.Name, "level": monster.Level, "max_hp": monster.MaxHP, "max_dmg": monster.MaxDmg, "armor": monster.Armor, "max_exp": monster.MaxExp, "max_gold": monster.MaxGold, "immune": monster.Immune, } if err := database.Update("monsters", fields, "id", id); err != nil { sess.SetFlash("error", "Failed to update monster") ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Monster %s updated successfully", monster.Name)) ctx.Redirect("/admin/monsters") } func populateMonsterFromForm(ctx sushi.Ctx, monster *monsters.Monster) error { monster.Name = strings.TrimSpace(ctx.Form("name").String()) monster.Level = ctx.Form("level").Int() monster.MaxHP = ctx.Form("max_hp").Int() monster.MaxDmg = ctx.Form("max_dmg").Int() monster.Armor = ctx.Form("armor").Int() monster.MaxExp = ctx.Form("max_exp").Int() monster.MaxGold = ctx.Form("max_gold").Int() monster.Immune = ctx.Form("immune").Int() if monster.Name == "" { return fmt.Errorf("monster name is required") } return nil } func checkMonsterNameConflict(name string, excludeID int) error { existingMonster, err := monsters.ByName(name) if err != nil { return nil // No conflict if no monster found or database error } if existingMonster != nil && existingMonster.ID != excludeID { return fmt.Errorf("a monster with the name '%s' already exists", name) } return nil } func bToMb(b uint64) uint64 { return b / 1024 / 1024 }