616 lines
16 KiB
Go

package routes
import (
"dk/internal/components"
"dk/internal/database"
"dk/internal/helpers"
"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)
}
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 bToMb(b uint64) uint64 {
return b / 1024 / 1024
}