migrate to sushi/nigiri
This commit is contained in:
parent
2d958bf8c4
commit
e5e9167854
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
|||||||
_sessions.json
|
_sessions.json
|
||||||
users.json
|
users.json
|
||||||
/tmp
|
/tmp
|
||||||
|
wal.log
|
@ -1,8 +1,11 @@
|
|||||||
{
|
[
|
||||||
"world_size": 200,
|
{
|
||||||
"open": 1,
|
"id": 1,
|
||||||
"admin_email": "",
|
"world_size": 200,
|
||||||
"class_1_name": "Mage",
|
"open": 1,
|
||||||
"class_2_name": "Warrior",
|
"admin_email": "",
|
||||||
"class_3_name": "Paladin"
|
"class_1_name": "Mage",
|
||||||
}
|
"class_2_name": "Warrior",
|
||||||
|
"class_3_name": "Paladin"
|
||||||
|
}
|
||||||
|
]
|
166
data/fights.json
166
data/fights.json
@ -824,165 +824,29 @@
|
|||||||
"won": false,
|
"won": false,
|
||||||
"reward_gold": 0,
|
"reward_gold": 0,
|
||||||
"reward_exp": 0,
|
"reward_exp": 0,
|
||||||
"actions": [
|
"actions": [],
|
||||||
{
|
"created": 1755222893,
|
||||||
"t": 1,
|
"updated": 1755222893
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Drakelor"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"created": 1755274841,
|
|
||||||
"updated": 1755275436
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 14,
|
"id": 5,
|
||||||
"user_id": 1,
|
"user_id": 1,
|
||||||
"monster_id": 4,
|
"monster_id": 5,
|
||||||
"monster_hp": 0,
|
"monster_hp": 10,
|
||||||
"monster_max_hp": 10,
|
"monster_max_hp": 10,
|
||||||
"monster_sleep": 0,
|
"monster_sleep": 0,
|
||||||
"monster_immune": 0,
|
"monster_immune": 1,
|
||||||
"uber_damage": 0,
|
"uber_damage": 0,
|
||||||
"uber_defense": 0,
|
"uber_defense": 0,
|
||||||
"first_strike": true,
|
"first_strike": true,
|
||||||
"turn": 5,
|
"turn": 1,
|
||||||
"ran_away": false,
|
"ran_away": false,
|
||||||
"victory": true,
|
"victory": false,
|
||||||
"won": true,
|
"won": false,
|
||||||
"reward_gold": 1,
|
"reward_gold": 0,
|
||||||
"reward_exp": 3,
|
"reward_exp": 0,
|
||||||
"actions": [
|
"actions": [],
|
||||||
{
|
"created": 1755608716,
|
||||||
"t": 1,
|
"updated": 1755608716
|
||||||
"d": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Creature"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Creature"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Creature"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 8,
|
|
||||||
"d": 1,
|
|
||||||
"n": "Creature"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 1,
|
|
||||||
"d": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 11,
|
|
||||||
"n": "Creature"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"created": 1755275442,
|
|
||||||
"updated": 1755275447
|
|
||||||
}
|
}
|
||||||
]
|
]
|
8
go.mod
8
go.mod
@ -1,15 +1,17 @@
|
|||||||
module dk
|
module dk
|
||||||
|
|
||||||
go 1.24.6
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/valyala/fasthttp v1.64.0
|
git.sharkk.net/Sharkk/Nigiri v1.0.0
|
||||||
golang.org/x/crypto v0.41.0
|
git.sharkk.net/Sharkk/Sushi v1.1.0
|
||||||
|
github.com/valyala/fasthttp v1.65.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
)
|
)
|
||||||
|
8
go.sum
8
go.sum
@ -1,11 +1,15 @@
|
|||||||
|
git.sharkk.net/Sharkk/Nigiri v1.0.0 h1:N0MvWOoX54iXjR8D1LqGIFrtMAPdaoj/32n13Ou/p90=
|
||||||
|
git.sharkk.net/Sharkk/Nigiri v1.0.0/go.mod h1:HWpMtXaodPXE7dZXQ6tbZNL0DRV9PT65D0DOV0NAwsM=
|
||||||
|
git.sharkk.net/Sharkk/Sushi v1.1.0 h1:mOcQlcLEl941ozjbOzHOnBAmsOcZ7Q5BkFowILwxNow=
|
||||||
|
git.sharkk.net/Sharkk/Sushi v1.1.0/go.mod h1:S84ACGkuZ+BKzBO4lb5WQnm5aw9+l7VSO2T1bjzxL3o=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
|
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
||||||
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
|
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
|
@ -1,171 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dk/internal/cookies"
|
|
||||||
"dk/internal/helpers"
|
|
||||||
"dk/internal/models/users"
|
|
||||||
"dk/internal/router"
|
|
||||||
"dk/internal/session"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const SessionCookieName = "dk_session"
|
|
||||||
|
|
||||||
func Middleware() router.Middleware {
|
|
||||||
return func(next router.Handler) router.Handler {
|
|
||||||
return func(ctx router.Ctx, params []string) {
|
|
||||||
sessionID := cookies.GetCookie(ctx, SessionCookieName)
|
|
||||||
var sess *session.Session
|
|
||||||
|
|
||||||
if sessionID != "" {
|
|
||||||
if existingSess, exists := session.Get(sessionID); exists {
|
|
||||||
sess = existingSess
|
|
||||||
sess.Touch()
|
|
||||||
|
|
||||||
if sess.UserID > 0 { // User session
|
|
||||||
user, err := users.Find(sess.UserID)
|
|
||||||
if err == nil && user != nil {
|
|
||||||
ctx.SetUserValue("user", user)
|
|
||||||
} else {
|
|
||||||
// User not found, reset to guest session
|
|
||||||
sess.SetUserID(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
session.Store(sess)
|
|
||||||
setSessionCookie(ctx, sessionID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create guest session if none exists
|
|
||||||
if sess == nil {
|
|
||||||
sess = session.Create(0) // Guest session
|
|
||||||
setSessionCookie(ctx, sess.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SetUserValue("session", sess)
|
|
||||||
next(ctx, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(redirect, fasthttp.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ctx.UserValue("user").(*users.User)
|
|
||||||
user.UpdateLastOnline()
|
|
||||||
user.Save()
|
|
||||||
|
|
||||||
next(ctx, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
fmt.Println("RequireGuest: user is authenticated")
|
|
||||||
ctx.Redirect(redirect, fasthttp.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next(ctx, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsAuthenticated(ctx router.Ctx) bool {
|
|
||||||
if user, ok := ctx.UserValue("user").(*users.User); ok && user != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCurrentUser(ctx router.Ctx) *users.User {
|
|
||||||
if user, ok := ctx.UserValue("user").(*users.User); ok {
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCurrentSession(ctx router.Ctx) *session.Session {
|
|
||||||
if sess, ok := ctx.UserValue("session").(*session.Session); ok {
|
|
||||||
return sess
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Login(ctx router.Ctx, user *users.User) {
|
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
|
||||||
|
|
||||||
// Update the session to be authenticated
|
|
||||||
sess.SetUserID(user.ID) // This updates the struct field
|
|
||||||
sess.RegenerateID() // Generate new ID for security
|
|
||||||
sess.SetFlash("success", fmt.Sprintf("Welcome back, %s!", user.Username))
|
|
||||||
|
|
||||||
// Remove any old user_id from session data if it exists
|
|
||||||
sess.Delete("user_id")
|
|
||||||
|
|
||||||
session.Store(sess)
|
|
||||||
|
|
||||||
// Update context values
|
|
||||||
ctx.SetUserValue("session", sess)
|
|
||||||
ctx.SetUserValue("user", user)
|
|
||||||
|
|
||||||
// Update cookie with new session ID
|
|
||||||
setSessionCookie(ctx, sess.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Logout(ctx router.Ctx) {
|
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
|
||||||
if sess != nil {
|
|
||||||
// Convert back to guest session
|
|
||||||
sess.SetUserID(0) // Reset to guest
|
|
||||||
sess.RegenerateID() // Generate new ID for security
|
|
||||||
|
|
||||||
// Clean up any user-related session data
|
|
||||||
sess.Delete("user_id")
|
|
||||||
|
|
||||||
session.Store(sess)
|
|
||||||
ctx.SetUserValue("session", sess)
|
|
||||||
|
|
||||||
// Update cookie with new session ID
|
|
||||||
setSessionCookie(ctx, sess.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SetUserValue("user", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for session cookies
|
|
||||||
func setSessionCookie(ctx router.Ctx, sessionID string) {
|
|
||||||
cookies.SetSecureCookie(ctx, cookies.CookieOptions{
|
|
||||||
Name: SessionCookieName,
|
|
||||||
Value: sessionID,
|
|
||||||
Path: "/",
|
|
||||||
Expires: time.Now().Add(24 * time.Hour),
|
|
||||||
HTTPOnly: true,
|
|
||||||
Secure: helpers.IsHTTPS(ctx),
|
|
||||||
SameSite: "lax",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteSessionCookie(ctx router.Ctx) {
|
|
||||||
cookies.DeleteCookie(ctx, SessionCookieName)
|
|
||||||
}
|
|
@ -1,21 +1,21 @@
|
|||||||
package components
|
package components
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/auth"
|
|
||||||
"dk/internal/helpers"
|
"dk/internal/helpers"
|
||||||
"dk/internal/models/spells"
|
"dk/internal/models/spells"
|
||||||
"dk/internal/models/towns"
|
"dk/internal/models/towns"
|
||||||
"dk/internal/models/users"
|
"dk/internal/models/users"
|
||||||
"dk/internal/router"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LeftAside generates the data map for the left sidebar.
|
// LeftAside generates the data map for the left sidebar.
|
||||||
// Returns an empty map when not auth'd.
|
// Returns an empty map when not auth'd.
|
||||||
func LeftAside(ctx router.Ctx) map[string]any {
|
func LeftAside(ctx sushi.Ctx) map[string]any {
|
||||||
data := map[string]any{}
|
data := map[string]any{}
|
||||||
|
|
||||||
if !auth.IsAuthenticated(ctx) {
|
if !ctx.IsAuthenticated() {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,10 +37,10 @@ func LeftAside(ctx router.Ctx) map[string]any {
|
|||||||
|
|
||||||
// RightAside generates the data map for the right sidebar.
|
// RightAside generates the data map for the right sidebar.
|
||||||
// Returns an empty map when not auth'd.
|
// Returns an empty map when not auth'd.
|
||||||
func RightAside(ctx router.Ctx) map[string]any {
|
func RightAside(ctx sushi.Ctx) map[string]any {
|
||||||
data := map[string]any{}
|
data := map[string]any{}
|
||||||
|
|
||||||
if !auth.IsAuthenticated(ctx) {
|
if !ctx.IsAuthenticated() {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,16 +6,14 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dk/internal/auth"
|
|
||||||
"dk/internal/csrf"
|
|
||||||
"dk/internal/middleware"
|
|
||||||
"dk/internal/router"
|
|
||||||
"dk/internal/session"
|
|
||||||
"dk/internal/template"
|
"dk/internal/template"
|
||||||
|
|
||||||
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/csrf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RenderPage renders a page using the layout template with common data and additional custom data
|
// RenderPage renders a page using the layout template with common data and additional custom data
|
||||||
func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[string]any) error {
|
func RenderPage(ctx sushi.Ctx, title, tmplPath string, additionalData map[string]any) error {
|
||||||
if template.Cache == nil {
|
if template.Cache == nil {
|
||||||
return fmt.Errorf("template.Cache not initialized")
|
return fmt.Errorf("template.Cache not initialized")
|
||||||
}
|
}
|
||||||
@ -25,19 +23,19 @@ func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[strin
|
|||||||
return fmt.Errorf("failed to load layout template: %w", err)
|
return fmt.Errorf("failed to load layout template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
|
||||||
|
|
||||||
var m runtime.MemStats
|
var m runtime.MemStats
|
||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
|
sess := ctx.GetCurrentSession()
|
||||||
|
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"_title": PageTitle(title),
|
"_title": PageTitle(title),
|
||||||
"authenticated": auth.IsAuthenticated(ctx),
|
"authenticated": ctx.IsAuthenticated(),
|
||||||
"csrf": csrf.HiddenField(ctx),
|
"csrf": csrf.HiddenField(ctx),
|
||||||
"_totaltime": middleware.GetRequestTime(ctx),
|
"_totaltime": ctx.UserValue("request_time"),
|
||||||
"_version": "1.0.0",
|
"_version": "1.0.0",
|
||||||
"_build": "dev",
|
"_build": "dev",
|
||||||
"user": auth.GetCurrentUser(ctx),
|
"user": ctx.GetCurrentUser(),
|
||||||
"_memalloc": m.Alloc / 1024 / 1024,
|
"_memalloc": m.Alloc / 1024 / 1024,
|
||||||
"_errormsg": sess.GetFlashMessage("error"),
|
"_errormsg": sess.GetFlashMessage("error"),
|
||||||
"_successmsg": sess.GetFlashMessage("success"),
|
"_successmsg": sess.GetFlashMessage("success"),
|
||||||
@ -47,8 +45,7 @@ func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[strin
|
|||||||
maps.Copy(data, RightAside(ctx))
|
maps.Copy(data, RightAside(ctx))
|
||||||
maps.Copy(data, additionalData)
|
maps.Copy(data, additionalData)
|
||||||
|
|
||||||
tmpl.WriteTo(ctx, data)
|
return tmpl.WriteTo(ctx, data)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageTitle returns a proper title for a rendered page. If an empty string
|
// PageTitle returns a proper title for a rendered page. If an empty string
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
package cookies
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CookieOptions struct {
|
|
||||||
Name string
|
|
||||||
Value string
|
|
||||||
Path string
|
|
||||||
Domain string
|
|
||||||
Expires time.Time
|
|
||||||
MaxAge int
|
|
||||||
Secure bool
|
|
||||||
HTTPOnly bool
|
|
||||||
SameSite string
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetSecureCookie(ctx *fasthttp.RequestCtx, opts CookieOptions) {
|
|
||||||
cookie := &fasthttp.Cookie{}
|
|
||||||
|
|
||||||
cookie.SetKey(opts.Name)
|
|
||||||
cookie.SetValue(opts.Value)
|
|
||||||
|
|
||||||
if opts.Path != "" {
|
|
||||||
cookie.SetPath(opts.Path)
|
|
||||||
} else {
|
|
||||||
cookie.SetPath("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.Domain != "" {
|
|
||||||
cookie.SetDomain(opts.Domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.Expires.IsZero() {
|
|
||||||
cookie.SetExpire(opts.Expires)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.MaxAge > 0 {
|
|
||||||
cookie.SetMaxAge(opts.MaxAge)
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie.SetSecure(opts.Secure)
|
|
||||||
cookie.SetHTTPOnly(opts.HTTPOnly)
|
|
||||||
|
|
||||||
switch opts.SameSite {
|
|
||||||
case "strict":
|
|
||||||
cookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
|
|
||||||
case "lax":
|
|
||||||
cookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
|
|
||||||
case "none":
|
|
||||||
cookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
|
|
||||||
default:
|
|
||||||
cookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Response.Header.SetCookie(cookie)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCookie(ctx *fasthttp.RequestCtx, name string) string {
|
|
||||||
return string(ctx.Request.Header.Cookie(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteCookie(ctx *fasthttp.RequestCtx, name string) {
|
|
||||||
SetSecureCookie(ctx, CookieOptions{
|
|
||||||
Name: name,
|
|
||||||
Value: "",
|
|
||||||
Path: "/",
|
|
||||||
Expires: time.Unix(0, 0),
|
|
||||||
MaxAge: -1,
|
|
||||||
HTTPOnly: true,
|
|
||||||
Secure: true,
|
|
||||||
SameSite: "lax",
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,195 +0,0 @@
|
|||||||
// Package csrf provides Cross-Site Request Forgery (CSRF) protection
|
|
||||||
// with session-based token storage and form helpers.
|
|
||||||
//
|
|
||||||
// # Basic Usage
|
|
||||||
//
|
|
||||||
// // Generate token and store in session
|
|
||||||
// token := csrf.GenerateToken(ctx)
|
|
||||||
//
|
|
||||||
// // In templates - generate hidden input field
|
|
||||||
// hiddenField := csrf.HiddenField(ctx)
|
|
||||||
//
|
|
||||||
// // Verify form submission
|
|
||||||
// if !csrf.ValidateToken(ctx, formToken) {
|
|
||||||
// // Handle CSRF validation failure
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// # Middleware Integration
|
|
||||||
//
|
|
||||||
// // Add CSRF middleware to protected routes
|
|
||||||
// r.Use(csrf.Middleware())
|
|
||||||
package csrf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"dk/internal/router"
|
|
||||||
"dk/internal/session"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
TokenLength = 32
|
|
||||||
TokenFieldName = "_csrf_token"
|
|
||||||
SessionKey = "csrf_token"
|
|
||||||
SessionCtxKey = "session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetCurrentSession retrieves the session from context
|
|
||||||
func GetCurrentSession(ctx router.Ctx) *session.Session {
|
|
||||||
if sess, ok := ctx.UserValue(SessionCtxKey).(*session.Session); ok {
|
|
||||||
return sess
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateToken creates a new CSRF token and stores it in the session
|
|
||||||
func GenerateToken(ctx router.Ctx) string {
|
|
||||||
// Generate cryptographically secure random bytes
|
|
||||||
tokenBytes := make([]byte, TokenLength)
|
|
||||||
if _, err := rand.Read(tokenBytes); err != nil {
|
|
||||||
// Fallback - this should never happen in practice
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
token := base64.URLEncoding.EncodeToString(tokenBytes)
|
|
||||||
|
|
||||||
// Store token in session (both guests and authenticated users have sessions)
|
|
||||||
if sess := GetCurrentSession(ctx); sess != nil {
|
|
||||||
StoreToken(sess, token)
|
|
||||||
session.Store(sess)
|
|
||||||
}
|
|
||||||
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetToken retrieves the current CSRF token from session, generating one if needed
|
|
||||||
func GetToken(ctx router.Ctx) string {
|
|
||||||
sess := GetCurrentSession(ctx)
|
|
||||||
if sess == nil {
|
|
||||||
return "" // No session available
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing token
|
|
||||||
if existingToken := GetStoredToken(sess); existingToken != "" {
|
|
||||||
return existingToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new token if none exists
|
|
||||||
return GenerateToken(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateToken verifies a CSRF token against the stored session token
|
|
||||||
func ValidateToken(ctx router.Ctx, submittedToken string) bool {
|
|
||||||
if submittedToken == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
sess := GetCurrentSession(ctx)
|
|
||||||
if sess == nil {
|
|
||||||
return false // No session
|
|
||||||
}
|
|
||||||
|
|
||||||
storedToken := GetStoredToken(sess)
|
|
||||||
if storedToken == "" {
|
|
||||||
return false // No stored token
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use constant-time comparison to prevent timing attacks
|
|
||||||
return subtle.ConstantTimeCompare([]byte(submittedToken), []byte(storedToken)) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// StoreToken saves a CSRF token in the session
|
|
||||||
func StoreToken(sess *session.Session, token string) {
|
|
||||||
sess.Set(SessionKey, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStoredToken retrieves the CSRF token from session
|
|
||||||
func GetStoredToken(sess *session.Session) string {
|
|
||||||
if token, ok := sess.Get(SessionKey); ok {
|
|
||||||
if tokenStr, ok := token.(string); ok {
|
|
||||||
return tokenStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// RotateToken generates a new token and replaces the old one in the session
|
|
||||||
func RotateToken(ctx router.Ctx) string {
|
|
||||||
sess := GetCurrentSession(ctx)
|
|
||||||
if sess == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new token (this will automatically store it)
|
|
||||||
newToken := GenerateToken(ctx)
|
|
||||||
return newToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// HiddenField generates an HTML hidden input field with the CSRF token
|
|
||||||
func HiddenField(ctx router.Ctx) string {
|
|
||||||
token := GetToken(ctx)
|
|
||||||
if token == "" {
|
|
||||||
return "" // No token available
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`,
|
|
||||||
TokenFieldName, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenMeta generates HTML meta tag for JavaScript access to CSRF token
|
|
||||||
func TokenMeta(ctx router.Ctx) string {
|
|
||||||
token := GetToken(ctx)
|
|
||||||
if token == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(`<meta name="csrf-token" content="%s">`, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateFormToken is a convenience function to validate CSRF token from form data
|
|
||||||
func ValidateFormToken(ctx router.Ctx) bool {
|
|
||||||
// Try to get token from form data
|
|
||||||
tokenBytes := ctx.PostArgs().Peek(TokenFieldName)
|
|
||||||
if len(tokenBytes) == 0 {
|
|
||||||
// Try from query parameters as fallback
|
|
||||||
tokenBytes = ctx.QueryArgs().Peek(TokenFieldName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tokenBytes) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValidateToken(ctx, string(tokenBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTokenFromCookie retrieves a CSRF token from cookie (legacy support)
|
|
||||||
func GetTokenFromCookie(ctx router.Ctx) string {
|
|
||||||
return string(ctx.Request.Header.Cookie("_csrf"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Middleware returns a middleware function that automatically validates CSRF tokens
|
|
||||||
// for state-changing HTTP methods (POST, PUT, PATCH, DELETE)
|
|
||||||
func Middleware() router.Middleware {
|
|
||||||
return func(next router.Handler) router.Handler {
|
|
||||||
return func(ctx router.Ctx, params []string) {
|
|
||||||
method := string(ctx.Method())
|
|
||||||
|
|
||||||
// Only validate CSRF for state-changing methods
|
|
||||||
if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" {
|
|
||||||
if !ValidateFormToken(ctx) {
|
|
||||||
fmt.Println("Failed CSRF validation.")
|
|
||||||
RotateToken(ctx)
|
|
||||||
currentPath := string(ctx.Path())
|
|
||||||
ctx.Redirect(currentPath, 302)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue to next handler
|
|
||||||
next(ctx, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dk/internal/models/users"
|
|
||||||
"dk/internal/router"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RequireFighting ensures the user is in a fight when accessing fight routes
|
|
||||||
func RequireFighting() 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.IsFighting() {
|
|
||||||
ctx.Redirect("/", 303)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next(ctx, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleFightRedirect redirects users to appropriate locations based on fight status
|
|
||||||
func HandleFightRedirect() 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 {
|
|
||||||
next(ctx, params)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPath := string(ctx.URI().Path())
|
|
||||||
|
|
||||||
// If user is fighting and not on fight page, redirect to fight
|
|
||||||
if user.IsFighting() && !strings.HasPrefix(currentPath, "/fight") {
|
|
||||||
ctx.Redirect("/fight", 303)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If user is not fighting and on fight page, redirect to home
|
|
||||||
if !user.IsFighting() && strings.HasPrefix(currentPath, "/fight") {
|
|
||||||
ctx.Redirect("/", 303)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next(ctx, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"dk/internal/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
const RequestTimerKey = "request_start_time"
|
|
||||||
|
|
||||||
// Timing adds request timing functionality
|
|
||||||
func Timing() router.Middleware {
|
|
||||||
return func(next router.Handler) router.Handler {
|
|
||||||
return func(ctx router.Ctx, params []string) {
|
|
||||||
startTime := time.Now()
|
|
||||||
ctx.SetUserValue(RequestTimerKey, startTime)
|
|
||||||
|
|
||||||
next(ctx, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRequestTime returns the total request processing time in seconds (formatted)
|
|
||||||
func GetRequestTime(ctx router.Ctx) string {
|
|
||||||
startTime, ok := ctx.UserValue(RequestTimerKey).(time.Time)
|
|
||||||
if !ok {
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
duration := time.Since(startTime)
|
|
||||||
seconds := duration.Seconds()
|
|
||||||
|
|
||||||
if seconds < 0.001 {
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%.3f", seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRequestDuration returns the raw duration
|
|
||||||
func GetRequestDuration(ctx router.Ctx) time.Duration {
|
|
||||||
startTime, ok := ctx.UserValue(RequestTimerKey).(time.Time)
|
|
||||||
if !ok {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Since(startTime)
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dk/internal/models/towns"
|
|
||||||
"dk/internal/models/users"
|
|
||||||
"dk/internal/router"
|
|
||||||
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +1,22 @@
|
|||||||
package babble
|
package babble
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Babble represents a global chat message in the game
|
// Babble represents a global chat message in the game
|
||||||
type Babble struct {
|
type Babble struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Posted int64 `json:"posted"`
|
Posted int64 `json:"posted"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author" db:"index"`
|
||||||
Babble string `json:"babble"`
|
Babble string `json:"babble"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Babble) Save() error {
|
|
||||||
return GetStore().UpdateWithRebuild(b.ID, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Babble) Delete() error {
|
|
||||||
GetStore().RemoveWithRebuild(b.ID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new Babble with sensible defaults
|
|
||||||
func New() *Babble {
|
|
||||||
return &Babble{
|
|
||||||
Posted: time.Now().Unix(),
|
|
||||||
Author: "",
|
|
||||||
Babble: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks if babble has valid values
|
// Validate checks if babble has valid values
|
||||||
func (b *Babble) Validate() error {
|
func (b *Babble) Validate() error {
|
||||||
if b.Posted <= 0 {
|
if b.Posted <= 0 {
|
||||||
@ -48,58 +31,78 @@ func (b *Babble) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BabbleStore with enhanced BaseStore
|
|
||||||
type BabbleStore struct {
|
|
||||||
*store.BaseStore[Babble]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global store with singleton pattern
|
// Global store with singleton pattern
|
||||||
var GetStore = store.NewSingleton(func() *BabbleStore {
|
var store *nigiri.BaseStore[Babble]
|
||||||
bs := &BabbleStore{BaseStore: store.NewBaseStore[Babble]()}
|
var db *nigiri.Collection
|
||||||
|
|
||||||
// Register indices
|
// Init sets up the Nigiri store and indices
|
||||||
bs.RegisterIndex("byAuthor", store.BuildStringGroupIndex(func(b *Babble) string {
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[Babble]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byAuthor", nigiri.BuildStringGroupIndex(func(b *Babble) string {
|
||||||
return strings.ToLower(b.Author)
|
return strings.ToLower(b.Author)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
bs.RegisterIndex("allByPosted", store.BuildSortedListIndex(func(a, b *Babble) bool {
|
store.RegisterIndex("allByPosted", nigiri.BuildSortedListIndex(func(a, b *Babble) bool {
|
||||||
if a.Posted != b.Posted {
|
if a.Posted != b.Posted {
|
||||||
return a.Posted > b.Posted // DESC
|
return a.Posted > b.Posted // DESC
|
||||||
}
|
}
|
||||||
return a.ID > b.ID // DESC
|
return a.ID > b.ID // DESC
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return bs
|
store.RebuildIndices()
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (bs *BabbleStore) AddBabble(babble *Babble) error {
|
|
||||||
return bs.AddWithRebuild(babble.ID, babble)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bs *BabbleStore) RemoveBabble(id int) {
|
// GetStore returns the babble store
|
||||||
bs.RemoveWithRebuild(id)
|
func GetStore() *nigiri.BaseStore[Babble] {
|
||||||
|
if store == nil {
|
||||||
|
panic("babble store not initialized - call Initialize first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bs *BabbleStore) UpdateBabble(babble *Babble) error {
|
// Creates a new Babble with sensible defaults
|
||||||
return bs.UpdateWithRebuild(babble.ID, babble)
|
func New() *Babble {
|
||||||
|
return &Babble{
|
||||||
|
Posted: time.Now().Unix(),
|
||||||
|
Author: "",
|
||||||
|
Babble: "",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data persistence
|
// CRUD operations
|
||||||
func LoadData(dataPath string) error {
|
func (b *Babble) Save() error {
|
||||||
bs := GetStore()
|
if b.ID == 0 {
|
||||||
return bs.BaseStore.LoadData(dataPath)
|
id, err := store.Create(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.ID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return store.Update(b.ID, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
func (b *Babble) Delete() error {
|
||||||
bs := GetStore()
|
store.Remove(b.ID)
|
||||||
return bs.BaseStore.SaveData(dataPath)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query functions using enhanced store
|
// Insert with ID assignment
|
||||||
|
func (b *Babble) Insert() error {
|
||||||
|
id, err := store.Create(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.ID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query functions
|
||||||
func Find(id int) (*Babble, error) {
|
func Find(id int) (*Babble, error) {
|
||||||
bs := GetStore()
|
babble, exists := store.Find(id)
|
||||||
babble, exists := bs.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("babble with ID %d not found", id)
|
return nil, fmt.Errorf("babble with ID %d not found", id)
|
||||||
}
|
}
|
||||||
@ -107,13 +110,11 @@ func Find(id int) (*Babble, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*Babble, error) {
|
func All() ([]*Babble, error) {
|
||||||
bs := GetStore()
|
return store.AllSorted("allByPosted"), nil
|
||||||
return bs.AllSorted("allByPosted"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByAuthor(author string) ([]*Babble, error) {
|
func ByAuthor(author string) ([]*Babble, error) {
|
||||||
bs := GetStore()
|
messages := store.GroupByIndex("byAuthor", strings.ToLower(author))
|
||||||
messages := bs.GroupByIndex("byAuthor", strings.ToLower(author))
|
|
||||||
|
|
||||||
// Sort by posted DESC, then ID DESC
|
// Sort by posted DESC, then ID DESC
|
||||||
sort.Slice(messages, func(i, j int) bool {
|
sort.Slice(messages, func(i, j int) bool {
|
||||||
@ -127,8 +128,7 @@ func ByAuthor(author string) ([]*Babble, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Recent(limit int) ([]*Babble, error) {
|
func Recent(limit int) ([]*Babble, error) {
|
||||||
bs := GetStore()
|
all := store.AllSorted("allByPosted")
|
||||||
all := bs.AllSorted("allByPosted")
|
|
||||||
if limit > len(all) {
|
if limit > len(all) {
|
||||||
limit = len(all)
|
limit = len(all)
|
||||||
}
|
}
|
||||||
@ -136,23 +136,20 @@ func Recent(limit int) ([]*Babble, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Since(since int64) ([]*Babble, error) {
|
func Since(since int64) ([]*Babble, error) {
|
||||||
bs := GetStore()
|
return store.FilterByIndex("allByPosted", func(b *Babble) bool {
|
||||||
return bs.FilterByIndex("allByPosted", func(b *Babble) bool {
|
|
||||||
return b.Posted >= since
|
return b.Posted >= since
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Between(start, end int64) ([]*Babble, error) {
|
func Between(start, end int64) ([]*Babble, error) {
|
||||||
bs := GetStore()
|
return store.FilterByIndex("allByPosted", func(b *Babble) bool {
|
||||||
return bs.FilterByIndex("allByPosted", func(b *Babble) bool {
|
|
||||||
return b.Posted >= start && b.Posted <= end
|
return b.Posted >= start && b.Posted <= end
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Search(term string) ([]*Babble, error) {
|
func Search(term string) ([]*Babble, error) {
|
||||||
bs := GetStore()
|
|
||||||
lowerTerm := strings.ToLower(term)
|
lowerTerm := strings.ToLower(term)
|
||||||
return bs.FilterByIndex("allByPosted", func(b *Babble) bool {
|
return store.FilterByIndex("allByPosted", func(b *Babble) bool {
|
||||||
return strings.Contains(strings.ToLower(b.Babble), lowerTerm)
|
return strings.Contains(strings.ToLower(b.Babble), lowerTerm)
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
@ -168,15 +165,6 @@ func RecentByAuthor(author string, limit int) ([]*Babble, error) {
|
|||||||
return messages[:limit], nil
|
return messages[:limit], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (b *Babble) Insert() error {
|
|
||||||
bs := GetStore()
|
|
||||||
if b.ID == 0 {
|
|
||||||
b.ID = bs.GetNextID()
|
|
||||||
}
|
|
||||||
return bs.AddBabble(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
func (b *Babble) PostedTime() time.Time {
|
func (b *Babble) PostedTime() time.Time {
|
||||||
return time.Unix(b.Posted, 0)
|
return time.Unix(b.Posted, 0)
|
||||||
@ -279,3 +267,14 @@ func (b *Babble) HasMention(username string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (will be removed later)
|
||||||
|
func LoadData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,24 +1,22 @@
|
|||||||
package control
|
package control
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
global *Control
|
store *nigiri.BaseStore[Control]
|
||||||
configPath string
|
db *nigiri.Collection
|
||||||
mu sync.RWMutex
|
global *Control
|
||||||
|
mu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
global = New()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Control represents the game control settings
|
// Control represents the game control settings
|
||||||
type Control struct {
|
type Control struct {
|
||||||
|
ID int `json:"id"`
|
||||||
WorldSize int `json:"world_size"`
|
WorldSize int `json:"world_size"`
|
||||||
Open int `json:"open"`
|
Open int `json:"open"`
|
||||||
AdminEmail string `json:"admin_email"`
|
AdminEmail string `json:"admin_email"`
|
||||||
@ -27,9 +25,46 @@ type Control struct {
|
|||||||
Class3Name string `json:"class_3_name"`
|
Class3Name string `json:"class_3_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init sets up the Nigiri store for control settings
|
||||||
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[Control]()
|
||||||
|
|
||||||
|
// Load or create the singleton control instance
|
||||||
|
all := store.GetAll()
|
||||||
|
if len(all) == 0 {
|
||||||
|
// Create default control settings
|
||||||
|
global = New()
|
||||||
|
global.ID = 1
|
||||||
|
store.Add(1, global)
|
||||||
|
} else {
|
||||||
|
// Use the first (and only) control entry
|
||||||
|
for _, ctrl := range all {
|
||||||
|
global = ctrl
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Apply defaults for any missing fields
|
||||||
|
defaults := New()
|
||||||
|
if global.WorldSize == 0 {
|
||||||
|
global.WorldSize = defaults.WorldSize
|
||||||
|
}
|
||||||
|
if global.Class1Name == "" {
|
||||||
|
global.Class1Name = defaults.Class1Name
|
||||||
|
}
|
||||||
|
if global.Class2Name == "" {
|
||||||
|
global.Class2Name = defaults.Class2Name
|
||||||
|
}
|
||||||
|
if global.Class3Name == "" {
|
||||||
|
global.Class3Name = defaults.Class3Name
|
||||||
|
}
|
||||||
|
store.Update(global.ID, global)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// New creates a new Control with sensible defaults
|
// New creates a new Control with sensible defaults
|
||||||
func New() *Control {
|
func New() *Control {
|
||||||
return &Control{
|
return &Control{
|
||||||
|
ID: 1, // Singleton
|
||||||
WorldSize: 200,
|
WorldSize: 200,
|
||||||
Open: 1,
|
Open: 1,
|
||||||
AdminEmail: "",
|
AdminEmail: "",
|
||||||
@ -39,84 +74,56 @@ func New() *Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads control settings from JSON file into global instance
|
|
||||||
func Load(filename string) error {
|
|
||||||
mu.Lock()
|
|
||||||
configPath = filename
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil // Keep defaults
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to read config file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
control := &Control{}
|
|
||||||
if err := json.Unmarshal(data, control); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply defaults for any missing fields
|
|
||||||
defaults := New()
|
|
||||||
if control.WorldSize == 0 {
|
|
||||||
control.WorldSize = defaults.WorldSize
|
|
||||||
}
|
|
||||||
if control.Class1Name == "" {
|
|
||||||
control.Class1Name = defaults.Class1Name
|
|
||||||
}
|
|
||||||
if control.Class2Name == "" {
|
|
||||||
control.Class2Name = defaults.Class2Name
|
|
||||||
}
|
|
||||||
if control.Class3Name == "" {
|
|
||||||
control.Class3Name = defaults.Class3Name
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
global = control
|
|
||||||
mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save saves global control settings to the loaded path
|
|
||||||
func Save() error {
|
|
||||||
mu.RLock()
|
|
||||||
defer mu.RUnlock()
|
|
||||||
|
|
||||||
if configPath == "" {
|
|
||||||
return fmt.Errorf("no config path set, call Load() first")
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.MarshalIndent(global, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write config file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the global control instance (thread-safe)
|
// Get returns the global control instance (thread-safe)
|
||||||
func Get() *Control {
|
func Get() *Control {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
defer mu.RUnlock()
|
defer mu.RUnlock()
|
||||||
|
if global == nil {
|
||||||
|
panic("control not initialized - call Initialize first")
|
||||||
|
}
|
||||||
return global
|
return global
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set updates the global control instance (thread-safe)
|
// Set updates the global control instance (thread-safe)
|
||||||
func Set(control *Control) {
|
func Set(control *Control) error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
control.ID = 1 // Ensure it's always ID 1 (singleton)
|
||||||
|
if err := control.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.Update(1, control); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
global = control
|
global = control
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update updates specific fields of the control settings
|
||||||
|
func Update(updater func(*Control)) error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
// Create a copy to work with
|
||||||
|
updated := *global
|
||||||
|
updater(&updated)
|
||||||
|
|
||||||
|
if err := updated.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.Update(1, &updated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
global = &updated
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if control settings have valid values
|
||||||
func (c *Control) Validate() error {
|
func (c *Control) Validate() error {
|
||||||
if c.WorldSize <= 0 || c.WorldSize > 10000 {
|
if c.WorldSize <= 0 || c.WorldSize > 10000 {
|
||||||
return fmt.Errorf("WorldSize must be between 1 and 10000")
|
return fmt.Errorf("WorldSize must be between 1 and 10000")
|
||||||
@ -124,6 +131,15 @@ func (c *Control) Validate() error {
|
|||||||
if c.Open != 0 && c.Open != 1 {
|
if c.Open != 0 && c.Open != 1 {
|
||||||
return fmt.Errorf("Open must be 0 or 1")
|
return fmt.Errorf("Open must be 0 or 1")
|
||||||
}
|
}
|
||||||
|
if c.Class1Name == "" {
|
||||||
|
return fmt.Errorf("Class1Name cannot be empty")
|
||||||
|
}
|
||||||
|
if c.Class2Name == "" {
|
||||||
|
return fmt.Errorf("Class2Name cannot be empty")
|
||||||
|
}
|
||||||
|
if c.Class3Name == "" {
|
||||||
|
return fmt.Errorf("Class3Name cannot be empty")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +148,17 @@ func (c *Control) IsOpen() bool {
|
|||||||
return c.Open == 1
|
return c.Open == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOpen sets whether the game world is open for new players
|
||||||
|
func SetOpen(open bool) error {
|
||||||
|
return Update(func(c *Control) {
|
||||||
|
if open {
|
||||||
|
c.Open = 1
|
||||||
|
} else {
|
||||||
|
c.Open = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// GetClassNames returns all class names as a slice
|
// GetClassNames returns all class names as a slice
|
||||||
func (c *Control) GetClassNames() []string {
|
func (c *Control) GetClassNames() []string {
|
||||||
classes := make([]string, 0, 3)
|
classes := make([]string, 0, 3)
|
||||||
@ -209,3 +236,14 @@ func (c *Control) GetWorldBounds() (minX, minY, maxX, maxY int) {
|
|||||||
radius := c.GetWorldRadius()
|
radius := c.GetWorldRadius()
|
||||||
return -radius, -radius, radius, radius
|
return -radius, -radius, radius, radius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (will be removed later)
|
||||||
|
func Load(filename string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Save() error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,26 +1,56 @@
|
|||||||
package drops
|
package drops
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Drop represents a drop item in the game
|
// Drop represents a drop item in the game
|
||||||
type Drop struct {
|
type Drop struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" db:"required"`
|
||||||
Level int `json:"level"`
|
Level int `json:"level" db:"index"`
|
||||||
Type int `json:"type"`
|
Type int `json:"type" db:"index"`
|
||||||
Att string `json:"att"`
|
Att string `json:"att"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Drop) Save() error {
|
// DropType constants for drop types
|
||||||
return GetStore().UpdateWithRebuild(d.ID, d)
|
const (
|
||||||
|
TypeConsumable = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global store
|
||||||
|
var store *nigiri.BaseStore[Drop]
|
||||||
|
var db *nigiri.Collection
|
||||||
|
|
||||||
|
// Init sets up the Nigiri store and indices
|
||||||
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[Drop]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byLevel", nigiri.BuildIntGroupIndex(func(d *Drop) int {
|
||||||
|
return d.Level
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byType", nigiri.BuildIntGroupIndex(func(d *Drop) int {
|
||||||
|
return d.Type
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByID", nigiri.BuildSortedListIndex(func(a, b *Drop) bool {
|
||||||
|
return a.ID < b.ID
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RebuildIndices()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Drop) Delete() error {
|
// GetStore returns the drops store
|
||||||
GetStore().RemoveWithRebuild(d.ID)
|
func GetStore() *nigiri.BaseStore[Drop] {
|
||||||
return nil
|
if store == nil {
|
||||||
|
panic("drops store not initialized - call Initialize first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new Drop with sensible defaults
|
// Creates a new Drop with sensible defaults
|
||||||
@ -47,64 +77,37 @@ func (d *Drop) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DropType constants for drop types
|
// CRUD operations
|
||||||
const (
|
func (d *Drop) Save() error {
|
||||||
TypeConsumable = 1
|
if d.ID == 0 {
|
||||||
)
|
id, err := store.Create(d)
|
||||||
|
if err != nil {
|
||||||
// DropStore with enhanced BaseStore
|
return err
|
||||||
type DropStore struct {
|
}
|
||||||
*store.BaseStore[Drop]
|
d.ID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return store.Update(d.ID, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global store with singleton pattern
|
func (d *Drop) Delete() error {
|
||||||
var GetStore = store.NewSingleton(func() *DropStore {
|
store.Remove(d.ID)
|
||||||
ds := &DropStore{BaseStore: store.NewBaseStore[Drop]()}
|
return nil
|
||||||
|
|
||||||
// Register indices
|
|
||||||
ds.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(d *Drop) int {
|
|
||||||
return d.Level
|
|
||||||
}))
|
|
||||||
|
|
||||||
ds.RegisterIndex("byType", store.BuildIntGroupIndex(func(d *Drop) int {
|
|
||||||
return d.Type
|
|
||||||
}))
|
|
||||||
|
|
||||||
ds.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Drop) bool {
|
|
||||||
return a.ID < b.ID
|
|
||||||
}))
|
|
||||||
|
|
||||||
return ds
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (ds *DropStore) AddDrop(drop *Drop) error {
|
|
||||||
return ds.AddWithRebuild(drop.ID, drop)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *DropStore) RemoveDrop(id int) {
|
// Insert with ID assignment
|
||||||
ds.RemoveWithRebuild(id)
|
func (d *Drop) Insert() error {
|
||||||
|
id, err := store.Create(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.ID = id
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *DropStore) UpdateDrop(drop *Drop) error {
|
// Query functions
|
||||||
return ds.UpdateWithRebuild(drop.ID, drop)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data persistence
|
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
ds := GetStore()
|
|
||||||
return ds.BaseStore.LoadData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
ds := GetStore()
|
|
||||||
return ds.BaseStore.SaveData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query functions using enhanced store
|
|
||||||
func Find(id int) (*Drop, error) {
|
func Find(id int) (*Drop, error) {
|
||||||
ds := GetStore()
|
drop, exists := store.Find(id)
|
||||||
drop, exists := ds.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("drop with ID %d not found", id)
|
return nil, fmt.Errorf("drop with ID %d not found", id)
|
||||||
}
|
}
|
||||||
@ -112,29 +115,17 @@ func Find(id int) (*Drop, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*Drop, error) {
|
func All() ([]*Drop, error) {
|
||||||
ds := GetStore()
|
return store.AllSorted("allByID"), nil
|
||||||
return ds.AllSorted("allByID"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByLevel(minLevel int) ([]*Drop, error) {
|
func ByLevel(minLevel int) ([]*Drop, error) {
|
||||||
ds := GetStore()
|
return store.FilterByIndex("allByID", func(d *Drop) bool {
|
||||||
return ds.FilterByIndex("allByID", func(d *Drop) bool {
|
|
||||||
return d.Level <= minLevel
|
return d.Level <= minLevel
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByType(dropType int) ([]*Drop, error) {
|
func ByType(dropType int) ([]*Drop, error) {
|
||||||
ds := GetStore()
|
return store.GroupByIndex("byType", dropType), nil
|
||||||
return ds.GroupByIndex("byType", dropType), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (d *Drop) Insert() error {
|
|
||||||
ds := GetStore()
|
|
||||||
if d.ID == 0 {
|
|
||||||
d.ID = ds.GetNextID()
|
|
||||||
}
|
|
||||||
return ds.AddDrop(d)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
@ -150,3 +141,14 @@ func (d *Drop) TypeName() string {
|
|||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (will be removed later)
|
||||||
|
func LoadData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
package fights
|
package fights
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fight represents a fight, past or present
|
// Fight represents a fight, past or present
|
||||||
type Fight struct {
|
type Fight struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id" db:"index"`
|
||||||
MonsterID int `json:"monster_id"`
|
MonsterID int `json:"monster_id" db:"index"`
|
||||||
MonsterHP int `json:"monster_hp"`
|
MonsterHP int `json:"monster_hp"`
|
||||||
MonsterMaxHP int `json:"monster_max_hp"`
|
MonsterMaxHP int `json:"monster_max_hp"`
|
||||||
MonsterSleep int `json:"monster_sleep"`
|
MonsterSleep int `json:"monster_sleep"`
|
||||||
@ -29,16 +30,59 @@ type Fight struct {
|
|||||||
Updated int64 `json:"updated"`
|
Updated int64 `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fight) Save() error {
|
// Global store
|
||||||
f.Updated = time.Now().Unix()
|
var store *nigiri.BaseStore[Fight]
|
||||||
return GetStore().UpdateWithRebuild(f.ID, f)
|
var db *nigiri.Collection
|
||||||
|
|
||||||
|
// Init sets up the Nigiri store and indices
|
||||||
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[Fight]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byUserID", nigiri.BuildIntGroupIndex(func(f *Fight) int {
|
||||||
|
return f.UserID
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byMonsterID", nigiri.BuildIntGroupIndex(func(f *Fight) int {
|
||||||
|
return f.MonsterID
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("activeFights", nigiri.BuildFilteredIntGroupIndex(
|
||||||
|
func(f *Fight) bool {
|
||||||
|
return !f.RanAway && !f.Victory
|
||||||
|
},
|
||||||
|
func(f *Fight) int {
|
||||||
|
return f.UserID
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByCreated", nigiri.BuildSortedListIndex(func(a, b *Fight) bool {
|
||||||
|
if a.Created != b.Created {
|
||||||
|
return a.Created > b.Created // DESC
|
||||||
|
}
|
||||||
|
return a.ID > b.ID // DESC
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByUpdated", nigiri.BuildSortedListIndex(func(a, b *Fight) bool {
|
||||||
|
if a.Updated != b.Updated {
|
||||||
|
return a.Updated > b.Updated // DESC
|
||||||
|
}
|
||||||
|
return a.ID > b.ID // DESC
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RebuildIndices()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fight) Delete() error {
|
// GetStore returns the fights store
|
||||||
GetStore().RemoveWithRebuild(f.ID)
|
func GetStore() *nigiri.BaseStore[Fight] {
|
||||||
return nil
|
if store == nil {
|
||||||
|
panic("fights store not initialized - call Initialize first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new Fight with sensible defaults
|
||||||
func New(userID, monsterID int) *Fight {
|
func New(userID, monsterID int) *Fight {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
return &Fight{
|
return &Fight{
|
||||||
@ -86,78 +130,39 @@ func (f *Fight) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FightStore with enhanced BaseStore
|
// CRUD operations
|
||||||
type FightStore struct {
|
func (f *Fight) Save() error {
|
||||||
*store.BaseStore[Fight]
|
f.Updated = time.Now().Unix()
|
||||||
}
|
if f.ID == 0 {
|
||||||
|
id, err := store.Create(f)
|
||||||
// Global store with singleton pattern
|
if err != nil {
|
||||||
var GetStore = store.NewSingleton(func() *FightStore {
|
return err
|
||||||
fs := &FightStore{BaseStore: store.NewBaseStore[Fight]()}
|
|
||||||
|
|
||||||
// Register indices
|
|
||||||
fs.RegisterIndex("byUserID", store.BuildIntGroupIndex(func(f *Fight) int {
|
|
||||||
return f.UserID
|
|
||||||
}))
|
|
||||||
|
|
||||||
fs.RegisterIndex("byMonsterID", store.BuildIntGroupIndex(func(f *Fight) int {
|
|
||||||
return f.MonsterID
|
|
||||||
}))
|
|
||||||
|
|
||||||
fs.RegisterIndex("activeFights", store.BuildFilteredIntGroupIndex(
|
|
||||||
func(f *Fight) bool {
|
|
||||||
return !f.RanAway && !f.Victory
|
|
||||||
},
|
|
||||||
func(f *Fight) int {
|
|
||||||
return f.UserID
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
fs.RegisterIndex("allByCreated", store.BuildSortedListIndex(func(a, b *Fight) bool {
|
|
||||||
if a.Created != b.Created {
|
|
||||||
return a.Created > b.Created // DESC
|
|
||||||
}
|
}
|
||||||
return a.ID > b.ID // DESC
|
f.ID = id
|
||||||
}))
|
return nil
|
||||||
|
}
|
||||||
fs.RegisterIndex("allByUpdated", store.BuildSortedListIndex(func(a, b *Fight) bool {
|
return store.Update(f.ID, f)
|
||||||
if a.Updated != b.Updated {
|
|
||||||
return a.Updated > b.Updated // DESC
|
|
||||||
}
|
|
||||||
return a.ID > b.ID // DESC
|
|
||||||
}))
|
|
||||||
|
|
||||||
return fs
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (fs *FightStore) AddFight(fight *Fight) error {
|
|
||||||
return fs.AddWithRebuild(fight.ID, fight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FightStore) RemoveFight(id int) {
|
func (f *Fight) Delete() error {
|
||||||
fs.RemoveWithRebuild(id)
|
store.Remove(f.ID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FightStore) UpdateFight(fight *Fight) error {
|
// Insert with ID assignment
|
||||||
return fs.UpdateWithRebuild(fight.ID, fight)
|
func (f *Fight) Insert() error {
|
||||||
|
f.Updated = time.Now().Unix()
|
||||||
|
id, err := store.Create(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.ID = id
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data persistence
|
// Query functions
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
fs := GetStore()
|
|
||||||
return fs.BaseStore.LoadData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
fs := GetStore()
|
|
||||||
return fs.BaseStore.SaveData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query functions using enhanced store
|
|
||||||
func Find(id int) (*Fight, error) {
|
func Find(id int) (*Fight, error) {
|
||||||
fs := GetStore()
|
fight, exists := store.Find(id)
|
||||||
fight, exists := fs.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("fight with ID %d not found", id)
|
return nil, fmt.Errorf("fight with ID %d not found", id)
|
||||||
}
|
}
|
||||||
@ -165,54 +170,38 @@ func Find(id int) (*Fight, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*Fight, error) {
|
func All() ([]*Fight, error) {
|
||||||
fs := GetStore()
|
return store.AllSorted("allByCreated"), nil
|
||||||
return fs.AllSorted("allByCreated"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByUserID(userID int) ([]*Fight, error) {
|
func ByUserID(userID int) ([]*Fight, error) {
|
||||||
fs := GetStore()
|
return store.GroupByIndex("byUserID", userID), nil
|
||||||
return fs.GroupByIndex("byUserID", userID), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByMonsterID(monsterID int) ([]*Fight, error) {
|
func ByMonsterID(monsterID int) ([]*Fight, error) {
|
||||||
fs := GetStore()
|
return store.GroupByIndex("byMonsterID", monsterID), nil
|
||||||
return fs.GroupByIndex("byMonsterID", monsterID), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ActiveByUserID(userID int) ([]*Fight, error) {
|
func ActiveByUserID(userID int) ([]*Fight, error) {
|
||||||
fs := GetStore()
|
return store.GroupByIndex("activeFights", userID), nil
|
||||||
return fs.GroupByIndex("activeFights", userID), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Active() ([]*Fight, error) {
|
func Active() ([]*Fight, error) {
|
||||||
fs := GetStore()
|
result := store.FilterByIndex("allByCreated", func(f *Fight) bool {
|
||||||
result := fs.FilterByIndex("allByCreated", func(f *Fight) bool {
|
|
||||||
return !f.RanAway && !f.Victory
|
return !f.RanAway && !f.Victory
|
||||||
})
|
})
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Recent(within time.Duration) ([]*Fight, error) {
|
func Recent(within time.Duration) ([]*Fight, error) {
|
||||||
fs := GetStore()
|
|
||||||
cutoff := time.Now().Add(-within).Unix()
|
cutoff := time.Now().Add(-within).Unix()
|
||||||
|
|
||||||
result := fs.FilterByIndex("allByCreated", func(f *Fight) bool {
|
result := store.FilterByIndex("allByCreated", func(f *Fight) bool {
|
||||||
return f.Created >= cutoff
|
return f.Created >= cutoff
|
||||||
})
|
})
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (f *Fight) Insert() error {
|
|
||||||
fs := GetStore()
|
|
||||||
if f.ID == 0 {
|
|
||||||
f.ID = fs.GetNextID()
|
|
||||||
}
|
|
||||||
f.Updated = time.Now().Unix()
|
|
||||||
return fs.AddFight(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
func (f *Fight) CreatedTime() time.Time {
|
func (f *Fight) CreatedTime() time.Time {
|
||||||
return time.Unix(f.Created, 0)
|
return time.Unix(f.Created, 0)
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package forum
|
package forum
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Forum represents a forum post or thread in the game
|
// Forum represents a forum post or thread in the game
|
||||||
@ -13,20 +14,47 @@ type Forum struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Posted int64 `json:"posted"`
|
Posted int64 `json:"posted"`
|
||||||
LastPost int64 `json:"last_post"`
|
LastPost int64 `json:"last_post"`
|
||||||
Author int `json:"author"`
|
Author int `json:"author" db:"index"`
|
||||||
Parent int `json:"parent"`
|
Parent int `json:"parent" db:"index"`
|
||||||
Replies int `json:"replies"`
|
Replies int `json:"replies"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title" db:"required"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content" db:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Forum) Save() error {
|
// Global store
|
||||||
return GetStore().UpdateWithRebuild(f.ID, f)
|
var store *nigiri.BaseStore[Forum]
|
||||||
|
var db *nigiri.Collection
|
||||||
|
|
||||||
|
// Init sets up the Nigiri store and indices
|
||||||
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[Forum]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byParent", nigiri.BuildIntGroupIndex(func(f *Forum) int {
|
||||||
|
return f.Parent
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byAuthor", nigiri.BuildIntGroupIndex(func(f *Forum) int {
|
||||||
|
return f.Author
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByLastPost", nigiri.BuildSortedListIndex(func(a, b *Forum) bool {
|
||||||
|
if a.LastPost != b.LastPost {
|
||||||
|
return a.LastPost > b.LastPost // DESC
|
||||||
|
}
|
||||||
|
return a.ID > b.ID // DESC
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RebuildIndices()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Forum) Delete() error {
|
// GetStore returns the forum store
|
||||||
GetStore().RemoveWithRebuild(f.ID)
|
func GetStore() *nigiri.BaseStore[Forum] {
|
||||||
return nil
|
if store == nil {
|
||||||
|
panic("forum store not initialized - call Initialize first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new Forum with sensible defaults
|
// Creates a new Forum with sensible defaults
|
||||||
@ -66,62 +94,37 @@ func (f *Forum) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForumStore with enhanced BaseStore
|
// CRUD operations
|
||||||
type ForumStore struct {
|
func (f *Forum) Save() error {
|
||||||
*store.BaseStore[Forum]
|
if f.ID == 0 {
|
||||||
}
|
id, err := store.Create(f)
|
||||||
|
if err != nil {
|
||||||
// Global store with singleton pattern
|
return err
|
||||||
var GetStore = store.NewSingleton(func() *ForumStore {
|
|
||||||
fs := &ForumStore{BaseStore: store.NewBaseStore[Forum]()}
|
|
||||||
|
|
||||||
// Register indices
|
|
||||||
fs.RegisterIndex("byParent", store.BuildIntGroupIndex(func(f *Forum) int {
|
|
||||||
return f.Parent
|
|
||||||
}))
|
|
||||||
|
|
||||||
fs.RegisterIndex("byAuthor", store.BuildIntGroupIndex(func(f *Forum) int {
|
|
||||||
return f.Author
|
|
||||||
}))
|
|
||||||
|
|
||||||
fs.RegisterIndex("allByLastPost", store.BuildSortedListIndex(func(a, b *Forum) bool {
|
|
||||||
if a.LastPost != b.LastPost {
|
|
||||||
return a.LastPost > b.LastPost // DESC
|
|
||||||
}
|
}
|
||||||
return a.ID > b.ID // DESC
|
f.ID = id
|
||||||
}))
|
return nil
|
||||||
|
}
|
||||||
return fs
|
return store.Update(f.ID, f)
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (fs *ForumStore) AddForum(forum *Forum) error {
|
|
||||||
return fs.AddWithRebuild(forum.ID, forum)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *ForumStore) RemoveForum(id int) {
|
func (f *Forum) Delete() error {
|
||||||
fs.RemoveWithRebuild(id)
|
store.Remove(f.ID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *ForumStore) UpdateForum(forum *Forum) error {
|
// Insert with ID assignment
|
||||||
return fs.UpdateWithRebuild(forum.ID, forum)
|
func (f *Forum) Insert() error {
|
||||||
|
id, err := store.Create(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.ID = id
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data persistence
|
// Query functions
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
fs := GetStore()
|
|
||||||
return fs.BaseStore.LoadData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
fs := GetStore()
|
|
||||||
return fs.BaseStore.SaveData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query functions using enhanced store
|
|
||||||
func Find(id int) (*Forum, error) {
|
func Find(id int) (*Forum, error) {
|
||||||
fs := GetStore()
|
forum, exists := store.Find(id)
|
||||||
forum, exists := fs.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("forum post with ID %d not found", id)
|
return nil, fmt.Errorf("forum post with ID %d not found", id)
|
||||||
}
|
}
|
||||||
@ -129,20 +132,17 @@ func Find(id int) (*Forum, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*Forum, error) {
|
func All() ([]*Forum, error) {
|
||||||
fs := GetStore()
|
return store.AllSorted("allByLastPost"), nil
|
||||||
return fs.AllSorted("allByLastPost"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Threads() ([]*Forum, error) {
|
func Threads() ([]*Forum, error) {
|
||||||
fs := GetStore()
|
return store.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
||||||
return fs.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
|
||||||
return f.Parent == 0
|
return f.Parent == 0
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByParent(parentID int) ([]*Forum, error) {
|
func ByParent(parentID int) ([]*Forum, error) {
|
||||||
fs := GetStore()
|
replies := store.GroupByIndex("byParent", parentID)
|
||||||
replies := fs.GroupByIndex("byParent", parentID)
|
|
||||||
|
|
||||||
// Sort replies chronologically (posted ASC, then ID ASC)
|
// Sort replies chronologically (posted ASC, then ID ASC)
|
||||||
if parentID > 0 && len(replies) > 1 {
|
if parentID > 0 && len(replies) > 1 {
|
||||||
@ -158,8 +158,7 @@ func ByParent(parentID int) ([]*Forum, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ByAuthor(authorID int) ([]*Forum, error) {
|
func ByAuthor(authorID int) ([]*Forum, error) {
|
||||||
fs := GetStore()
|
posts := store.GroupByIndex("byAuthor", authorID)
|
||||||
posts := fs.GroupByIndex("byAuthor", authorID)
|
|
||||||
|
|
||||||
// Sort by posted DESC, then ID DESC
|
// Sort by posted DESC, then ID DESC
|
||||||
sort.Slice(posts, func(i, j int) bool {
|
sort.Slice(posts, func(i, j int) bool {
|
||||||
@ -173,8 +172,7 @@ func ByAuthor(authorID int) ([]*Forum, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Recent(limit int) ([]*Forum, error) {
|
func Recent(limit int) ([]*Forum, error) {
|
||||||
fs := GetStore()
|
all := store.AllSorted("allByLastPost")
|
||||||
all := fs.AllSorted("allByLastPost")
|
|
||||||
if limit > len(all) {
|
if limit > len(all) {
|
||||||
limit = len(all)
|
limit = len(all)
|
||||||
}
|
}
|
||||||
@ -182,30 +180,19 @@ func Recent(limit int) ([]*Forum, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Search(term string) ([]*Forum, error) {
|
func Search(term string) ([]*Forum, error) {
|
||||||
fs := GetStore()
|
|
||||||
lowerTerm := strings.ToLower(term)
|
lowerTerm := strings.ToLower(term)
|
||||||
return fs.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
return store.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
||||||
return strings.Contains(strings.ToLower(f.Title), lowerTerm) ||
|
return strings.Contains(strings.ToLower(f.Title), lowerTerm) ||
|
||||||
strings.Contains(strings.ToLower(f.Content), lowerTerm)
|
strings.Contains(strings.ToLower(f.Content), lowerTerm)
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Since(since int64) ([]*Forum, error) {
|
func Since(since int64) ([]*Forum, error) {
|
||||||
fs := GetStore()
|
return store.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
||||||
return fs.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
|
||||||
return f.LastPost >= since
|
return f.LastPost >= since
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (f *Forum) Insert() error {
|
|
||||||
fs := GetStore()
|
|
||||||
if f.ID == 0 {
|
|
||||||
f.ID = fs.GetNextID()
|
|
||||||
}
|
|
||||||
return fs.AddForum(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
func (f *Forum) PostedTime() time.Time {
|
func (f *Forum) PostedTime() time.Time {
|
||||||
return time.Unix(f.Posted, 0)
|
return time.Unix(f.Posted, 0)
|
||||||
@ -324,3 +311,14 @@ func (f *Forum) GetThread() (*Forum, error) {
|
|||||||
}
|
}
|
||||||
return Find(f.Parent)
|
return Find(f.Parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (will be removed later)
|
||||||
|
func LoadData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,27 +1,55 @@
|
|||||||
package items
|
package items
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Item represents an item in the game
|
// Item represents an item in the game
|
||||||
type Item struct {
|
type Item struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Type int `json:"type"`
|
Type int `json:"type" db:"index"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" db:"required"`
|
||||||
Value int `json:"value"`
|
Value int `json:"value"`
|
||||||
Att int `json:"att"`
|
Att int `json:"att"`
|
||||||
Special string `json:"special"`
|
Special string `json:"special"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Item) Save() error {
|
// ItemType constants for item types
|
||||||
return GetStore().UpdateWithRebuild(i.ID, i)
|
const (
|
||||||
|
TypeWeapon = 1
|
||||||
|
TypeArmor = 2
|
||||||
|
TypeShield = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global store
|
||||||
|
var store *nigiri.BaseStore[Item]
|
||||||
|
var db *nigiri.Collection
|
||||||
|
|
||||||
|
// Init sets up the Nigiri store and indices
|
||||||
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[Item]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byType", nigiri.BuildIntGroupIndex(func(i *Item) int {
|
||||||
|
return i.Type
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByID", nigiri.BuildSortedListIndex(func(a, b *Item) bool {
|
||||||
|
return a.ID < b.ID
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RebuildIndices()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Item) Delete() error {
|
// GetStore returns the items store
|
||||||
GetStore().RemoveWithRebuild(i.ID)
|
func GetStore() *nigiri.BaseStore[Item] {
|
||||||
return nil
|
if store == nil {
|
||||||
|
panic("items store not initialized - call Initialize first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new Item with sensible defaults
|
// Creates a new Item with sensible defaults
|
||||||
@ -52,62 +80,37 @@ func (i *Item) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ItemType constants for item types
|
// CRUD operations
|
||||||
const (
|
func (i *Item) Save() error {
|
||||||
TypeWeapon = 1
|
if i.ID == 0 {
|
||||||
TypeArmor = 2
|
id, err := store.Create(i)
|
||||||
TypeShield = 3
|
if err != nil {
|
||||||
)
|
return err
|
||||||
|
}
|
||||||
// ItemStore with enhanced BaseStore
|
i.ID = id
|
||||||
type ItemStore struct {
|
return nil
|
||||||
*store.BaseStore[Item]
|
}
|
||||||
|
return store.Update(i.ID, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global store with singleton pattern
|
func (i *Item) Delete() error {
|
||||||
var GetStore = store.NewSingleton(func() *ItemStore {
|
store.Remove(i.ID)
|
||||||
is := &ItemStore{BaseStore: store.NewBaseStore[Item]()}
|
return nil
|
||||||
|
|
||||||
// Register indices
|
|
||||||
is.RegisterIndex("byType", store.BuildIntGroupIndex(func(i *Item) int {
|
|
||||||
return i.Type
|
|
||||||
}))
|
|
||||||
|
|
||||||
is.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Item) bool {
|
|
||||||
return a.ID < b.ID
|
|
||||||
}))
|
|
||||||
|
|
||||||
return is
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (is *ItemStore) AddItem(item *Item) error {
|
|
||||||
return is.AddWithRebuild(item.ID, item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (is *ItemStore) RemoveItem(id int) {
|
// Insert with ID assignment
|
||||||
is.RemoveWithRebuild(id)
|
func (i *Item) Insert() error {
|
||||||
|
id, err := store.Create(i)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.ID = id
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (is *ItemStore) UpdateItem(item *Item) error {
|
// Query functions
|
||||||
return is.UpdateWithRebuild(item.ID, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data persistence
|
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
is := GetStore()
|
|
||||||
return is.BaseStore.LoadData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
is := GetStore()
|
|
||||||
return is.BaseStore.SaveData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query functions using enhanced store
|
|
||||||
func Find(id int) (*Item, error) {
|
func Find(id int) (*Item, error) {
|
||||||
is := GetStore()
|
item, exists := store.Find(id)
|
||||||
item, exists := is.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("item with ID %d not found", id)
|
return nil, fmt.Errorf("item with ID %d not found", id)
|
||||||
}
|
}
|
||||||
@ -115,22 +118,11 @@ func Find(id int) (*Item, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*Item, error) {
|
func All() ([]*Item, error) {
|
||||||
is := GetStore()
|
return store.AllSorted("allByID"), nil
|
||||||
return is.AllSorted("allByID"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByType(itemType int) ([]*Item, error) {
|
func ByType(itemType int) ([]*Item, error) {
|
||||||
is := GetStore()
|
return store.GroupByIndex("byType", itemType), nil
|
||||||
return is.GroupByIndex("byType", itemType), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (i *Item) Insert() error {
|
|
||||||
is := GetStore()
|
|
||||||
if i.ID == 0 {
|
|
||||||
i.ID = is.GetNextID()
|
|
||||||
}
|
|
||||||
return is.AddItem(i)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
@ -166,3 +158,14 @@ func (i *Item) HasSpecial() bool {
|
|||||||
func (i *Item) IsEquippable() bool {
|
func (i *Item) IsEquippable() bool {
|
||||||
return i.Type == TypeWeapon || i.Type == TypeArmor || i.Type == TypeShield
|
return i.Type == TypeWeapon || i.Type == TypeArmor || i.Type == TypeShield
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (will be removed later)
|
||||||
|
func LoadData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,30 +1,65 @@
|
|||||||
package monsters
|
package monsters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Monster represents a monster in the game
|
// Monster represents a monster in the game
|
||||||
type Monster struct {
|
type Monster struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" db:"required"`
|
||||||
MaxHP int `json:"max_hp"`
|
MaxHP int `json:"max_hp"`
|
||||||
MaxDmg int `json:"max_dmg"`
|
MaxDmg int `json:"max_dmg"`
|
||||||
Armor int `json:"armor"`
|
Armor int `json:"armor"`
|
||||||
Level int `json:"level"`
|
Level int `json:"level" db:"index"`
|
||||||
MaxExp int `json:"max_exp"`
|
MaxExp int `json:"max_exp"`
|
||||||
MaxGold int `json:"max_gold"`
|
MaxGold int `json:"max_gold"`
|
||||||
Immune int `json:"immune"`
|
Immune int `json:"immune" db:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Monster) Save() error {
|
// Immunity constants
|
||||||
return GetStore().UpdateWithRebuild(m.ID, m)
|
const (
|
||||||
|
ImmuneNone = 0
|
||||||
|
ImmuneHurt = 1
|
||||||
|
ImmuneSleep = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global store
|
||||||
|
var store *nigiri.BaseStore[Monster]
|
||||||
|
var db *nigiri.Collection
|
||||||
|
|
||||||
|
// Init sets up the Nigiri store and indices
|
||||||
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[Monster]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byLevel", nigiri.BuildIntGroupIndex(func(m *Monster) int {
|
||||||
|
return m.Level
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byImmunity", nigiri.BuildIntGroupIndex(func(m *Monster) int {
|
||||||
|
return m.Immune
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByLevel", nigiri.BuildSortedListIndex(func(a, b *Monster) bool {
|
||||||
|
if a.Level == b.Level {
|
||||||
|
return a.ID < b.ID
|
||||||
|
}
|
||||||
|
return a.Level < b.Level
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RebuildIndices()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Monster) Delete() error {
|
// GetStore returns the monsters store
|
||||||
GetStore().RemoveWithRebuild(m.ID)
|
func GetStore() *nigiri.BaseStore[Monster] {
|
||||||
return nil
|
if store == nil {
|
||||||
|
panic("monsters store not initialized - call Initialize first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new Monster with sensible defaults
|
// Creates a new Monster with sensible defaults
|
||||||
@ -58,69 +93,37 @@ func (m *Monster) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Immunity constants
|
// CRUD operations
|
||||||
const (
|
func (m *Monster) Save() error {
|
||||||
ImmuneNone = 0
|
if m.ID == 0 {
|
||||||
ImmuneHurt = 1
|
id, err := store.Create(m)
|
||||||
ImmuneSleep = 2
|
if err != nil {
|
||||||
)
|
return err
|
||||||
|
|
||||||
// MonsterStore with enhanced BaseStore
|
|
||||||
type MonsterStore struct {
|
|
||||||
*store.BaseStore[Monster]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global store with singleton pattern
|
|
||||||
var GetStore = store.NewSingleton(func() *MonsterStore {
|
|
||||||
ms := &MonsterStore{BaseStore: store.NewBaseStore[Monster]()}
|
|
||||||
|
|
||||||
// Register indices
|
|
||||||
ms.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(m *Monster) int {
|
|
||||||
return m.Level
|
|
||||||
}))
|
|
||||||
|
|
||||||
ms.RegisterIndex("byImmunity", store.BuildIntGroupIndex(func(m *Monster) int {
|
|
||||||
return m.Immune
|
|
||||||
}))
|
|
||||||
|
|
||||||
ms.RegisterIndex("allByLevel", store.BuildSortedListIndex(func(a, b *Monster) bool {
|
|
||||||
if a.Level == b.Level {
|
|
||||||
return a.ID < b.ID
|
|
||||||
}
|
}
|
||||||
return a.Level < b.Level
|
m.ID = id
|
||||||
}))
|
return nil
|
||||||
|
}
|
||||||
return ms
|
return store.Update(m.ID, m)
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (ms *MonsterStore) AddMonster(monster *Monster) error {
|
|
||||||
return ms.AddWithRebuild(monster.ID, monster)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MonsterStore) RemoveMonster(id int) {
|
func (m *Monster) Delete() error {
|
||||||
ms.RemoveWithRebuild(id)
|
store.Remove(m.ID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MonsterStore) UpdateMonster(monster *Monster) error {
|
// Insert with ID assignment
|
||||||
return ms.UpdateWithRebuild(monster.ID, monster)
|
func (m *Monster) Insert() error {
|
||||||
|
id, err := store.Create(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.ID = id
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data persistence
|
// Query functions
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
ms := GetStore()
|
|
||||||
return ms.BaseStore.LoadData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
ms := GetStore()
|
|
||||||
return ms.BaseStore.SaveData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query functions using enhanced store
|
|
||||||
func Find(id int) (*Monster, error) {
|
func Find(id int) (*Monster, error) {
|
||||||
ms := GetStore()
|
monster, exists := store.Find(id)
|
||||||
monster, exists := ms.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("monster with ID %d not found", id)
|
return nil, fmt.Errorf("monster with ID %d not found", id)
|
||||||
}
|
}
|
||||||
@ -128,37 +131,24 @@ func Find(id int) (*Monster, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*Monster, error) {
|
func All() ([]*Monster, error) {
|
||||||
ms := GetStore()
|
return store.AllSorted("allByLevel"), nil
|
||||||
return ms.AllSorted("allByLevel"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByLevel(level int) ([]*Monster, error) {
|
func ByLevel(level int) ([]*Monster, error) {
|
||||||
ms := GetStore()
|
return store.GroupByIndex("byLevel", level), nil
|
||||||
return ms.GroupByIndex("byLevel", level), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) {
|
func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) {
|
||||||
ms := GetStore()
|
|
||||||
var result []*Monster
|
var result []*Monster
|
||||||
for level := minLevel; level <= maxLevel; level++ {
|
for level := minLevel; level <= maxLevel; level++ {
|
||||||
monsters := ms.GroupByIndex("byLevel", level)
|
monsters := store.GroupByIndex("byLevel", level)
|
||||||
result = append(result, monsters...)
|
result = append(result, monsters...)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByImmunity(immunityType int) ([]*Monster, error) {
|
func ByImmunity(immunityType int) ([]*Monster, error) {
|
||||||
ms := GetStore()
|
return store.GroupByIndex("byImmunity", immunityType), nil
|
||||||
return ms.GroupByIndex("byImmunity", immunityType), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (m *Monster) Insert() error {
|
|
||||||
ms := GetStore()
|
|
||||||
if m.ID == 0 {
|
|
||||||
m.ID = ms.GetNextID()
|
|
||||||
}
|
|
||||||
return ms.AddMonster(m)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
@ -207,3 +197,14 @@ func (m *Monster) GoldPerHP() float64 {
|
|||||||
}
|
}
|
||||||
return float64(m.MaxGold) / float64(m.MaxHP)
|
return float64(m.MaxGold) / float64(m.MaxHP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (will be removed later)
|
||||||
|
func LoadData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,27 +1,51 @@
|
|||||||
package news
|
package news
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// News represents a news post in the game
|
// News represents a news post in the game
|
||||||
type News struct {
|
type News struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Author int `json:"author"`
|
Author int `json:"author" db:"index"`
|
||||||
Posted int64 `json:"posted"`
|
Posted int64 `json:"posted"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content" db:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *News) Save() error {
|
// Global store
|
||||||
return GetStore().UpdateWithRebuild(n.ID, n)
|
var store *nigiri.BaseStore[News]
|
||||||
|
var db *nigiri.Collection
|
||||||
|
|
||||||
|
// Init sets up the Nigiri store and indices
|
||||||
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[News]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byAuthor", nigiri.BuildIntGroupIndex(func(n *News) int {
|
||||||
|
return n.Author
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByPosted", nigiri.BuildSortedListIndex(func(a, b *News) bool {
|
||||||
|
if a.Posted != b.Posted {
|
||||||
|
return a.Posted > b.Posted // DESC
|
||||||
|
}
|
||||||
|
return a.ID > b.ID // DESC
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RebuildIndices()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *News) Delete() error {
|
// GetStore returns the news store
|
||||||
GetStore().RemoveWithRebuild(n.ID)
|
func GetStore() *nigiri.BaseStore[News] {
|
||||||
return nil
|
if store == nil {
|
||||||
|
panic("news store not initialized - call Init first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new News with sensible defaults
|
// Creates a new News with sensible defaults
|
||||||
@ -44,58 +68,37 @@ func (n *News) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewsStore with enhanced BaseStore
|
// CRUD operations
|
||||||
type NewsStore struct {
|
func (n *News) Save() error {
|
||||||
*store.BaseStore[News]
|
if n.ID == 0 {
|
||||||
}
|
id, err := store.Create(n)
|
||||||
|
if err != nil {
|
||||||
// Global store with singleton pattern
|
return err
|
||||||
var GetStore = store.NewSingleton(func() *NewsStore {
|
|
||||||
ns := &NewsStore{BaseStore: store.NewBaseStore[News]()}
|
|
||||||
|
|
||||||
// Register indices
|
|
||||||
ns.RegisterIndex("byAuthor", store.BuildIntGroupIndex(func(n *News) int {
|
|
||||||
return n.Author
|
|
||||||
}))
|
|
||||||
|
|
||||||
ns.RegisterIndex("allByPosted", store.BuildSortedListIndex(func(a, b *News) bool {
|
|
||||||
if a.Posted != b.Posted {
|
|
||||||
return a.Posted > b.Posted // DESC
|
|
||||||
}
|
}
|
||||||
return a.ID > b.ID // DESC
|
n.ID = id
|
||||||
}))
|
return nil
|
||||||
|
}
|
||||||
return ns
|
return store.Update(n.ID, n)
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (ns *NewsStore) AddNews(news *News) error {
|
|
||||||
return ns.AddWithRebuild(news.ID, news)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *NewsStore) RemoveNews(id int) {
|
func (n *News) Delete() error {
|
||||||
ns.RemoveWithRebuild(id)
|
store.Remove(n.ID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *NewsStore) UpdateNews(news *News) error {
|
// Insert with ID assignment
|
||||||
return ns.UpdateWithRebuild(news.ID, news)
|
func (n *News) Insert() error {
|
||||||
|
id, err := store.Create(n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n.ID = id
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data persistence
|
// Query functions
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
ns := GetStore()
|
|
||||||
return ns.BaseStore.LoadData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
ns := GetStore()
|
|
||||||
return ns.BaseStore.SaveData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query functions using enhanced store
|
|
||||||
func Find(id int) (*News, error) {
|
func Find(id int) (*News, error) {
|
||||||
ns := GetStore()
|
news, exists := store.Find(id)
|
||||||
news, exists := ns.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("news with ID %d not found", id)
|
return nil, fmt.Errorf("news with ID %d not found", id)
|
||||||
}
|
}
|
||||||
@ -103,18 +106,15 @@ func Find(id int) (*News, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*News, error) {
|
func All() ([]*News, error) {
|
||||||
ns := GetStore()
|
return store.AllSorted("allByPosted"), nil
|
||||||
return ns.AllSorted("allByPosted"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByAuthor(authorID int) ([]*News, error) {
|
func ByAuthor(authorID int) ([]*News, error) {
|
||||||
ns := GetStore()
|
return store.GroupByIndex("byAuthor", authorID), nil
|
||||||
return ns.GroupByIndex("byAuthor", authorID), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Recent(limit int) ([]*News, error) {
|
func Recent(limit int) ([]*News, error) {
|
||||||
ns := GetStore()
|
all := store.AllSorted("allByPosted")
|
||||||
all := ns.AllSorted("allByPosted")
|
|
||||||
if limit > len(all) {
|
if limit > len(all) {
|
||||||
limit = len(all)
|
limit = len(all)
|
||||||
}
|
}
|
||||||
@ -122,36 +122,24 @@ func Recent(limit int) ([]*News, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Since(since int64) ([]*News, error) {
|
func Since(since int64) ([]*News, error) {
|
||||||
ns := GetStore()
|
return store.FilterByIndex("allByPosted", func(n *News) bool {
|
||||||
return ns.FilterByIndex("allByPosted", func(n *News) bool {
|
|
||||||
return n.Posted >= since
|
return n.Posted >= since
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Between(start, end int64) ([]*News, error) {
|
func Between(start, end int64) ([]*News, error) {
|
||||||
ns := GetStore()
|
return store.FilterByIndex("allByPosted", func(n *News) bool {
|
||||||
return ns.FilterByIndex("allByPosted", func(n *News) bool {
|
|
||||||
return n.Posted >= start && n.Posted <= end
|
return n.Posted >= start && n.Posted <= end
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Search(term string) ([]*News, error) {
|
func Search(term string) ([]*News, error) {
|
||||||
ns := GetStore()
|
|
||||||
lowerTerm := strings.ToLower(term)
|
lowerTerm := strings.ToLower(term)
|
||||||
return ns.FilterByIndex("allByPosted", func(n *News) bool {
|
return store.FilterByIndex("allByPosted", func(n *News) bool {
|
||||||
return strings.Contains(strings.ToLower(n.Content), lowerTerm)
|
return strings.Contains(strings.ToLower(n.Content), lowerTerm)
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (n *News) Insert() error {
|
|
||||||
ns := GetStore()
|
|
||||||
if n.ID == 0 {
|
|
||||||
n.ID = ns.GetNextID()
|
|
||||||
}
|
|
||||||
return ns.AddNews(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
func (n *News) PostedTime() time.Time {
|
func (n *News) PostedTime() time.Time {
|
||||||
return time.Unix(n.Posted, 0)
|
return time.Unix(n.Posted, 0)
|
||||||
@ -227,3 +215,14 @@ func (n *News) Contains(term string) bool {
|
|||||||
func (n *News) IsEmpty() bool {
|
func (n *News) IsEmpty() bool {
|
||||||
return strings.TrimSpace(n.Content) == ""
|
return strings.TrimSpace(n.Content) == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (will be removed later)
|
||||||
|
func LoadData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,27 +1,71 @@
|
|||||||
package spells
|
package spells
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Spell represents a spell in the game
|
// Spell represents a spell in the game
|
||||||
type Spell struct {
|
type Spell struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" db:"required,unique"`
|
||||||
MP int `json:"mp"`
|
MP int `json:"mp" db:"index"`
|
||||||
Attribute int `json:"attribute"`
|
Attribute int `json:"attribute"`
|
||||||
Type int `json:"type"`
|
Type int `json:"type" db:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spell) Save() error {
|
// SpellType constants for spell types
|
||||||
return GetStore().UpdateWithRebuild(s.ID, s)
|
const (
|
||||||
|
TypeHealing = 1
|
||||||
|
TypeHurt = 2
|
||||||
|
TypeSleep = 3
|
||||||
|
TypeAttackBoost = 4
|
||||||
|
TypeDefenseBoost = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global store
|
||||||
|
var store *nigiri.BaseStore[Spell]
|
||||||
|
var db *nigiri.Collection
|
||||||
|
|
||||||
|
// Init sets up the Nigiri store and indices
|
||||||
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[Spell]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byType", nigiri.BuildIntGroupIndex(func(s *Spell) int {
|
||||||
|
return s.Type
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byName", nigiri.BuildCaseInsensitiveLookupIndex(func(s *Spell) string {
|
||||||
|
return s.Name
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byMP", nigiri.BuildIntGroupIndex(func(s *Spell) int {
|
||||||
|
return s.MP
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByTypeMP", nigiri.BuildSortedListIndex(func(a, b *Spell) bool {
|
||||||
|
if a.Type != b.Type {
|
||||||
|
return a.Type < b.Type
|
||||||
|
}
|
||||||
|
if a.MP != b.MP {
|
||||||
|
return a.MP < b.MP
|
||||||
|
}
|
||||||
|
return a.ID < b.ID
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RebuildIndices()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spell) Delete() error {
|
// GetStore returns the spells store
|
||||||
GetStore().RemoveWithRebuild(s.ID)
|
func GetStore() *nigiri.BaseStore[Spell] {
|
||||||
return nil
|
if store == nil {
|
||||||
|
panic("spells store not initialized - call Initialize first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new Spell with sensible defaults
|
// Creates a new Spell with sensible defaults
|
||||||
@ -51,78 +95,37 @@ func (s *Spell) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpellType constants for spell types
|
// CRUD operations
|
||||||
const (
|
func (s *Spell) Save() error {
|
||||||
TypeHealing = 1
|
if s.ID == 0 {
|
||||||
TypeHurt = 2
|
id, err := store.Create(s)
|
||||||
TypeSleep = 3
|
if err != nil {
|
||||||
TypeAttackBoost = 4
|
return err
|
||||||
TypeDefenseBoost = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
// SpellStore with enhanced BaseStore
|
|
||||||
type SpellStore struct {
|
|
||||||
*store.BaseStore[Spell]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global store with singleton pattern
|
|
||||||
var GetStore = store.NewSingleton(func() *SpellStore {
|
|
||||||
ss := &SpellStore{BaseStore: store.NewBaseStore[Spell]()}
|
|
||||||
|
|
||||||
// Register indices
|
|
||||||
ss.RegisterIndex("byType", store.BuildIntGroupIndex(func(s *Spell) int {
|
|
||||||
return s.Type
|
|
||||||
}))
|
|
||||||
|
|
||||||
ss.RegisterIndex("byName", store.BuildCaseInsensitiveLookupIndex(func(s *Spell) string {
|
|
||||||
return s.Name
|
|
||||||
}))
|
|
||||||
|
|
||||||
ss.RegisterIndex("byMP", store.BuildIntGroupIndex(func(s *Spell) int {
|
|
||||||
return s.MP
|
|
||||||
}))
|
|
||||||
|
|
||||||
ss.RegisterIndex("allByTypeMP", store.BuildSortedListIndex(func(a, b *Spell) bool {
|
|
||||||
if a.Type != b.Type {
|
|
||||||
return a.Type < b.Type
|
|
||||||
}
|
}
|
||||||
if a.MP != b.MP {
|
s.ID = id
|
||||||
return a.MP < b.MP
|
return nil
|
||||||
}
|
}
|
||||||
return a.ID < b.ID
|
return store.Update(s.ID, s)
|
||||||
}))
|
|
||||||
|
|
||||||
return ss
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (ss *SpellStore) AddSpell(spell *Spell) error {
|
|
||||||
return ss.AddWithRebuild(spell.ID, spell)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SpellStore) RemoveSpell(id int) {
|
func (s *Spell) Delete() error {
|
||||||
ss.RemoveWithRebuild(id)
|
store.Remove(s.ID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SpellStore) UpdateSpell(spell *Spell) error {
|
// Insert with ID assignment
|
||||||
return ss.UpdateWithRebuild(spell.ID, spell)
|
func (s *Spell) Insert() error {
|
||||||
|
id, err := store.Create(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.ID = id
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data persistence
|
// Query functions
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
ss := GetStore()
|
|
||||||
return ss.BaseStore.LoadData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
ss := GetStore()
|
|
||||||
return ss.BaseStore.SaveData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query functions using enhanced store
|
|
||||||
func Find(id int) (*Spell, error) {
|
func Find(id int) (*Spell, error) {
|
||||||
ss := GetStore()
|
spell, exists := store.Find(id)
|
||||||
spell, exists := ss.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("spell with ID %d not found", id)
|
return nil, fmt.Errorf("spell with ID %d not found", id)
|
||||||
}
|
}
|
||||||
@ -130,47 +133,33 @@ func Find(id int) (*Spell, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*Spell, error) {
|
func All() ([]*Spell, error) {
|
||||||
ss := GetStore()
|
return store.AllSorted("allByTypeMP"), nil
|
||||||
return ss.AllSorted("allByTypeMP"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByType(spellType int) ([]*Spell, error) {
|
func ByType(spellType int) ([]*Spell, error) {
|
||||||
ss := GetStore()
|
return store.GroupByIndex("byType", spellType), nil
|
||||||
return ss.GroupByIndex("byType", spellType), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByMaxMP(maxMP int) ([]*Spell, error) {
|
func ByMaxMP(maxMP int) ([]*Spell, error) {
|
||||||
ss := GetStore()
|
return store.FilterByIndex("allByTypeMP", func(s *Spell) bool {
|
||||||
return ss.FilterByIndex("allByTypeMP", func(s *Spell) bool {
|
|
||||||
return s.MP <= maxMP
|
return s.MP <= maxMP
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) {
|
func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) {
|
||||||
ss := GetStore()
|
return store.FilterByIndex("allByTypeMP", func(s *Spell) bool {
|
||||||
return ss.FilterByIndex("allByTypeMP", func(s *Spell) bool {
|
|
||||||
return s.Type == spellType && s.MP <= maxMP
|
return s.Type == spellType && s.MP <= maxMP
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByName(name string) (*Spell, error) {
|
func ByName(name string) (*Spell, error) {
|
||||||
ss := GetStore()
|
spell, exists := store.LookupByIndex("byName", strings.ToLower(name))
|
||||||
spell, exists := ss.LookupByIndex("byName", strings.ToLower(name))
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("spell with name '%s' not found", name)
|
return nil, fmt.Errorf("spell with name '%s' not found", name)
|
||||||
}
|
}
|
||||||
return spell, nil
|
return spell, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (s *Spell) Insert() error {
|
|
||||||
ss := GetStore()
|
|
||||||
if s.ID == 0 {
|
|
||||||
s.ID = ss.GetNextID()
|
|
||||||
}
|
|
||||||
return ss.AddSpell(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
func (s *Spell) IsHealing() bool {
|
func (s *Spell) IsHealing() bool {
|
||||||
return s.Type == TypeHealing
|
return s.Type == TypeHealing
|
||||||
@ -227,3 +216,14 @@ func (s *Spell) IsOffensive() bool {
|
|||||||
func (s *Spell) IsSupport() bool {
|
func (s *Spell) IsSupport() bool {
|
||||||
return s.Type == TypeHealing || s.Type == TypeAttackBoost || s.Type == TypeDefenseBoost
|
return s.Type == TypeHealing || s.Type == TypeAttackBoost || s.Type == TypeDefenseBoost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (will be removed later)
|
||||||
|
func LoadData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package towns
|
package towns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
@ -10,12 +9,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dk/internal/helpers"
|
"dk/internal/helpers"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Town represents a town in the game
|
// Town represents a town in the game
|
||||||
type Town struct {
|
type Town struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" db:"required,unique"`
|
||||||
X int `json:"x"`
|
X int `json:"x"`
|
||||||
Y int `json:"y"`
|
Y int `json:"y"`
|
||||||
InnCost int `json:"inn_cost"`
|
InnCost int `json:"inn_cost"`
|
||||||
@ -24,13 +25,50 @@ type Town struct {
|
|||||||
ShopList string `json:"shop_list"`
|
ShopList string `json:"shop_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Town) Save() error {
|
// Global store
|
||||||
return GetStore().UpdateWithRebuild(t.ID, t)
|
var store *nigiri.BaseStore[Town]
|
||||||
|
var db *nigiri.Collection
|
||||||
|
|
||||||
|
// coordsKey creates a key for coordinate-based lookup
|
||||||
|
func coordsKey(x, y int) string {
|
||||||
|
return strconv.Itoa(x) + "," + strconv.Itoa(y)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Town) Delete() error {
|
// Init sets up the Nigiri store and indices
|
||||||
GetStore().RemoveWithRebuild(t.ID)
|
func Init(collection *nigiri.Collection) {
|
||||||
return nil
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[Town]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byName", nigiri.BuildCaseInsensitiveLookupIndex(func(t *Town) string {
|
||||||
|
return t.Name
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byCoords", nigiri.BuildStringLookupIndex(func(t *Town) string {
|
||||||
|
return coordsKey(t.X, t.Y)
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byInnCost", nigiri.BuildIntGroupIndex(func(t *Town) int {
|
||||||
|
return t.InnCost
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byTPCost", nigiri.BuildIntGroupIndex(func(t *Town) int {
|
||||||
|
return t.TPCost
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByID", nigiri.BuildSortedListIndex(func(a, b *Town) bool {
|
||||||
|
return a.ID < b.ID
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RebuildIndices()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStore returns the towns store
|
||||||
|
func GetStore() *nigiri.BaseStore[Town] {
|
||||||
|
if store == nil {
|
||||||
|
panic("towns store not initialized - call Initialize first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new Town with sensible defaults
|
// Creates a new Town with sensible defaults
|
||||||
@ -63,72 +101,37 @@ func (t *Town) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// coordsKey creates a key for coordinate-based lookup
|
// CRUD operations
|
||||||
func coordsKey(x, y int) string {
|
func (t *Town) Save() error {
|
||||||
return strconv.Itoa(x) + "," + strconv.Itoa(y)
|
if t.ID == 0 {
|
||||||
|
id, err := store.Create(t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.ID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return store.Update(t.ID, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TownStore with enhanced BaseStore
|
func (t *Town) Delete() error {
|
||||||
type TownStore struct {
|
store.Remove(t.ID)
|
||||||
*store.BaseStore[Town]
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global store with singleton pattern
|
// Insert with ID assignment
|
||||||
var GetStore = store.NewSingleton(func() *TownStore {
|
func (t *Town) Insert() error {
|
||||||
ts := &TownStore{BaseStore: store.NewBaseStore[Town]()}
|
id, err := store.Create(t)
|
||||||
|
if err != nil {
|
||||||
// Register indices
|
return err
|
||||||
ts.RegisterIndex("byName", store.BuildCaseInsensitiveLookupIndex(func(t *Town) string {
|
}
|
||||||
return t.Name
|
t.ID = id
|
||||||
}))
|
return nil
|
||||||
|
|
||||||
ts.RegisterIndex("byCoords", store.BuildStringLookupIndex(func(t *Town) string {
|
|
||||||
return coordsKey(t.X, t.Y)
|
|
||||||
}))
|
|
||||||
|
|
||||||
ts.RegisterIndex("byInnCost", store.BuildIntGroupIndex(func(t *Town) int {
|
|
||||||
return t.InnCost
|
|
||||||
}))
|
|
||||||
|
|
||||||
ts.RegisterIndex("byTPCost", store.BuildIntGroupIndex(func(t *Town) int {
|
|
||||||
return t.TPCost
|
|
||||||
}))
|
|
||||||
|
|
||||||
ts.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Town) bool {
|
|
||||||
return a.ID < b.ID
|
|
||||||
}))
|
|
||||||
|
|
||||||
return ts
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (ts *TownStore) AddTown(town *Town) error {
|
|
||||||
return ts.AddWithRebuild(town.ID, town)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TownStore) RemoveTown(id int) {
|
// Query functions
|
||||||
ts.RemoveWithRebuild(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TownStore) UpdateTown(town *Town) error {
|
|
||||||
return ts.UpdateWithRebuild(town.ID, town)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data persistence
|
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
ts := GetStore()
|
|
||||||
return ts.BaseStore.LoadData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
ts := GetStore()
|
|
||||||
return ts.BaseStore.SaveData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query functions using enhanced store
|
|
||||||
func Find(id int) (*Town, error) {
|
func Find(id int) (*Town, error) {
|
||||||
ts := GetStore()
|
town, exists := store.Find(id)
|
||||||
town, exists := ts.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("town with ID %d not found", id)
|
return nil, fmt.Errorf("town with ID %d not found", id)
|
||||||
}
|
}
|
||||||
@ -136,13 +139,11 @@ func Find(id int) (*Town, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*Town, error) {
|
func All() ([]*Town, error) {
|
||||||
ts := GetStore()
|
return store.AllSorted("allByID"), nil
|
||||||
return ts.AllSorted("allByID"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByName(name string) (*Town, error) {
|
func ByName(name string) (*Town, error) {
|
||||||
ts := GetStore()
|
town, exists := store.LookupByIndex("byName", strings.ToLower(name))
|
||||||
town, exists := ts.LookupByIndex("byName", strings.ToLower(name))
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("town with name '%s' not found", name)
|
return nil, fmt.Errorf("town with name '%s' not found", name)
|
||||||
}
|
}
|
||||||
@ -150,22 +151,19 @@ func ByName(name string) (*Town, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ByMaxInnCost(maxCost int) ([]*Town, error) {
|
func ByMaxInnCost(maxCost int) ([]*Town, error) {
|
||||||
ts := GetStore()
|
return store.FilterByIndex("allByID", func(t *Town) bool {
|
||||||
return ts.FilterByIndex("allByID", func(t *Town) bool {
|
|
||||||
return t.InnCost <= maxCost
|
return t.InnCost <= maxCost
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByMaxTPCost(maxCost int) ([]*Town, error) {
|
func ByMaxTPCost(maxCost int) ([]*Town, error) {
|
||||||
ts := GetStore()
|
return store.FilterByIndex("allByID", func(t *Town) bool {
|
||||||
return ts.FilterByIndex("allByID", func(t *Town) bool {
|
|
||||||
return t.TPCost <= maxCost
|
return t.TPCost <= maxCost
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByCoords(x, y int) (*Town, error) {
|
func ByCoords(x, y int) (*Town, error) {
|
||||||
ts := GetStore()
|
town, exists := store.LookupByIndex("byCoords", coordsKey(x, y))
|
||||||
town, exists := ts.LookupByIndex("byCoords", coordsKey(x, y))
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, nil // Return nil if not found (like original)
|
return nil, nil // Return nil if not found (like original)
|
||||||
}
|
}
|
||||||
@ -173,16 +171,14 @@ func ByCoords(x, y int) (*Town, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ExistsAt(x, y int) bool {
|
func ExistsAt(x, y int) bool {
|
||||||
ts := GetStore()
|
_, exists := store.LookupByIndex("byCoords", coordsKey(x, y))
|
||||||
_, exists := ts.LookupByIndex("byCoords", coordsKey(x, y))
|
|
||||||
return exists
|
return exists
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
||||||
ts := GetStore()
|
|
||||||
maxDistance2 := float64(maxDistance * maxDistance)
|
maxDistance2 := float64(maxDistance * maxDistance)
|
||||||
|
|
||||||
result := ts.FilterByIndex("allByID", func(t *Town) bool {
|
result := store.FilterByIndex("allByID", func(t *Town) bool {
|
||||||
return t.DistanceFromSquared(fromX, fromY) <= maxDistance2
|
return t.DistanceFromSquared(fromX, fromY) <= maxDistance2
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -199,15 +195,6 @@ func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (t *Town) Insert() error {
|
|
||||||
ts := GetStore()
|
|
||||||
if t.ID == 0 {
|
|
||||||
t.ID = ts.GetNextID()
|
|
||||||
}
|
|
||||||
return ts.AddTown(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
func (t *Town) GetShopItems() []int {
|
func (t *Town) GetShopItems() []int {
|
||||||
return helpers.StringToInts(t.ShopList)
|
return helpers.StringToInts(t.ShopList)
|
||||||
@ -259,3 +246,14 @@ func (t *Town) SetPosition(x, y int) {
|
|||||||
t.X = x
|
t.X = x
|
||||||
t.Y = y
|
t.Y = y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (will be removed later)
|
||||||
|
func LoadData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveData(dataPath string) error {
|
||||||
|
// No longer needed - Nigiri handles this
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package users
|
package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/helpers/exp"
|
|
||||||
"dk/internal/store"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
@ -10,14 +8,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dk/internal/helpers"
|
"dk/internal/helpers"
|
||||||
|
|
||||||
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User represents a user in the game
|
// User represents a user in the game
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username" db:"required,unique"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password" db:"required"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email" db:"required,unique"`
|
||||||
Verified int `json:"verified"`
|
Verified int `json:"verified"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Registered int64 `json:"registered"`
|
Registered int64 `json:"registered"`
|
||||||
@ -34,7 +34,7 @@ type User struct {
|
|||||||
MaxHP int `json:"max_hp"`
|
MaxHP int `json:"max_hp"`
|
||||||
MaxMP int `json:"max_mp"`
|
MaxMP int `json:"max_mp"`
|
||||||
MaxTP int `json:"max_tp"`
|
MaxTP int `json:"max_tp"`
|
||||||
Level int `json:"level"`
|
Level int `json:"level" db:"index"`
|
||||||
Gold int `json:"gold"`
|
Gold int `json:"gold"`
|
||||||
Exp int `json:"exp"`
|
Exp int `json:"exp"`
|
||||||
GoldBonus int `json:"gold_bonus"`
|
GoldBonus int `json:"gold_bonus"`
|
||||||
@ -59,15 +59,57 @@ type User struct {
|
|||||||
Towns string `json:"towns"`
|
Towns string `json:"towns"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) Save() error {
|
// Global store
|
||||||
return GetStore().UpdateWithRebuild(u.ID, u)
|
var store *nigiri.BaseStore[User]
|
||||||
|
var db *nigiri.Collection
|
||||||
|
|
||||||
|
// Init sets up the Nigiri store and indices
|
||||||
|
func Init(collection *nigiri.Collection) {
|
||||||
|
db = collection
|
||||||
|
store = nigiri.NewBaseStore[User]()
|
||||||
|
|
||||||
|
// Register custom indices
|
||||||
|
store.RegisterIndex("byUsername", nigiri.BuildCaseInsensitiveLookupIndex(func(u *User) string {
|
||||||
|
return u.Username
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byEmail", nigiri.BuildStringLookupIndex(func(u *User) string {
|
||||||
|
return u.Email
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("byLevel", nigiri.BuildIntGroupIndex(func(u *User) int {
|
||||||
|
return u.Level
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByRegistered", nigiri.BuildSortedListIndex(func(a, b *User) bool {
|
||||||
|
if a.Registered != b.Registered {
|
||||||
|
return a.Registered > b.Registered // DESC
|
||||||
|
}
|
||||||
|
return a.ID > b.ID // DESC
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RegisterIndex("allByLevelExp", nigiri.BuildSortedListIndex(func(a, b *User) bool {
|
||||||
|
if a.Level != b.Level {
|
||||||
|
return a.Level > b.Level // Level DESC
|
||||||
|
}
|
||||||
|
if a.Exp != b.Exp {
|
||||||
|
return a.Exp > b.Exp // Exp DESC
|
||||||
|
}
|
||||||
|
return a.ID < b.ID // ID ASC
|
||||||
|
}))
|
||||||
|
|
||||||
|
store.RebuildIndices()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) Delete() error {
|
// GetStore returns the users store
|
||||||
GetStore().RemoveWithRebuild(u.ID)
|
func GetStore() *nigiri.BaseStore[User] {
|
||||||
return nil
|
if store == nil {
|
||||||
|
panic("users store not initialized - call Initialize first")
|
||||||
|
}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new User with sensible defaults
|
||||||
func New() *User {
|
func New() *User {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
return &User{
|
return &User{
|
||||||
@ -125,90 +167,57 @@ func (u *User) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserStore with enhanced BaseStore
|
// CRUD operations
|
||||||
type UserStore struct {
|
func (u *User) Save() error {
|
||||||
*store.BaseStore[User]
|
if u.ID == 0 {
|
||||||
}
|
id, err := store.Create(u)
|
||||||
|
if err != nil {
|
||||||
// Global store with singleton pattern
|
return err
|
||||||
var GetStore = store.NewSingleton(func() *UserStore {
|
|
||||||
us := &UserStore{BaseStore: store.NewBaseStore[User]()}
|
|
||||||
|
|
||||||
// Register indices
|
|
||||||
us.RegisterIndex("byUsername", store.BuildCaseInsensitiveLookupIndex(func(u *User) string {
|
|
||||||
return u.Username
|
|
||||||
}))
|
|
||||||
|
|
||||||
us.RegisterIndex("byEmail", store.BuildStringLookupIndex(func(u *User) string {
|
|
||||||
return u.Email
|
|
||||||
}))
|
|
||||||
|
|
||||||
us.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(u *User) int {
|
|
||||||
return u.Level
|
|
||||||
}))
|
|
||||||
|
|
||||||
us.RegisterIndex("allByRegistered", store.BuildSortedListIndex(func(a, b *User) bool {
|
|
||||||
if a.Registered != b.Registered {
|
|
||||||
return a.Registered > b.Registered // DESC
|
|
||||||
}
|
}
|
||||||
return a.ID > b.ID // DESC
|
u.ID = id
|
||||||
}))
|
return nil
|
||||||
|
}
|
||||||
us.RegisterIndex("allByLevelExp", store.BuildSortedListIndex(func(a, b *User) bool {
|
return store.Update(u.ID, u)
|
||||||
if a.Level != b.Level {
|
|
||||||
return a.Level > b.Level // Level DESC
|
|
||||||
}
|
|
||||||
if a.Exp != b.Exp {
|
|
||||||
return a.Exp > b.Exp // Exp DESC
|
|
||||||
}
|
|
||||||
return a.ID < b.ID // ID ASC
|
|
||||||
}))
|
|
||||||
|
|
||||||
return us
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enhanced CRUD operations
|
|
||||||
func (us *UserStore) AddUser(user *User) error {
|
|
||||||
return us.AddWithRebuild(user.ID, user)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (us *UserStore) RemoveUser(id int) {
|
func (u *User) Delete() error {
|
||||||
us.RemoveWithRebuild(id)
|
store.Remove(u.ID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (us *UserStore) UpdateUser(user *User) error {
|
// Insert with ID assignment
|
||||||
return us.UpdateWithRebuild(user.ID, user)
|
func (u *User) Insert() error {
|
||||||
|
id, err := store.Create(u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.ID = id
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data persistence
|
// Query functions
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
us := GetStore()
|
|
||||||
return us.BaseStore.LoadData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
us := GetStore()
|
|
||||||
return us.BaseStore.SaveData(dataPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query functions using enhanced store
|
|
||||||
func Find(id int) (*User, error) {
|
func Find(id int) (*User, error) {
|
||||||
us := GetStore()
|
user, exists := store.Find(id)
|
||||||
user, exists := us.Find(id)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("user with ID %d not found", id)
|
return nil, fmt.Errorf("user with ID %d not found", id)
|
||||||
}
|
}
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetByID(id int) *User {
|
||||||
|
user, exists := store.Find(id)
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
func All() ([]*User, error) {
|
func All() ([]*User, error) {
|
||||||
us := GetStore()
|
return store.AllSorted("allByRegistered"), nil
|
||||||
return us.AllSorted("allByRegistered"), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByUsername(username string) (*User, error) {
|
func ByUsername(username string) (*User, error) {
|
||||||
us := GetStore()
|
user, exists := store.LookupByIndex("byUsername", strings.ToLower(username))
|
||||||
user, exists := us.LookupByIndex("byUsername", strings.ToLower(username))
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("user with username '%s' not found", username)
|
return nil, fmt.Errorf("user with username '%s' not found", username)
|
||||||
}
|
}
|
||||||
@ -216,8 +225,7 @@ func ByUsername(username string) (*User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ByEmail(email string) (*User, error) {
|
func ByEmail(email string) (*User, error) {
|
||||||
us := GetStore()
|
user, exists := store.LookupByIndex("Email_idx", email)
|
||||||
user, exists := us.LookupByIndex("byEmail", email)
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("user with email '%s' not found", email)
|
return nil, fmt.Errorf("user with email '%s' not found", email)
|
||||||
}
|
}
|
||||||
@ -225,15 +233,13 @@ func ByEmail(email string) (*User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ByLevel(level int) ([]*User, error) {
|
func ByLevel(level int) ([]*User, error) {
|
||||||
us := GetStore()
|
return store.GroupByIndex("level_idx", level), nil
|
||||||
return us.GroupByIndex("byLevel", level), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Online(within time.Duration) ([]*User, error) {
|
func Online(within time.Duration) ([]*User, error) {
|
||||||
us := GetStore()
|
|
||||||
cutoff := time.Now().Add(-within).Unix()
|
cutoff := time.Now().Add(-within).Unix()
|
||||||
|
|
||||||
result := us.FilterByIndex("allByRegistered", func(u *User) bool {
|
result := store.FilterByIndex("allByRegistered", func(u *User) bool {
|
||||||
return u.LastOnline >= cutoff
|
return u.LastOnline >= cutoff
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -248,15 +254,6 @@ func Online(within time.Duration) ([]*User, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (u *User) Insert() error {
|
|
||||||
us := GetStore()
|
|
||||||
if u.ID == 0 {
|
|
||||||
u.ID = us.GetNextID()
|
|
||||||
}
|
|
||||||
return us.AddUser(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
func (u *User) RegisteredTime() time.Time {
|
func (u *User) RegisteredTime() time.Time {
|
||||||
return time.Unix(u.Registered, 0)
|
return time.Unix(u.Registered, 0)
|
||||||
@ -351,7 +348,7 @@ func (u *User) SetPosition(x, y int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) ExpNeededForNextLevel() int {
|
func (u *User) ExpNeededForNextLevel() int {
|
||||||
return exp.Calc(u.Level + 1)
|
return u.Level * u.Level * u.Level
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GrantExp(expAmount int) {
|
func (u *User) GrantExp(expAmount int) {
|
||||||
@ -384,7 +381,7 @@ func (u *User) ExpProgress() float64 {
|
|||||||
return float64(u.Exp) / float64(u.ExpNeededForNextLevel()) * 100
|
return float64(u.Exp) / float64(u.ExpNeededForNextLevel()) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
currentLevelExp := exp.Calc(u.Level)
|
currentLevelExp := u.Level * u.Level * u.Level
|
||||||
nextLevelExp := u.ExpNeededForNextLevel()
|
nextLevelExp := u.ExpNeededForNextLevel()
|
||||||
progressExp := u.Exp
|
progressExp := u.Exp
|
||||||
|
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
package password
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
time = 1
|
|
||||||
memory = 64 * 1024
|
|
||||||
threads = 4
|
|
||||||
keyLen = 32
|
|
||||||
)
|
|
||||||
|
|
||||||
// Hash creates an argon2id hash of the password
|
|
||||||
func Hash(password string) string {
|
|
||||||
salt := make([]byte, 16)
|
|
||||||
rand.Read(salt)
|
|
||||||
|
|
||||||
hash := argon2.IDKey([]byte(password), salt, time, memory, threads, keyLen)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify checks if a password matches the hash
|
|
||||||
func Verify(password, encodedHash string) (bool, error) {
|
|
||||||
parts := strings.Split(encodedHash, "$")
|
|
||||||
if len(parts) != 6 {
|
|
||||||
return false, fmt.Errorf("invalid hash format")
|
|
||||||
}
|
|
||||||
|
|
||||||
if parts[1] != "argon2id" {
|
|
||||||
return false, fmt.Errorf("invalid hash variant")
|
|
||||||
}
|
|
||||||
|
|
||||||
var version int
|
|
||||||
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if version != argon2.Version {
|
|
||||||
return false, fmt.Errorf("incompatible argon2 version")
|
|
||||||
}
|
|
||||||
|
|
||||||
var m, t, p uint32
|
|
||||||
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hash := argon2.IDKey([]byte(password), salt, t, m, uint8(p), uint32(len(expectedHash)))
|
|
||||||
|
|
||||||
// Use constant-time comparison to prevent timing attacks
|
|
||||||
if subtle.ConstantTimeCompare(hash, expectedHash) == 1 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
@ -1,435 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Ctx = *fasthttp.RequestCtx
|
|
||||||
|
|
||||||
// Handler is a request handler with parameters.
|
|
||||||
type Handler func(ctx Ctx, params []string)
|
|
||||||
|
|
||||||
func (h Handler) Serve(ctx Ctx, params []string) {
|
|
||||||
h(ctx, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Middleware func(Handler) Handler
|
|
||||||
|
|
||||||
type node struct {
|
|
||||||
segment string
|
|
||||||
handler Handler
|
|
||||||
children []*node
|
|
||||||
isDynamic bool
|
|
||||||
isWildcard bool
|
|
||||||
maxParams uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
type Router struct {
|
|
||||||
get *node
|
|
||||||
post *node
|
|
||||||
put *node
|
|
||||||
patch *node
|
|
||||||
delete *node
|
|
||||||
middleware []Middleware
|
|
||||||
paramsBuffer []string // Pre-allocated buffer for parameters
|
|
||||||
}
|
|
||||||
|
|
||||||
type Group struct {
|
|
||||||
router *Router
|
|
||||||
prefix string
|
|
||||||
middleware []Middleware
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new Router instance.
|
|
||||||
func New() *Router {
|
|
||||||
return &Router{
|
|
||||||
get: &node{},
|
|
||||||
post: &node{},
|
|
||||||
put: &node{},
|
|
||||||
patch: &node{},
|
|
||||||
delete: &node{},
|
|
||||||
middleware: []Middleware{},
|
|
||||||
paramsBuffer: make([]string, 64),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements the Handler interface for fasthttp
|
|
||||||
func (r *Router) ServeHTTP(ctx *fasthttp.RequestCtx) {
|
|
||||||
path := string(ctx.Path())
|
|
||||||
method := string(ctx.Method())
|
|
||||||
|
|
||||||
h, params, found := r.Lookup(method, path)
|
|
||||||
if !found {
|
|
||||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h(ctx, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a fasthttp request handler
|
|
||||||
func (r *Router) Handler() fasthttp.RequestHandler {
|
|
||||||
return r.ServeHTTP
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds middleware to the router.
|
|
||||||
func (r *Router) Use(mw ...Middleware) *Router {
|
|
||||||
r.middleware = append(r.middleware, mw...)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new route group.
|
|
||||||
func (r *Router) Group(prefix string) *Group {
|
|
||||||
return &Group{router: r, prefix: prefix, middleware: []Middleware{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds middleware to the group.
|
|
||||||
func (g *Group) Use(mw ...Middleware) *Group {
|
|
||||||
g.middleware = append(g.middleware, mw...)
|
|
||||||
return g
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a nested group.
|
|
||||||
func (g *Group) Group(prefix string) *Group {
|
|
||||||
return &Group{
|
|
||||||
router: g.router,
|
|
||||||
prefix: g.prefix + prefix,
|
|
||||||
middleware: append([]Middleware{}, g.middleware...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Applies middleware in reverse order.
|
|
||||||
func applyMiddleware(h Handler, mw []Middleware) Handler {
|
|
||||||
for i := len(mw) - 1; i >= 0; i-- {
|
|
||||||
h = mw[i](h)
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a handler for the given method and path.
|
|
||||||
func (r *Router) Handle(method, path string, h Handler) error {
|
|
||||||
root := r.methodNode(method)
|
|
||||||
if root == nil {
|
|
||||||
return fmt.Errorf("unsupported method: %s", method)
|
|
||||||
}
|
|
||||||
return r.addRoute(root, path, h, r.middleware)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) methodNode(method string) *node {
|
|
||||||
switch method {
|
|
||||||
case "GET":
|
|
||||||
return r.get
|
|
||||||
case "POST":
|
|
||||||
return r.post
|
|
||||||
case "PUT":
|
|
||||||
return r.put
|
|
||||||
case "PATCH":
|
|
||||||
return r.patch
|
|
||||||
case "DELETE":
|
|
||||||
return r.delete
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a GET handler.
|
|
||||||
func (r *Router) Get(path string, h Handler) error {
|
|
||||||
return r.Handle("GET", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a POST handler.
|
|
||||||
func (r *Router) Post(path string, h Handler) error {
|
|
||||||
return r.Handle("POST", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a PUT handler.
|
|
||||||
func (r *Router) Put(path string, h Handler) error {
|
|
||||||
return r.Handle("PUT", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a PATCH handler.
|
|
||||||
func (r *Router) Patch(path string, h Handler) error {
|
|
||||||
return r.Handle("PATCH", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a DELETE handler.
|
|
||||||
func (r *Router) Delete(path string, h Handler) error {
|
|
||||||
return r.Handle("DELETE", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Group) buildGroupMiddleware() []Middleware {
|
|
||||||
mw := append([]Middleware{}, g.router.middleware...)
|
|
||||||
return append(mw, g.middleware...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a handler in the group.
|
|
||||||
func (g *Group) Handle(method, path string, h Handler) error {
|
|
||||||
root := g.router.methodNode(method)
|
|
||||||
if root == nil {
|
|
||||||
return fmt.Errorf("unsupported method: %s", method)
|
|
||||||
}
|
|
||||||
return g.router.addRoute(root, g.prefix+path, h, g.buildGroupMiddleware())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a GET handler in the group.
|
|
||||||
func (g *Group) Get(path string, h Handler) error {
|
|
||||||
return g.Handle("GET", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a POST handler in the group.
|
|
||||||
func (g *Group) Post(path string, h Handler) error {
|
|
||||||
return g.Handle("POST", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a PUT handler in the group.
|
|
||||||
func (g *Group) Put(path string, h Handler) error {
|
|
||||||
return g.Handle("PUT", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a PATCH handler in the group.
|
|
||||||
func (g *Group) Patch(path string, h Handler) error {
|
|
||||||
return g.Handle("PATCH", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a DELETE handler in the group.
|
|
||||||
func (g *Group) Delete(path string, h Handler) error {
|
|
||||||
return g.Handle("DELETE", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Applies specific middleware for next registration.
|
|
||||||
func (r *Router) WithMiddleware(mw ...Middleware) *MiddlewareRouter {
|
|
||||||
return &MiddlewareRouter{router: r, middleware: mw}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Applies specific middleware for next group route.
|
|
||||||
func (g *Group) WithMiddleware(mw ...Middleware) *MiddlewareGroup {
|
|
||||||
return &MiddlewareGroup{group: g, middleware: mw}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MiddlewareRouter struct {
|
|
||||||
router *Router
|
|
||||||
middleware []Middleware
|
|
||||||
}
|
|
||||||
|
|
||||||
type MiddlewareGroup struct {
|
|
||||||
group *Group
|
|
||||||
middleware []Middleware
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *MiddlewareRouter) buildMiddleware() []Middleware {
|
|
||||||
mw := append([]Middleware{}, mr.router.middleware...)
|
|
||||||
return append(mw, mr.middleware...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a handler with middleware router.
|
|
||||||
func (mr *MiddlewareRouter) Handle(method, path string, h Handler) error {
|
|
||||||
root := mr.router.methodNode(method)
|
|
||||||
if root == nil {
|
|
||||||
return fmt.Errorf("unsupported method: %s", method)
|
|
||||||
}
|
|
||||||
return mr.router.addRoute(root, path, h, mr.buildMiddleware())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a GET handler with middleware router.
|
|
||||||
func (mr *MiddlewareRouter) Get(path string, h Handler) error {
|
|
||||||
return mr.Handle("GET", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a POST handler with middleware router.
|
|
||||||
func (mr *MiddlewareRouter) Post(path string, h Handler) error {
|
|
||||||
return mr.Handle("POST", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a PUT handler with middleware router.
|
|
||||||
func (mr *MiddlewareRouter) Put(path string, h Handler) error {
|
|
||||||
return mr.Handle("PUT", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a PATCH handler with middleware router.
|
|
||||||
func (mr *MiddlewareRouter) Patch(path string, h Handler) error {
|
|
||||||
return mr.Handle("PATCH", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a DELETE handler with middleware router.
|
|
||||||
func (mr *MiddlewareRouter) Delete(path string, h Handler) error {
|
|
||||||
return mr.Handle("DELETE", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mg *MiddlewareGroup) buildMiddleware() []Middleware {
|
|
||||||
mw := append([]Middleware{}, mg.group.router.middleware...)
|
|
||||||
mw = append(mw, mg.group.middleware...)
|
|
||||||
return append(mw, mg.middleware...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a handler with middleware group.
|
|
||||||
func (mg *MiddlewareGroup) Handle(method, path string, h Handler) error {
|
|
||||||
root := mg.group.router.methodNode(method)
|
|
||||||
if root == nil {
|
|
||||||
return fmt.Errorf("unsupported method: %s", method)
|
|
||||||
}
|
|
||||||
return mg.group.router.addRoute(root, mg.group.prefix+path, h, mg.buildMiddleware())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a GET handler with middleware group.
|
|
||||||
func (mg *MiddlewareGroup) Get(path string, h Handler) error {
|
|
||||||
return mg.Handle("GET", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a POST handler with middleware group.
|
|
||||||
func (mg *MiddlewareGroup) Post(path string, h Handler) error {
|
|
||||||
return mg.Handle("POST", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a PUT handler with middleware group.
|
|
||||||
func (mg *MiddlewareGroup) Put(path string, h Handler) error {
|
|
||||||
return mg.Handle("PUT", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a PATCH handler with middleware group.
|
|
||||||
func (mg *MiddlewareGroup) Patch(path string, h Handler) error {
|
|
||||||
return mg.Handle("PATCH", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registers a DELETE handler with middleware group.
|
|
||||||
func (mg *MiddlewareGroup) Delete(path string, h Handler) error {
|
|
||||||
return mg.Handle("DELETE", path, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adapts a standard fasthttp.RequestHandler to the router's Handler
|
|
||||||
func StandardHandler(handler fasthttp.RequestHandler) Handler {
|
|
||||||
return func(ctx Ctx, _ []string) {
|
|
||||||
handler(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extracts the next path segment.
|
|
||||||
func readSegment(path string, start int) (segment string, end int, hasMore bool) {
|
|
||||||
if start >= len(path) {
|
|
||||||
return "", start, false
|
|
||||||
}
|
|
||||||
if path[start] == '/' {
|
|
||||||
start++
|
|
||||||
}
|
|
||||||
if start >= len(path) {
|
|
||||||
return "", start, false
|
|
||||||
}
|
|
||||||
end = start
|
|
||||||
for end < len(path) && path[end] != '/' {
|
|
||||||
end++
|
|
||||||
}
|
|
||||||
return path[start:end], end, end < len(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a new route to the trie.
|
|
||||||
func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) error {
|
|
||||||
h = applyMiddleware(h, mw)
|
|
||||||
if path == "/" {
|
|
||||||
root.handler = h
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
current := root
|
|
||||||
pos := 0
|
|
||||||
lastWC := false
|
|
||||||
count := uint8(0)
|
|
||||||
for {
|
|
||||||
seg, newPos, more := readSegment(path, pos)
|
|
||||||
if seg == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
isDyn := len(seg) > 1 && seg[0] == ':'
|
|
||||||
isWC := len(seg) > 0 && seg[0] == '*'
|
|
||||||
if isWC {
|
|
||||||
if lastWC || more {
|
|
||||||
return fmt.Errorf("wildcard must be the last segment in the path")
|
|
||||||
}
|
|
||||||
lastWC = true
|
|
||||||
}
|
|
||||||
if isDyn || isWC {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
var child *node
|
|
||||||
for _, c := range current.children {
|
|
||||||
if c.segment == seg {
|
|
||||||
child = c
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if child == nil {
|
|
||||||
child = &node{segment: seg, isDynamic: isDyn, isWildcard: isWC}
|
|
||||||
current.children = append(current.children, child)
|
|
||||||
}
|
|
||||||
if child.maxParams < count {
|
|
||||||
child.maxParams = count
|
|
||||||
}
|
|
||||||
current = child
|
|
||||||
pos = newPos
|
|
||||||
}
|
|
||||||
current.handler = h
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finds a handler matching method and path.
|
|
||||||
func (r *Router) Lookup(method, path string) (Handler, []string, bool) {
|
|
||||||
root := r.methodNode(method)
|
|
||||||
if root == nil {
|
|
||||||
return nil, nil, false
|
|
||||||
}
|
|
||||||
if path == "/" {
|
|
||||||
return root.handler, nil, root.handler != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := r.paramsBuffer
|
|
||||||
if cap(buffer) < int(root.maxParams) {
|
|
||||||
buffer = make([]string, root.maxParams)
|
|
||||||
r.paramsBuffer = buffer
|
|
||||||
}
|
|
||||||
buffer = buffer[:0]
|
|
||||||
|
|
||||||
h, paramCount, found := match(root, path, 0, &buffer)
|
|
||||||
if !found {
|
|
||||||
return nil, nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return h, buffer[:paramCount], true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traverses the trie to find a handler.
|
|
||||||
func match(current *node, path string, start int, params *[]string) (Handler, int, bool) {
|
|
||||||
paramCount := 0
|
|
||||||
|
|
||||||
for _, c := range current.children {
|
|
||||||
if c.isWildcard {
|
|
||||||
rem := path[start:]
|
|
||||||
if len(rem) > 0 && rem[0] == '/' {
|
|
||||||
rem = rem[1:]
|
|
||||||
}
|
|
||||||
*params = append(*params, rem)
|
|
||||||
return c.handler, 1, c.handler != nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seg, pos, more := readSegment(path, start)
|
|
||||||
if seg == "" {
|
|
||||||
return current.handler, 0, current.handler != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range current.children {
|
|
||||||
if c.segment == seg || c.isDynamic {
|
|
||||||
if c.isDynamic {
|
|
||||||
*params = append(*params, seg)
|
|
||||||
paramCount++
|
|
||||||
}
|
|
||||||
if !more {
|
|
||||||
return c.handler, paramCount, c.handler != nil
|
|
||||||
}
|
|
||||||
h, nestedCount, ok := match(c, path, pos, params)
|
|
||||||
if ok {
|
|
||||||
return h, paramCount + nestedCount, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
@ -4,35 +4,31 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dk/internal/auth"
|
|
||||||
"dk/internal/components"
|
"dk/internal/components"
|
||||||
"dk/internal/models/users"
|
"dk/internal/models/users"
|
||||||
"dk/internal/password"
|
|
||||||
"dk/internal/router"
|
|
||||||
"dk/internal/session"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/auth"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/password"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterAuthRoutes sets up authentication routes
|
// RegisterAuthRoutes sets up authentication routes
|
||||||
func RegisterAuthRoutes(r *router.Router) {
|
func RegisterAuthRoutes(app *sushi.App) {
|
||||||
guests := r.Group("")
|
// Public routes (no auth required)
|
||||||
guests.Use(auth.RequireGuest())
|
app.Get("/login", showLogin)
|
||||||
|
app.Post("/login", processLogin)
|
||||||
guests.Get("/login", showLogin)
|
app.Get("/register", showRegister)
|
||||||
guests.Post("/login", processLogin)
|
app.Post("/register", processRegister)
|
||||||
guests.Get("/register", showRegister)
|
|
||||||
guests.Post("/register", processRegister)
|
|
||||||
|
|
||||||
authed := r.Group("")
|
|
||||||
authed.Use(auth.RequireAuth())
|
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
authed := app.Group("")
|
||||||
|
authed.Use(auth.RequireAuth("/login"))
|
||||||
authed.Post("/logout", processLogout)
|
authed.Post("/logout", processLogout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// showLogin displays the login form
|
// showLogin displays the login form
|
||||||
func showLogin(ctx router.Ctx, _ []string) {
|
func showLogin(ctx sushi.Ctx) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
var id string
|
var id string
|
||||||
|
|
||||||
if formData, exists := sess.Get("form_data"); exists {
|
if formData, exists := sess.Get("form_data"); exists {
|
||||||
@ -41,7 +37,6 @@ func showLogin(ctx router.Ctx, _ []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sess.Delete("form_data")
|
sess.Delete("form_data")
|
||||||
session.Store(sess)
|
|
||||||
|
|
||||||
components.RenderPage(ctx, "Log In", "auth/login.html", map[string]any{
|
components.RenderPage(ctx, "Log In", "auth/login.html", map[string]any{
|
||||||
"id": id,
|
"id": id,
|
||||||
@ -49,33 +44,35 @@ func showLogin(ctx router.Ctx, _ []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processLogin handles login form submission
|
// processLogin handles login form submission
|
||||||
func processLogin(ctx router.Ctx, _ []string) {
|
func processLogin(ctx sushi.Ctx) {
|
||||||
email := strings.TrimSpace(string(ctx.PostArgs().Peek("id")))
|
email := strings.TrimSpace(string(ctx.PostArgs().Peek("id")))
|
||||||
userPassword := string(ctx.PostArgs().Peek("password"))
|
userPassword := string(ctx.PostArgs().Peek("password"))
|
||||||
|
|
||||||
if email == "" || userPassword == "" {
|
if email == "" || userPassword == "" {
|
||||||
setFlashAndFormData(ctx, "Email and password are required", map[string]string{"id": email})
|
setFlashAndFormData(ctx, "Email and password are required", map[string]string{"id": email})
|
||||||
ctx.Redirect("/login", fasthttp.StatusFound)
|
ctx.Redirect("/login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := authenticate(email, userPassword)
|
user, err := authenticate(email, userPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setFlashAndFormData(ctx, "Invalid email or password", map[string]string{"id": email})
|
setFlashAndFormData(ctx, "Invalid email or password", map[string]string{"id": email})
|
||||||
ctx.Redirect("/login", fasthttp.StatusFound)
|
ctx.Redirect("/login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.Login(ctx, user)
|
ctx.Login(user.ID, user)
|
||||||
|
|
||||||
// CSRF token is already in session, no need to transfer from cookie
|
// Set success message
|
||||||
|
sess := ctx.GetCurrentSession()
|
||||||
|
sess.SetFlash("success", fmt.Sprintf("Welcome back, %s!", user.Username))
|
||||||
|
|
||||||
ctx.Redirect("/", fasthttp.StatusFound)
|
ctx.Redirect("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// showRegister displays the registration form
|
// showRegister displays the registration form
|
||||||
func showRegister(ctx router.Ctx, _ []string) {
|
func showRegister(ctx sushi.Ctx) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
var username, email string
|
var username, email string
|
||||||
|
|
||||||
if formData, exists := sess.Get("form_data"); exists {
|
if formData, exists := sess.Get("form_data"); exists {
|
||||||
@ -85,16 +82,16 @@ func showRegister(ctx router.Ctx, _ []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sess.Delete("form_data")
|
sess.Delete("form_data")
|
||||||
session.Store(sess)
|
|
||||||
|
|
||||||
components.RenderPage(ctx, "Register", "auth/register.html", map[string]any{
|
components.RenderPage(ctx, "Register", "auth/register.html", map[string]any{
|
||||||
"username": username,
|
"username": username,
|
||||||
"email": email,
|
"email": email,
|
||||||
|
"error_message": sess.GetFlashMessage("error"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// processRegister handles registration form submission
|
// processRegister handles registration form submission
|
||||||
func processRegister(ctx router.Ctx, _ []string) {
|
func processRegister(ctx sushi.Ctx) {
|
||||||
username := strings.TrimSpace(string(ctx.PostArgs().Peek("username")))
|
username := strings.TrimSpace(string(ctx.PostArgs().Peek("username")))
|
||||||
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
|
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
|
||||||
userPassword := string(ctx.PostArgs().Peek("password"))
|
userPassword := string(ctx.PostArgs().Peek("password"))
|
||||||
@ -107,53 +104,49 @@ func processRegister(ctx router.Ctx, _ []string) {
|
|||||||
|
|
||||||
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
|
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
|
||||||
setFlashAndFormData(ctx, err.Error(), formData)
|
setFlashAndFormData(ctx, err.Error(), formData)
|
||||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
ctx.Redirect("/register")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := users.ByUsername(username); err == nil {
|
if _, err := users.ByUsername(username); err == nil {
|
||||||
setFlashAndFormData(ctx, "Username already exists", formData)
|
setFlashAndFormData(ctx, "Username already exists", formData)
|
||||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
ctx.Redirect("/register")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := users.ByEmail(email); err == nil {
|
if _, err := users.ByEmail(email); err == nil {
|
||||||
setFlashAndFormData(ctx, "Email already registered", formData)
|
setFlashAndFormData(ctx, "Email already registered", formData)
|
||||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
ctx.Redirect("/register")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := users.New()
|
user := users.New()
|
||||||
user.Username = username
|
user.Username = username
|
||||||
user.Email = email
|
user.Email = email
|
||||||
user.Password = password.Hash(userPassword)
|
user.Password = password.HashPassword(userPassword)
|
||||||
user.ClassID = 1
|
user.ClassID = 1
|
||||||
user.Auth = 1
|
user.Auth = 1
|
||||||
|
|
||||||
if err := user.Insert(); err != nil {
|
if err := user.Insert(); err != nil {
|
||||||
setFlashAndFormData(ctx, "Failed to create account", formData)
|
setFlashAndFormData(ctx, "Failed to create account", formData)
|
||||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
ctx.Redirect("/register")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-login after registration (this will update the current session)
|
// Auto-login after registration
|
||||||
auth.Login(ctx, user)
|
ctx.Login(user.ID, user)
|
||||||
|
|
||||||
// Update success message (Login already sets a message, so override it)
|
// Set success message
|
||||||
if sess := ctx.UserValue("session").(*session.Session); sess != nil {
|
sess := ctx.GetCurrentSession()
|
||||||
sess.SetFlash("success", fmt.Sprintf("Greetings, %s!", user.Username))
|
sess.SetFlash("success", fmt.Sprintf("Greetings, %s!", user.Username))
|
||||||
session.Store(sess)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSRF token is already in session, no need to transfer from cookie
|
ctx.Redirect("/")
|
||||||
|
|
||||||
ctx.Redirect("/", fasthttp.StatusFound)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// processLogout handles logout
|
// processLogout handles logout
|
||||||
func processLogout(ctx router.Ctx, params []string) {
|
func processLogout(ctx sushi.Ctx) {
|
||||||
auth.Logout(ctx)
|
ctx.Logout()
|
||||||
ctx.Redirect("/", fasthttp.StatusFound)
|
ctx.Redirect("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
@ -183,11 +176,10 @@ func validateRegistration(username, email, password, confirmPassword string) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setFlashAndFormData(ctx router.Ctx, message string, formData map[string]string) {
|
func setFlashAndFormData(ctx sushi.Ctx, message string, formData map[string]string) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
sess.SetFlash("error", message)
|
sess.SetFlash("error", message)
|
||||||
sess.Set("form_data", formData)
|
sess.Set("form_data", formData)
|
||||||
session.Store(sess)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticate(usernameOrEmail, plainPassword string) (*users.User, error) {
|
func authenticate(usernameOrEmail, plainPassword string) (*users.User, error) {
|
||||||
@ -196,13 +188,15 @@ func authenticate(usernameOrEmail, plainPassword string) (*users.User, error) {
|
|||||||
|
|
||||||
user, err = users.ByUsername(usernameOrEmail)
|
user, err = users.ByUsername(usernameOrEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
user, err = users.ByEmail(usernameOrEmail)
|
user, err = users.ByEmail(usernameOrEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid, err := password.Verify(plainPassword, user.Password)
|
isValid, err := password.VerifyPassword(plainPassword, user.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -2,45 +2,61 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/actions"
|
"dk/internal/actions"
|
||||||
"dk/internal/auth"
|
|
||||||
"dk/internal/components"
|
"dk/internal/components"
|
||||||
"dk/internal/helpers"
|
"dk/internal/helpers"
|
||||||
"dk/internal/middleware"
|
|
||||||
"dk/internal/models/fights"
|
"dk/internal/models/fights"
|
||||||
"dk/internal/models/monsters"
|
"dk/internal/models/monsters"
|
||||||
"dk/internal/models/spells"
|
"dk/internal/models/spells"
|
||||||
"dk/internal/models/users"
|
"dk/internal/models/users"
|
||||||
"dk/internal/router"
|
|
||||||
"dk/internal/session"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterFightRoutes(r *router.Router) {
|
func RegisterFightRoutes(app *sushi.App) {
|
||||||
group := r.Group("/fight")
|
group := app.Group("/fight")
|
||||||
group.Use(auth.RequireAuth())
|
group.Use(auth.RequireAuth("/login"))
|
||||||
group.Use(middleware.RequireFighting())
|
group.Use(requireFighting())
|
||||||
|
|
||||||
group.Get("/", showFight)
|
group.Get("/", showFight)
|
||||||
group.Post("/", handleFightAction)
|
group.Post("/", handleFightAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showFight(ctx router.Ctx, _ []string) {
|
// requireFighting middleware ensures the user is in a fight
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
func requireFighting() sushi.Middleware {
|
||||||
user := ctx.UserValue("user").(*users.User)
|
return func(ctx sushi.Ctx, next func()) {
|
||||||
|
user := ctx.GetCurrentUser()
|
||||||
|
if user == nil {
|
||||||
|
ctx.SendError(401, "Not authenticated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userModel := user.(*users.User)
|
||||||
|
if !userModel.IsFighting() {
|
||||||
|
ctx.Redirect("/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showFight(ctx sushi.Ctx) {
|
||||||
|
sess := ctx.GetCurrentSession()
|
||||||
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
|
|
||||||
fight, err := fights.Find(user.FightID)
|
fight, err := fights.Find(user.FightID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetContentType("text/plain")
|
ctx.SendError(404, "Fight not found")
|
||||||
ctx.SetBodyString("Fight not found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
monster, err := monsters.Find(fight.MonsterID)
|
monster, err := monsters.Find(fight.MonsterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetContentType("text/plain")
|
ctx.SendError(404, "Monster not found for fight")
|
||||||
ctx.SetBodyString("Monster not found for fight")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,9 +98,9 @@ func showFight(ctx router.Ctx, _ []string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFightAction(ctx router.Ctx, _ []string) {
|
func handleFightAction(ctx sushi.Ctx) {
|
||||||
user := ctx.UserValue("user").(*users.User)
|
sess := ctx.GetCurrentSession()
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
|
|
||||||
fight, err := fights.Find(user.FightID)
|
fight, err := fights.Find(user.FightID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -5,52 +5,51 @@ import (
|
|||||||
"dk/internal/components"
|
"dk/internal/components"
|
||||||
"dk/internal/models/towns"
|
"dk/internal/models/towns"
|
||||||
"dk/internal/models/users"
|
"dk/internal/models/users"
|
||||||
"dk/internal/router"
|
|
||||||
"dk/internal/session"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Index(ctx router.Ctx, _ []string) {
|
func Index(ctx sushi.Ctx) {
|
||||||
user, ok := ctx.UserValue("user").(*users.User)
|
if !ctx.IsAuthenticated() {
|
||||||
if !ok || user == nil {
|
|
||||||
components.RenderPage(ctx, "", "intro.html", nil)
|
components.RenderPage(ctx, "", "intro.html", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
|
|
||||||
switch user.Currently {
|
switch user.Currently {
|
||||||
case "In Town":
|
case "In Town":
|
||||||
ctx.Redirect("/town", 303)
|
ctx.Redirect("/town")
|
||||||
case "Exploring":
|
case "Exploring":
|
||||||
ctx.Redirect("/explore", 303)
|
ctx.Redirect("/explore")
|
||||||
case "Fighting":
|
case "Fighting":
|
||||||
ctx.Redirect("/fight", 303)
|
ctx.Redirect("/fight")
|
||||||
default:
|
default:
|
||||||
ctx.Redirect("/explore", 303)
|
ctx.Redirect("/explore")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Move(ctx router.Ctx, _ []string) {
|
func Move(ctx sushi.Ctx) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
user := ctx.UserValue("user").(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
|
|
||||||
if user.Currently == "Fighting" {
|
if user.Currently == "Fighting" {
|
||||||
sess.SetFlash("error", "You can't just run from a fight!")
|
sess.SetFlash("error", "You can't just run from a fight!")
|
||||||
ctx.Redirect("/fight", 303)
|
ctx.Redirect("/fight")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dir, err := strconv.Atoi(string(ctx.PostArgs().Peek("direction")))
|
dir, err := strconv.Atoi(string(ctx.PostArgs().Peek("direction")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetContentType("text/plain")
|
ctx.SendError(400, "move form parsing error")
|
||||||
ctx.SetBodyString("move form parsing error")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currently, newX, newY, err := actions.Move(user, actions.Direction(dir))
|
currently, newX, newY, err := actions.Move(user, actions.Direction(dir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetContentType("text/plain")
|
ctx.SendError(400, "move error: "+err.Error())
|
||||||
ctx.SetBodyString("move error: " + err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,50 +59,45 @@ func Move(ctx router.Ctx, _ []string) {
|
|||||||
|
|
||||||
switch currently {
|
switch currently {
|
||||||
case "In Town":
|
case "In Town":
|
||||||
ctx.Redirect("/town", 303)
|
ctx.Redirect("/town")
|
||||||
case "Fighting":
|
case "Fighting":
|
||||||
ctx.Redirect("/fight", 303)
|
ctx.Redirect("/fight")
|
||||||
default:
|
default:
|
||||||
ctx.Redirect("/explore", 303)
|
ctx.Redirect("/explore")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Explore(ctx router.Ctx, _ []string) {
|
func Explore(ctx sushi.Ctx) {
|
||||||
user := ctx.UserValue("user").(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
if user.Currently != "Exploring" {
|
if user.Currently != "Exploring" {
|
||||||
ctx.Redirect("/", 303)
|
ctx.Redirect("/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
components.RenderPage(ctx, "", "explore.html", nil)
|
components.RenderPage(ctx, "", "explore.html", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Teleport(ctx router.Ctx, params []string) {
|
func Teleport(ctx sushi.Ctx) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
|
|
||||||
id, err := strconv.Atoi(params[0])
|
id := ctx.Param("id").Int()
|
||||||
if err != nil {
|
|
||||||
sess.SetFlash("error", "Error teleporting; "+err.Error())
|
|
||||||
ctx.Redirect("/", 302)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
town, err := towns.Find(id)
|
town, err := towns.Find(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sess.SetFlash("error", "Failed to teleport. Unknown town.")
|
sess.SetFlash("error", "Failed to teleport. Unknown town.")
|
||||||
ctx.Redirect("/", 302)
|
ctx.Redirect("/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := ctx.UserValue("user").(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
if !slices.Contains(user.GetTownIDs(), id) {
|
if !slices.Contains(user.GetTownIDs(), id) {
|
||||||
sess.SetFlash("error", "You don't have a map to "+town.Name+".")
|
sess.SetFlash("error", "You don't have a map to "+town.Name+".")
|
||||||
ctx.Redirect("/", 302)
|
ctx.Redirect("/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.TP < town.TPCost {
|
if user.TP < town.TPCost {
|
||||||
sess.SetFlash("error", "You don't have enough TP to teleport to "+town.Name+".")
|
sess.SetFlash("error", "You don't have enough TP to teleport to "+town.Name+".")
|
||||||
ctx.Redirect("/", 302)
|
ctx.Redirect("/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,5 +107,5 @@ func Teleport(ctx router.Ctx, params []string) {
|
|||||||
user.Save()
|
user.Save()
|
||||||
|
|
||||||
sess.SetFlash("success", "You teleported to "+town.Name+" successfully!")
|
sess.SetFlash("success", "You teleported to "+town.Name+" successfully!")
|
||||||
ctx.Redirect("/town", 302)
|
ctx.Redirect("/town")
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,16 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"dk/internal/actions"
|
"dk/internal/actions"
|
||||||
"dk/internal/auth"
|
|
||||||
"dk/internal/components"
|
"dk/internal/components"
|
||||||
"dk/internal/helpers"
|
"dk/internal/helpers"
|
||||||
"dk/internal/middleware"
|
|
||||||
"dk/internal/models/items"
|
"dk/internal/models/items"
|
||||||
"dk/internal/models/towns"
|
"dk/internal/models/towns"
|
||||||
"dk/internal/models/users"
|
"dk/internal/models/users"
|
||||||
"dk/internal/router"
|
"fmt"
|
||||||
"dk/internal/session"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Map acts as a representation of owned/unowned maps in the town stores.
|
// Map acts as a representation of owned/unowned maps in the town stores.
|
||||||
@ -26,10 +25,10 @@ type Map struct {
|
|||||||
TP int
|
TP int
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterTownRoutes(r *router.Router) {
|
func RegisterTownRoutes(app *sushi.App) {
|
||||||
group := r.Group("/town")
|
group := app.Group("/town")
|
||||||
group.Use(auth.RequireAuth())
|
group.Use(auth.RequireAuth("/login"))
|
||||||
group.Use(middleware.RequireTown())
|
group.Use(requireTown())
|
||||||
|
|
||||||
group.Get("/", showTown)
|
group.Get("/", showTown)
|
||||||
group.Get("/inn", showInn)
|
group.Get("/inn", showInn)
|
||||||
@ -40,7 +39,33 @@ func RegisterTownRoutes(r *router.Router) {
|
|||||||
group.Get("/maps/buy/:id", buyMap)
|
group.Get("/maps/buy/:id", buyMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showTown(ctx router.Ctx, _ []string) {
|
// requireTown middleware ensures the user is in a town
|
||||||
|
func requireTown() sushi.Middleware {
|
||||||
|
return func(ctx sushi.Ctx, next func()) {
|
||||||
|
user := ctx.GetCurrentUser()
|
||||||
|
if user == nil {
|
||||||
|
ctx.SendError(401, "Not authenticated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userModel := user.(*users.User)
|
||||||
|
if userModel.Currently != "In Town" {
|
||||||
|
ctx.SendError(403, "You must be in town")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
town, err := towns.ByCoords(userModel.X, userModel.Y)
|
||||||
|
if err != nil || town == nil || town.ID == 0 {
|
||||||
|
ctx.SendError(403, fmt.Sprintf("Invalid town location (%d, %d)", userModel.X, userModel.Y))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetUserValue("town", town)
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showTown(ctx sushi.Ctx) {
|
||||||
town := ctx.UserValue("town").(*towns.Town)
|
town := ctx.UserValue("town").(*towns.Town)
|
||||||
components.RenderPage(ctx, town.Name, "town/town.html", map[string]any{
|
components.RenderPage(ctx, town.Name, "town/town.html", map[string]any{
|
||||||
"town": town,
|
"town": town,
|
||||||
@ -49,7 +74,7 @@ func showTown(ctx router.Ctx, _ []string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func showInn(ctx router.Ctx, _ []string) {
|
func showInn(ctx sushi.Ctx) {
|
||||||
town := ctx.UserValue("town").(*towns.Town)
|
town := ctx.UserValue("town").(*towns.Town)
|
||||||
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
|
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
|
||||||
"town": town,
|
"town": town,
|
||||||
@ -57,19 +82,20 @@ func showInn(ctx router.Ctx, _ []string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func rest(ctx router.Ctx, _ []string) {
|
func rest(ctx sushi.Ctx) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
town := ctx.UserValue("town").(*towns.Town)
|
town := ctx.UserValue("town").(*towns.Town)
|
||||||
user := ctx.UserValue("user").(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
|
|
||||||
if user.Gold < town.InnCost {
|
if user.Gold < town.InnCost {
|
||||||
sess.SetFlash("error", "You can't afford to stay here tonight.")
|
sess.SetFlash("error", "You can't afford to stay here tonight.")
|
||||||
ctx.Redirect("/town/inn", 303)
|
ctx.Redirect("/town/inn")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Gold -= town.InnCost
|
user.Gold -= town.InnCost
|
||||||
user.HP, user.MP, user.TP = user.MaxHP, user.MaxMP, user.MaxTP
|
user.HP, user.MP, user.TP = user.MaxHP, user.MaxMP, user.MaxTP
|
||||||
|
user.Save()
|
||||||
|
|
||||||
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
|
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
|
||||||
"town": town,
|
"town": town,
|
||||||
@ -77,14 +103,13 @@ func rest(ctx router.Ctx, _ []string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func showShop(ctx router.Ctx, _ []string) {
|
func showShop(ctx sushi.Ctx) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
var errorHTML string
|
var errorHTML string
|
||||||
|
|
||||||
if flash, exists := sess.GetFlash("error"); exists {
|
errorMsg := sess.GetFlashMessage("error")
|
||||||
if msg, ok := flash.(string); ok {
|
if errorMsg != "" {
|
||||||
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + msg + "</div>"
|
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + errorMsg + "</div>"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
town := ctx.UserValue("town").(*towns.Town)
|
town := ctx.UserValue("town").(*towns.Town)
|
||||||
@ -107,34 +132,29 @@ func showShop(ctx router.Ctx, _ []string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func buyItem(ctx router.Ctx, params []string) {
|
func buyItem(ctx sushi.Ctx) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
|
|
||||||
id, err := strconv.Atoi(params[0])
|
id := ctx.Param("id").Int()
|
||||||
if err != nil {
|
|
||||||
sess.SetFlash("error", "Error purchasing item; "+err.Error())
|
|
||||||
ctx.Redirect("/town/shop", 302)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
town := ctx.UserValue("town").(*towns.Town)
|
town := ctx.UserValue("town").(*towns.Town)
|
||||||
if !slices.Contains(town.GetShopItems(), id) {
|
if !slices.Contains(town.GetShopItems(), id) {
|
||||||
sess.SetFlash("error", "The item doesn't exist in this shop.")
|
sess.SetFlash("error", "The item doesn't exist in this shop.")
|
||||||
ctx.Redirect("/town/shop", 302)
|
ctx.Redirect("/town/shop")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err := items.Find(id)
|
item, err := items.Find(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sess.SetFlash("error", "Error purchasing item; "+err.Error())
|
sess.SetFlash("error", "Error purchasing item; "+err.Error())
|
||||||
ctx.Redirect("/town/shop", 302)
|
ctx.Redirect("/town/shop")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := ctx.UserValue("user").(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
if user.Gold < item.Value {
|
if user.Gold < item.Value {
|
||||||
sess.SetFlash("error", "You don't have enough gold to buy "+item.Name)
|
sess.SetFlash("error", "You don't have enough gold to buy "+item.Name)
|
||||||
ctx.Redirect("/town/shop", 302)
|
ctx.Redirect("/town/shop")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,21 +162,20 @@ func buyItem(ctx router.Ctx, params []string) {
|
|||||||
actions.UserEquipItem(user, item)
|
actions.UserEquipItem(user, item)
|
||||||
user.Save()
|
user.Save()
|
||||||
|
|
||||||
ctx.Redirect("/town/shop", 302)
|
ctx.Redirect("/town/shop")
|
||||||
}
|
}
|
||||||
|
|
||||||
func showMaps(ctx router.Ctx, _ []string) {
|
func showMaps(ctx sushi.Ctx) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
var errorHTML string
|
var errorHTML string
|
||||||
|
|
||||||
if flash, exists := sess.GetFlash("error"); exists {
|
errorMsg := sess.GetFlashMessage("error")
|
||||||
if msg, ok := flash.(string); ok {
|
if errorMsg != "" {
|
||||||
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + msg + "</div>"
|
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + errorMsg + "</div>"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
town := ctx.UserValue("town").(*towns.Town)
|
town := ctx.UserValue("town").(*towns.Town)
|
||||||
user := ctx.UserValue("user").(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
|
|
||||||
maplist := helpers.NewOrderedMap[int, Map]()
|
maplist := helpers.NewOrderedMap[int, Map]()
|
||||||
towns, _ := towns.All()
|
towns, _ := towns.All()
|
||||||
@ -190,37 +209,30 @@ func showMaps(ctx router.Ctx, _ []string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func buyMap(ctx router.Ctx, params []string) {
|
func buyMap(ctx sushi.Ctx) {
|
||||||
sess := ctx.UserValue("session").(*session.Session)
|
sess := ctx.GetCurrentSession()
|
||||||
|
|
||||||
id, err := strconv.Atoi(params[0])
|
id := ctx.Param("id").Int()
|
||||||
if err != nil {
|
|
||||||
sess.SetFlash("error", "Error purchasing map; "+err.Error())
|
|
||||||
ctx.Redirect("/town/maps", 302)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mapped, err := towns.Find(id)
|
mapped, err := towns.Find(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sess.SetFlash("error", "Error purchasing map; "+err.Error())
|
sess.SetFlash("error", "Error purchasing map; "+err.Error())
|
||||||
ctx.Redirect("/town/maps", 302)
|
ctx.Redirect("/town/maps")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := ctx.UserValue("user").(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
if user.Gold < mapped.MapCost {
|
if user.Gold < mapped.MapCost {
|
||||||
sess.SetFlash("error", "You don't have enough gold to buy the map to "+mapped.Name)
|
sess.SetFlash("error", "You don't have enough gold to buy the map to "+mapped.Name)
|
||||||
ctx.Redirect("/town/maps", 302)
|
ctx.Redirect("/town/maps")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Gold -= mapped.MapCost
|
user.Gold -= mapped.MapCost
|
||||||
if user.Towns == "" {
|
townIDs := user.GetTownIDs()
|
||||||
user.Towns = params[0]
|
townIDs = append(townIDs, id)
|
||||||
} else {
|
user.SetTownIDs(townIDs)
|
||||||
user.Towns += "," + params[0]
|
|
||||||
}
|
|
||||||
user.Save()
|
user.Save()
|
||||||
|
|
||||||
ctx.Redirect("/town/maps", 302)
|
ctx.Redirect("/town/maps")
|
||||||
}
|
}
|
||||||
|
@ -1,180 +0,0 @@
|
|||||||
package session
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SessionManager handles session storage and persistence
|
|
||||||
type SessionManager struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
sessions map[string]*Session
|
|
||||||
filePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
var Manager *SessionManager
|
|
||||||
|
|
||||||
// sessionData represents session data for JSON serialization (excludes ID)
|
|
||||||
type sessionData struct {
|
|
||||||
UserID int `json:"user_id"`
|
|
||||||
ExpiresAt int64 `json:"expires_at"`
|
|
||||||
Data map[string]any `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the global session manager
|
|
||||||
func Init(filePath string) {
|
|
||||||
if Manager != nil {
|
|
||||||
panic("session manager already initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
Manager = &SessionManager{
|
|
||||||
sessions: make(map[string]*Session),
|
|
||||||
filePath: filePath,
|
|
||||||
}
|
|
||||||
|
|
||||||
Manager.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetManager returns the global session manager
|
|
||||||
func GetManager() *SessionManager {
|
|
||||||
if Manager == nil {
|
|
||||||
panic("session manager not initialized")
|
|
||||||
}
|
|
||||||
return Manager
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates and stores a new session
|
|
||||||
func (sm *SessionManager) Create(userID int) *Session {
|
|
||||||
sess := New(userID)
|
|
||||||
sm.mu.Lock()
|
|
||||||
sm.sessions[sess.ID] = sess
|
|
||||||
sm.mu.Unlock()
|
|
||||||
return sess
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves a session by ID
|
|
||||||
func (sm *SessionManager) Get(sessionID string) (*Session, bool) {
|
|
||||||
sm.mu.RLock()
|
|
||||||
sess, exists := sm.sessions[sessionID]
|
|
||||||
sm.mu.RUnlock()
|
|
||||||
|
|
||||||
if !exists || sess.IsExpired() {
|
|
||||||
if exists {
|
|
||||||
sm.Delete(sessionID)
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return sess, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store saves a session in memory (updates existing or creates new)
|
|
||||||
func (sm *SessionManager) Store(sess *Session) {
|
|
||||||
sm.mu.Lock()
|
|
||||||
sm.sessions[sess.ID] = sess
|
|
||||||
sm.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes a session
|
|
||||||
func (sm *SessionManager) Delete(sessionID string) {
|
|
||||||
sm.mu.Lock()
|
|
||||||
delete(sm.sessions, sessionID)
|
|
||||||
sm.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup removes expired sessions
|
|
||||||
func (sm *SessionManager) Cleanup() {
|
|
||||||
sm.mu.Lock()
|
|
||||||
for id, sess := range sm.sessions {
|
|
||||||
if sess.IsExpired() {
|
|
||||||
delete(sm.sessions, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sm.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stats returns session statistics
|
|
||||||
func (sm *SessionManager) Stats() (total, active int) {
|
|
||||||
sm.mu.RLock()
|
|
||||||
defer sm.mu.RUnlock()
|
|
||||||
|
|
||||||
total = len(sm.sessions)
|
|
||||||
for _, sess := range sm.sessions {
|
|
||||||
if !sess.IsExpired() {
|
|
||||||
active++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// load reads sessions from the JSON file
|
|
||||||
func (sm *SessionManager) load() {
|
|
||||||
if sm.filePath == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(sm.filePath)
|
|
||||||
if err != nil {
|
|
||||||
return // File doesn't exist or can't be read
|
|
||||||
}
|
|
||||||
|
|
||||||
var sessionsData map[string]*sessionData
|
|
||||||
if err := json.Unmarshal(data, &sessionsData); err != nil {
|
|
||||||
return // Invalid JSON
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().Unix()
|
|
||||||
sm.mu.Lock()
|
|
||||||
for id, data := range sessionsData {
|
|
||||||
if data != nil && data.ExpiresAt > now {
|
|
||||||
sess := &Session{
|
|
||||||
ID: id,
|
|
||||||
UserID: data.UserID, // Make sure we restore the UserID properly
|
|
||||||
ExpiresAt: data.ExpiresAt,
|
|
||||||
Data: data.Data,
|
|
||||||
}
|
|
||||||
if sess.Data == nil {
|
|
||||||
sess.Data = make(map[string]any)
|
|
||||||
}
|
|
||||||
sm.sessions[id] = sess
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sm.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save writes sessions to the JSON file
|
|
||||||
func (sm *SessionManager) Save() error {
|
|
||||||
if sm.filePath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sm.Cleanup() // Remove expired sessions before saving
|
|
||||||
|
|
||||||
sm.mu.RLock()
|
|
||||||
|
|
||||||
// Convert sessions to sessionData (without ID field)
|
|
||||||
sessionsData := make(map[string]*sessionData, len(sm.sessions))
|
|
||||||
for id, sess := range sm.sessions {
|
|
||||||
sessionsData[id] = &sessionData{
|
|
||||||
UserID: sess.UserID, // Save the actual UserID from the struct
|
|
||||||
ExpiresAt: sess.ExpiresAt,
|
|
||||||
Data: sess.Data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.MarshalIndent(sessionsData, "", "\t")
|
|
||||||
sm.mu.RUnlock()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(sm.filePath, data, 0600)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close saves sessions and cleans up
|
|
||||||
func (sm *SessionManager) Close() error {
|
|
||||||
return sm.Save()
|
|
||||||
}
|
|
@ -1,146 +0,0 @@
|
|||||||
// session.go
|
|
||||||
package session
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultExpiration = 24 * time.Hour
|
|
||||||
IDLength = 32
|
|
||||||
)
|
|
||||||
|
|
||||||
// Session represents a user session
|
|
||||||
type Session struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
UserID int `json:"user_id"` // 0 for guest sessions
|
|
||||||
ExpiresAt int64 `json:"expires_at"`
|
|
||||||
Data map[string]any `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new session
|
|
||||||
func New(userID int) *Session {
|
|
||||||
return &Session{
|
|
||||||
ID: generateID(),
|
|
||||||
UserID: userID,
|
|
||||||
ExpiresAt: time.Now().Add(DefaultExpiration).Unix(),
|
|
||||||
Data: make(map[string]any),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExpired checks if the session has expired
|
|
||||||
func (s *Session) IsExpired() bool {
|
|
||||||
return time.Now().Unix() > s.ExpiresAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Touch extends the session expiration
|
|
||||||
func (s *Session) Touch() {
|
|
||||||
s.ExpiresAt = time.Now().Add(DefaultExpiration).Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set stores a value in the session
|
|
||||||
func (s *Session) Set(key string, value any) {
|
|
||||||
s.Data[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves a value from the session
|
|
||||||
func (s *Session) Get(key string) (any, bool) {
|
|
||||||
value, exists := s.Data[key]
|
|
||||||
return value, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes a value from the session
|
|
||||||
func (s *Session) Delete(key string) {
|
|
||||||
delete(s.Data, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFlash stores a flash message (consumed on next Get)
|
|
||||||
func (s *Session) SetFlash(key string, value any) {
|
|
||||||
s.Set("flash_"+key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFlash retrieves and removes a flash message
|
|
||||||
func (s *Session) GetFlash(key string) (any, bool) {
|
|
||||||
flashKey := "flash_" + key
|
|
||||||
value, exists := s.Get(flashKey)
|
|
||||||
if exists {
|
|
||||||
s.Delete(flashKey)
|
|
||||||
}
|
|
||||||
return value, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFlashMessage retrieves and removes a flash message as string or empty string
|
|
||||||
func (s *Session) GetFlashMessage(key string) string {
|
|
||||||
if flash, exists := s.GetFlash(key); exists {
|
|
||||||
if msg, ok := flash.(string); ok {
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteFlash removes a flash from the session.
|
|
||||||
func (s *Session) DeleteFlash(key string) {
|
|
||||||
s.GetFlash(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegenerateID creates a new session ID and updates storage
|
|
||||||
func (s *Session) RegenerateID() {
|
|
||||||
oldID := s.ID
|
|
||||||
s.ID = generateID()
|
|
||||||
|
|
||||||
if Manager != nil {
|
|
||||||
Manager.mu.Lock()
|
|
||||||
delete(Manager.sessions, oldID)
|
|
||||||
Manager.sessions[s.ID] = s
|
|
||||||
Manager.mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUserID updates the session's user ID (for login/logout)
|
|
||||||
func (s *Session) SetUserID(userID int) {
|
|
||||||
s.UserID = userID
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateID creates a random session ID
|
|
||||||
func generateID() string {
|
|
||||||
bytes := make([]byte, IDLength)
|
|
||||||
rand.Read(bytes)
|
|
||||||
return hex.EncodeToString(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Package-level convenience functions
|
|
||||||
func Create(userID int) *Session {
|
|
||||||
return Manager.Create(userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Get(sessionID string) (*Session, bool) {
|
|
||||||
return Manager.Get(sessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Store(sess *Session) {
|
|
||||||
Manager.Store(sess)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Delete(sessionID string) {
|
|
||||||
Manager.Delete(sessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cleanup() {
|
|
||||||
Manager.Cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Stats() (total, active int) {
|
|
||||||
return Manager.Stats()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Close() error {
|
|
||||||
return Manager.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegenerateID regenerates the session ID for security (package-level convenience)
|
|
||||||
func RegenerateID(sess *Session) {
|
|
||||||
sess.RegenerateID()
|
|
||||||
}
|
|
@ -1,526 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validatable interface for entities that can validate themselves
|
|
||||||
type Validatable interface {
|
|
||||||
Validate() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// IndexBuilder function type for building custom indices
|
|
||||||
type IndexBuilder[T any] func(allItems map[int]*T) any
|
|
||||||
|
|
||||||
// BaseStore provides generic storage with index management
|
|
||||||
type BaseStore[T any] struct {
|
|
||||||
items map[int]*T
|
|
||||||
maxID int
|
|
||||||
mu sync.RWMutex
|
|
||||||
itemType reflect.Type
|
|
||||||
indices map[string]any
|
|
||||||
indexBuilders map[string]IndexBuilder[T]
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBaseStore creates a new base store for type T
|
|
||||||
func NewBaseStore[T any]() *BaseStore[T] {
|
|
||||||
var zero T
|
|
||||||
return &BaseStore[T]{
|
|
||||||
items: make(map[int]*T),
|
|
||||||
maxID: 0,
|
|
||||||
itemType: reflect.TypeOf(zero),
|
|
||||||
indices: make(map[string]any),
|
|
||||||
indexBuilders: make(map[string]IndexBuilder[T]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterIndex registers an index builder function
|
|
||||||
func (bs *BaseStore[T]) RegisterIndex(name string, builder IndexBuilder[T]) {
|
|
||||||
bs.mu.Lock()
|
|
||||||
defer bs.mu.Unlock()
|
|
||||||
bs.indexBuilders[name] = builder
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIndex retrieves a named index
|
|
||||||
func (bs *BaseStore[T]) GetIndex(name string) (any, bool) {
|
|
||||||
bs.mu.RLock()
|
|
||||||
defer bs.mu.RUnlock()
|
|
||||||
index, exists := bs.indices[name]
|
|
||||||
return index, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// RebuildIndices rebuilds all registered indices
|
|
||||||
func (bs *BaseStore[T]) RebuildIndices() {
|
|
||||||
bs.mu.Lock()
|
|
||||||
defer bs.mu.Unlock()
|
|
||||||
bs.rebuildIndicesUnsafe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *BaseStore[T]) rebuildIndicesUnsafe() {
|
|
||||||
allItems := make(map[int]*T, len(bs.items))
|
|
||||||
maps.Copy(allItems, bs.items)
|
|
||||||
|
|
||||||
for name, builder := range bs.indexBuilders {
|
|
||||||
bs.indices[name] = builder(allItems)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddWithRebuild adds item with validation and index rebuild
|
|
||||||
func (bs *BaseStore[T]) AddWithRebuild(id int, item *T) error {
|
|
||||||
bs.mu.Lock()
|
|
||||||
defer bs.mu.Unlock()
|
|
||||||
|
|
||||||
if validatable, ok := any(item).(Validatable); ok {
|
|
||||||
if err := validatable.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bs.items[id] = item
|
|
||||||
if id > bs.maxID {
|
|
||||||
bs.maxID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
bs.rebuildIndicesUnsafe()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveWithRebuild removes item and rebuilds indices
|
|
||||||
func (bs *BaseStore[T]) RemoveWithRebuild(id int) {
|
|
||||||
bs.mu.Lock()
|
|
||||||
defer bs.mu.Unlock()
|
|
||||||
delete(bs.items, id)
|
|
||||||
bs.rebuildIndicesUnsafe()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateWithRebuild updates item with validation and index rebuild
|
|
||||||
func (bs *BaseStore[T]) UpdateWithRebuild(id int, item *T) error {
|
|
||||||
return bs.AddWithRebuild(id, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common Query Methods
|
|
||||||
|
|
||||||
// Find retrieves an item by ID
|
|
||||||
func (bs *BaseStore[T]) Find(id int) (*T, bool) {
|
|
||||||
bs.mu.RLock()
|
|
||||||
defer bs.mu.RUnlock()
|
|
||||||
item, exists := bs.items[id]
|
|
||||||
return item, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllSorted returns all items using named sorted index
|
|
||||||
func (bs *BaseStore[T]) AllSorted(indexName string) []*T {
|
|
||||||
bs.mu.RLock()
|
|
||||||
defer bs.mu.RUnlock()
|
|
||||||
|
|
||||||
if index, exists := bs.indices[indexName]; exists {
|
|
||||||
if sortedIDs, ok := index.([]int); ok {
|
|
||||||
result := make([]*T, 0, len(sortedIDs))
|
|
||||||
for _, id := range sortedIDs {
|
|
||||||
if item, exists := bs.items[id]; exists {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: return all items by ID order
|
|
||||||
ids := make([]int, 0, len(bs.items))
|
|
||||||
for id := range bs.items {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
sort.Ints(ids)
|
|
||||||
|
|
||||||
result := make([]*T, 0, len(ids))
|
|
||||||
for _, id := range ids {
|
|
||||||
result = append(result, bs.items[id])
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupByIndex finds single item using string lookup index
|
|
||||||
func (bs *BaseStore[T]) LookupByIndex(indexName, key string) (*T, bool) {
|
|
||||||
bs.mu.RLock()
|
|
||||||
defer bs.mu.RUnlock()
|
|
||||||
|
|
||||||
if index, exists := bs.indices[indexName]; exists {
|
|
||||||
if lookupMap, ok := index.(map[string]int); ok {
|
|
||||||
if id, found := lookupMap[key]; found {
|
|
||||||
if item, exists := bs.items[id]; exists {
|
|
||||||
return item, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupByIndex returns items grouped by key
|
|
||||||
func (bs *BaseStore[T]) GroupByIndex(indexName string, key any) []*T {
|
|
||||||
bs.mu.RLock()
|
|
||||||
defer bs.mu.RUnlock()
|
|
||||||
|
|
||||||
if index, exists := bs.indices[indexName]; exists {
|
|
||||||
switch groupMap := index.(type) {
|
|
||||||
case map[int][]int:
|
|
||||||
if intKey, ok := key.(int); ok {
|
|
||||||
if ids, found := groupMap[intKey]; found {
|
|
||||||
result := make([]*T, 0, len(ids))
|
|
||||||
for _, id := range ids {
|
|
||||||
if item, exists := bs.items[id]; exists {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case map[string][]int:
|
|
||||||
if strKey, ok := key.(string); ok {
|
|
||||||
if ids, found := groupMap[strKey]; found {
|
|
||||||
result := make([]*T, 0, len(ids))
|
|
||||||
for _, id := range ids {
|
|
||||||
if item, exists := bs.items[id]; exists {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []*T{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterByIndex returns items matching filter criteria
|
|
||||||
func (bs *BaseStore[T]) FilterByIndex(indexName string, filterFunc func(*T) bool) []*T {
|
|
||||||
bs.mu.RLock()
|
|
||||||
defer bs.mu.RUnlock()
|
|
||||||
|
|
||||||
var sourceIDs []int
|
|
||||||
|
|
||||||
if index, exists := bs.indices[indexName]; exists {
|
|
||||||
if sortedIDs, ok := index.([]int); ok {
|
|
||||||
sourceIDs = sortedIDs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sourceIDs == nil {
|
|
||||||
for id := range bs.items {
|
|
||||||
sourceIDs = append(sourceIDs, id)
|
|
||||||
}
|
|
||||||
sort.Ints(sourceIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []*T
|
|
||||||
for _, id := range sourceIDs {
|
|
||||||
if item, exists := bs.items[id]; exists && filterFunc(item) {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildStringLookupIndex creates string-to-ID mapping
|
|
||||||
func BuildStringLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
|
|
||||||
return func(allItems map[int]*T) any {
|
|
||||||
index := make(map[string]int)
|
|
||||||
for id, item := range allItems {
|
|
||||||
key := keyFunc(item)
|
|
||||||
index[key] = id
|
|
||||||
}
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildCaseInsensitiveLookupIndex creates lowercase string-to-ID mapping
|
|
||||||
func BuildCaseInsensitiveLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
|
|
||||||
return func(allItems map[int]*T) any {
|
|
||||||
index := make(map[string]int)
|
|
||||||
for id, item := range allItems {
|
|
||||||
key := strings.ToLower(keyFunc(item))
|
|
||||||
index[key] = id
|
|
||||||
}
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildIntGroupIndex creates int-to-[]ID mapping
|
|
||||||
func BuildIntGroupIndex[T any](keyFunc func(*T) int) IndexBuilder[T] {
|
|
||||||
return func(allItems map[int]*T) any {
|
|
||||||
index := make(map[int][]int)
|
|
||||||
for id, item := range allItems {
|
|
||||||
key := keyFunc(item)
|
|
||||||
index[key] = append(index[key], id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort each group by ID
|
|
||||||
for key := range index {
|
|
||||||
sort.Ints(index[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildStringGroupIndex creates string-to-[]ID mapping
|
|
||||||
func BuildStringGroupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
|
|
||||||
return func(allItems map[int]*T) any {
|
|
||||||
index := make(map[string][]int)
|
|
||||||
for id, item := range allItems {
|
|
||||||
key := keyFunc(item)
|
|
||||||
index[key] = append(index[key], id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort each group by ID
|
|
||||||
for key := range index {
|
|
||||||
sort.Ints(index[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildSortedListIndex creates sorted []ID list
|
|
||||||
func BuildSortedListIndex[T any](sortFunc func(*T, *T) bool) IndexBuilder[T] {
|
|
||||||
return func(allItems map[int]*T) any {
|
|
||||||
ids := make([]int, 0, len(allItems))
|
|
||||||
for id := range allItems {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(ids, func(i, j int) bool {
|
|
||||||
return sortFunc(allItems[ids[i]], allItems[ids[j]])
|
|
||||||
})
|
|
||||||
|
|
||||||
return ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSingleton creates singleton store pattern with sync.Once
|
|
||||||
func NewSingleton[S any](initFunc func() *S) func() *S {
|
|
||||||
var store *S
|
|
||||||
var once sync.Once
|
|
||||||
|
|
||||||
return func() *S {
|
|
||||||
once.Do(func() {
|
|
||||||
store = initFunc()
|
|
||||||
})
|
|
||||||
return store
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNextID returns the next available ID atomically
|
|
||||||
func (bs *BaseStore[T]) GetNextID() int {
|
|
||||||
bs.mu.Lock()
|
|
||||||
defer bs.mu.Unlock()
|
|
||||||
bs.maxID++
|
|
||||||
return bs.maxID
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByID retrieves an item by ID
|
|
||||||
func (bs *BaseStore[T]) GetByID(id int) (*T, bool) {
|
|
||||||
return bs.Find(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds an item to the store
|
|
||||||
func (bs *BaseStore[T]) Add(id int, item *T) {
|
|
||||||
bs.mu.Lock()
|
|
||||||
defer bs.mu.Unlock()
|
|
||||||
bs.items[id] = item
|
|
||||||
if id > bs.maxID {
|
|
||||||
bs.maxID = id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes an item from the store
|
|
||||||
func (bs *BaseStore[T]) Remove(id int) {
|
|
||||||
bs.mu.Lock()
|
|
||||||
defer bs.mu.Unlock()
|
|
||||||
delete(bs.items, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAll returns all items
|
|
||||||
func (bs *BaseStore[T]) GetAll() map[int]*T {
|
|
||||||
bs.mu.RLock()
|
|
||||||
defer bs.mu.RUnlock()
|
|
||||||
result := make(map[int]*T, len(bs.items))
|
|
||||||
maps.Copy(result, bs.items)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all items
|
|
||||||
func (bs *BaseStore[T]) Clear() {
|
|
||||||
bs.mu.Lock()
|
|
||||||
defer bs.mu.Unlock()
|
|
||||||
bs.items = make(map[int]*T)
|
|
||||||
bs.maxID = 0
|
|
||||||
bs.rebuildIndicesUnsafe()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadFromJSON loads items from JSON using reflection
|
|
||||||
func (bs *BaseStore[T]) LoadFromJSON(filename string) error {
|
|
||||||
bs.mu.Lock()
|
|
||||||
defer bs.mu.Unlock()
|
|
||||||
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to read JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create slice of pointers to T
|
|
||||||
sliceType := reflect.SliceOf(reflect.PointerTo(bs.itemType))
|
|
||||||
slicePtr := reflect.New(sliceType)
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, slicePtr.Interface()); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear existing data
|
|
||||||
bs.items = make(map[int]*T)
|
|
||||||
bs.maxID = 0
|
|
||||||
|
|
||||||
// Extract items using reflection
|
|
||||||
slice := slicePtr.Elem()
|
|
||||||
for i := 0; i < slice.Len(); i++ {
|
|
||||||
item := slice.Index(i).Interface().(*T)
|
|
||||||
|
|
||||||
// Get ID using reflection
|
|
||||||
itemValue := reflect.ValueOf(item).Elem()
|
|
||||||
idField := itemValue.FieldByName("ID")
|
|
||||||
if !idField.IsValid() {
|
|
||||||
return fmt.Errorf("item type must have an ID field")
|
|
||||||
}
|
|
||||||
|
|
||||||
id := int(idField.Int())
|
|
||||||
bs.items[id] = item
|
|
||||||
if id > bs.maxID {
|
|
||||||
bs.maxID = id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveToJSON saves items to JSON atomically with consistent ID ordering
|
|
||||||
func (bs *BaseStore[T]) SaveToJSON(filename string) error {
|
|
||||||
bs.mu.RLock()
|
|
||||||
defer bs.mu.RUnlock()
|
|
||||||
|
|
||||||
// Get sorted IDs for consistent ordering
|
|
||||||
ids := make([]int, 0, len(bs.items))
|
|
||||||
for id := range bs.items {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
sort.Ints(ids)
|
|
||||||
|
|
||||||
// Build items slice in ID order
|
|
||||||
items := make([]*T, 0, len(bs.items))
|
|
||||||
for _, id := range ids {
|
|
||||||
items = append(items, bs.items[id])
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.MarshalIndent(items, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal to JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomic write
|
|
||||||
tempFile := filename + ".tmp"
|
|
||||||
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write temp JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Rename(tempFile, filename); err != nil {
|
|
||||||
os.Remove(tempFile)
|
|
||||||
return fmt.Errorf("failed to rename temp JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadData loads from JSON file or starts empty
|
|
||||||
func (bs *BaseStore[T]) LoadData(dataPath string) error {
|
|
||||||
if err := bs.LoadFromJSON(dataPath); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
fmt.Println("No existing data found, starting with empty store")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to load from JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Loaded %d items from %s\n", len(bs.items), dataPath)
|
|
||||||
bs.RebuildIndices() // Rebuild indices after loading
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveData saves to JSON file
|
|
||||||
func (bs *BaseStore[T]) SaveData(dataPath string) error {
|
|
||||||
// Ensure directory exists
|
|
||||||
dataDir := filepath.Dir(dataPath)
|
|
||||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create data directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bs.SaveToJSON(dataPath); err != nil {
|
|
||||||
return fmt.Errorf("failed to save to JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Saved %d items to %s\n", len(bs.items), dataPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildFilteredIntGroupIndex creates int-to-[]ID mapping for items passing filter
|
|
||||||
func BuildFilteredIntGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) int) IndexBuilder[T] {
|
|
||||||
return func(allItems map[int]*T) any {
|
|
||||||
index := make(map[int][]int)
|
|
||||||
for id, item := range allItems {
|
|
||||||
if filterFunc(item) {
|
|
||||||
key := keyFunc(item)
|
|
||||||
index[key] = append(index[key], id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort each group by ID
|
|
||||||
for key := range index {
|
|
||||||
sort.Ints(index[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildFilteredStringGroupIndex creates string-to-[]ID mapping for items passing filter
|
|
||||||
func BuildFilteredStringGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) string) IndexBuilder[T] {
|
|
||||||
return func(allItems map[int]*T) any {
|
|
||||||
index := make(map[string][]int)
|
|
||||||
for id, item := range allItems {
|
|
||||||
if filterFunc(item) {
|
|
||||||
key := keyFunc(item)
|
|
||||||
index[key] = append(index[key], id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort each group by ID
|
|
||||||
for key := range index {
|
|
||||||
sort.Ints(index[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Template struct {
|
type Template struct {
|
||||||
@ -43,14 +43,12 @@ func (t *Template) RenderNamed(data map[string]any) string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Template) WriteTo(ctx *fasthttp.RequestCtx, data any) {
|
func (t *Template) Render(data any) string {
|
||||||
var result string
|
|
||||||
|
|
||||||
switch v := data.(type) {
|
switch v := data.(type) {
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
result = t.RenderNamed(v)
|
return t.RenderNamed(v)
|
||||||
case []any:
|
case []any:
|
||||||
result = t.RenderPositional(v...)
|
return t.RenderPositional(v...)
|
||||||
default:
|
default:
|
||||||
rv := reflect.ValueOf(data)
|
rv := reflect.ValueOf(data)
|
||||||
if rv.Kind() == reflect.Slice {
|
if rv.Kind() == reflect.Slice {
|
||||||
@ -58,14 +56,17 @@ func (t *Template) WriteTo(ctx *fasthttp.RequestCtx, data any) {
|
|||||||
for i := 0; i < rv.Len(); i++ {
|
for i := 0; i < rv.Len(); i++ {
|
||||||
args[i] = rv.Index(i).Interface()
|
args[i] = rv.Index(i).Interface()
|
||||||
}
|
}
|
||||||
result = t.RenderPositional(args...)
|
return t.RenderPositional(args...)
|
||||||
} else {
|
} else {
|
||||||
result = t.RenderPositional(data)
|
return t.RenderPositional(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.SetContentType("text/html; charset=utf-8")
|
func (t *Template) WriteTo(ctx sushi.Ctx, data any) error {
|
||||||
ctx.WriteString(result)
|
result := t.Render(data)
|
||||||
|
ctx.SendHTML(result)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Template) processBlocks(content string, blocks map[string]string) string {
|
func (t *Template) processBlocks(content string, blocks map[string]string) string {
|
||||||
|
233
main.go
233
main.go
@ -9,9 +9,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"dk/internal/auth"
|
|
||||||
"dk/internal/csrf"
|
|
||||||
"dk/internal/middleware"
|
|
||||||
"dk/internal/models/babble"
|
"dk/internal/models/babble"
|
||||||
"dk/internal/models/control"
|
"dk/internal/models/control"
|
||||||
"dk/internal/models/drops"
|
"dk/internal/models/drops"
|
||||||
@ -23,12 +20,15 @@ import (
|
|||||||
"dk/internal/models/spells"
|
"dk/internal/models/spells"
|
||||||
"dk/internal/models/towns"
|
"dk/internal/models/towns"
|
||||||
"dk/internal/models/users"
|
"dk/internal/models/users"
|
||||||
"dk/internal/router"
|
|
||||||
"dk/internal/routes"
|
"dk/internal/routes"
|
||||||
"dk/internal/session"
|
|
||||||
"dk/internal/template"
|
"dk/internal/template"
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||||
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/auth"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/csrf"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/session"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/timing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -53,106 +53,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadModels(dataDir string) error {
|
|
||||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create data directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := users.LoadData(filepath.Join(dataDir, "users.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load users data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := towns.LoadData(filepath.Join(dataDir, "towns.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load towns data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := spells.LoadData(filepath.Join(dataDir, "spells.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load spells data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := news.LoadData(filepath.Join(dataDir, "news.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load news data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := monsters.LoadData(filepath.Join(dataDir, "monsters.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load monsters data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := items.LoadData(filepath.Join(dataDir, "items.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load items data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := forum.LoadData(filepath.Join(dataDir, "forum.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load forum data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := drops.LoadData(filepath.Join(dataDir, "drops.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load drops data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := babble.LoadData(filepath.Join(dataDir, "babble.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load babble data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := control.Load(filepath.Join(dataDir, "control.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load control data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fights.LoadData(filepath.Join(dataDir, "fights.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to load fights data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveModels(dataDir string) error {
|
|
||||||
if err := users.SaveData(filepath.Join(dataDir, "users.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save users data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := towns.SaveData(filepath.Join(dataDir, "towns.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save towns data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := spells.SaveData(filepath.Join(dataDir, "spells.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save spells data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := news.SaveData(filepath.Join(dataDir, "news.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save news data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := monsters.SaveData(filepath.Join(dataDir, "monsters.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save monsters data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := items.SaveData(filepath.Join(dataDir, "items.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save items data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := forum.SaveData(filepath.Join(dataDir, "forum.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save forum data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := drops.SaveData(filepath.Join(dataDir, "drops.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save drops data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := babble.SaveData(filepath.Join(dataDir, "babble.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save babble data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := control.Save(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save control data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fights.SaveData(filepath.Join(dataDir, "fights.json")); err != nil {
|
|
||||||
return fmt.Errorf("failed to save fights data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startServer(port string) {
|
func startServer(port string) {
|
||||||
fmt.Println("Starting Dragon Knight server...")
|
fmt.Println("Starting Dragon Knight server...")
|
||||||
if err := start(port); err != nil {
|
if err := start(port); err != nil {
|
||||||
@ -168,94 +68,85 @@ func start(port string) error {
|
|||||||
|
|
||||||
template.InitializeCache(cwd)
|
template.InitializeCache(cwd)
|
||||||
|
|
||||||
if err := loadModels(filepath.Join(cwd, "data")); err != nil {
|
db := nigiri.NewCollection(filepath.Join(cwd, "data"))
|
||||||
return fmt.Errorf("failed to load models: %w", err)
|
if err := setupStores(db); err != nil {
|
||||||
|
return fmt.Errorf("failed to setup Nigiri stores: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
session.Init(filepath.Join(cwd, "data/_sessions.json"))
|
app := sushi.New()
|
||||||
|
sushi.InitSessions(filepath.Join(cwd, "data/_sessions.json"))
|
||||||
|
|
||||||
r := router.New()
|
app.Use(session.Middleware())
|
||||||
r.Use(middleware.Timing())
|
app.Use(auth.Middleware(getUserByID))
|
||||||
r.Use(auth.Middleware())
|
app.Use(csrf.Middleware())
|
||||||
r.Use(csrf.Middleware())
|
app.Use(timing.Middleware())
|
||||||
|
|
||||||
r.Get("/", routes.Index)
|
app.Get("/", routes.Index)
|
||||||
|
|
||||||
actions := r.Group("")
|
protected := app.Group("")
|
||||||
actions.Use(auth.RequireAuth())
|
protected.Use(auth.RequireAuth("/login"))
|
||||||
actions.Get("/explore", routes.Explore)
|
protected.Get("/explore", routes.Explore)
|
||||||
actions.Post("/move", routes.Move)
|
protected.Post("/move", routes.Move)
|
||||||
actions.Get("/teleport/:to", routes.Teleport)
|
protected.Get("/teleport/:to", routes.Teleport)
|
||||||
|
|
||||||
routes.RegisterAuthRoutes(r)
|
routes.RegisterAuthRoutes(app)
|
||||||
routes.RegisterTownRoutes(r)
|
routes.RegisterTownRoutes(app)
|
||||||
routes.RegisterFightRoutes(r)
|
routes.RegisterFightRoutes(app)
|
||||||
|
|
||||||
// Use current working directory for static files
|
app.Get("/assets/*path", sushi.Static(cwd))
|
||||||
assetsDir := filepath.Join(cwd, "assets")
|
|
||||||
|
|
||||||
// Static file server for /assets
|
|
||||||
fs := &fasthttp.FS{
|
|
||||||
Root: assetsDir,
|
|
||||||
Compress: false,
|
|
||||||
}
|
|
||||||
assetsHandler := fs.NewRequestHandler()
|
|
||||||
|
|
||||||
// Combined handler
|
|
||||||
requestHandler := func(ctx *fasthttp.RequestCtx) {
|
|
||||||
path := string(ctx.Path())
|
|
||||||
|
|
||||||
// Handle static assets - strip /assets prefix
|
|
||||||
if len(path) >= 7 && path[:7] == "/assets" {
|
|
||||||
// Strip the /assets prefix for the file system handler
|
|
||||||
originalPath := ctx.Path()
|
|
||||||
ctx.Request.URI().SetPath(path[7:]) // Remove "/assets" prefix
|
|
||||||
assetsHandler(ctx)
|
|
||||||
ctx.Request.URI().SetPathBytes(originalPath) // Restore original path
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle routes
|
|
||||||
r.ServeHTTP(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := ":" + port
|
addr := ":" + port
|
||||||
log.Printf("Server starting on %s", addr)
|
log.Printf("Server starting on %s", addr)
|
||||||
|
|
||||||
// Setup graceful shutdown
|
|
||||||
server := &fasthttp.Server{
|
|
||||||
Handler: requestHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel to listen for interrupt signal
|
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
// Start server in a goroutine
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := server.ListenAndServe(addr); err != nil {
|
app.Listen(addr)
|
||||||
log.Printf("Server error: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for interrupt signal
|
|
||||||
<-c
|
<-c
|
||||||
log.Println("Received shutdown signal, shutting down gracefully...")
|
log.Println("\nReceived shutdown signal, shutting down gracefully...")
|
||||||
|
|
||||||
// Save all model data before shutdown
|
log.Println("Saving database...")
|
||||||
log.Println("Saving model data...")
|
if err := db.Save(); err != nil {
|
||||||
if err := saveModels(filepath.Join(cwd, "data")); err != nil {
|
log.Printf("Error saving database: %v", err)
|
||||||
log.Printf("Error saving model data: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save sessions before shutdown
|
|
||||||
log.Println("Saving sessions...")
|
log.Println("Saving sessions...")
|
||||||
if err := session.Close(); err != nil {
|
sushi.SaveSessions()
|
||||||
log.Printf("Error saving sessions: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FastHTTP doesn't have a graceful Shutdown method like net/http
|
|
||||||
// We just let the server stop naturally when the main function exits
|
|
||||||
log.Println("Server stopped")
|
log.Println("Server stopped")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupStores(db *nigiri.Collection) error {
|
||||||
|
users.Init(db)
|
||||||
|
towns.Init(db)
|
||||||
|
spells.Init(db)
|
||||||
|
news.Init(db)
|
||||||
|
monsters.Init(db)
|
||||||
|
items.Init(db)
|
||||||
|
forum.Init(db)
|
||||||
|
drops.Init(db)
|
||||||
|
babble.Init(db)
|
||||||
|
fights.Init(db)
|
||||||
|
control.Init(db)
|
||||||
|
|
||||||
|
db.Add("users", users.GetStore())
|
||||||
|
db.Add("towns", towns.GetStore())
|
||||||
|
db.Add("spells", spells.GetStore())
|
||||||
|
db.Add("news", news.GetStore())
|
||||||
|
db.Add("monsters", monsters.GetStore())
|
||||||
|
db.Add("items", items.GetStore())
|
||||||
|
db.Add("forum", forum.GetStore())
|
||||||
|
db.Add("drops", drops.GetStore())
|
||||||
|
db.Add("babble", babble.GetStore())
|
||||||
|
db.Add("fights", fights.GetStore())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserByID(userID int) any {
|
||||||
|
return users.GetByID(userID)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user