refactor sessions, auth, cookies, etc.

This commit is contained in:
Sky Johnson 2025-08-14 12:13:23 -05:00
parent c083c55212
commit 7b99ffc02c
19 changed files with 2374 additions and 2328 deletions

View File

@ -1,109 +1,4 @@
[
{
"id": 31,
"name": "Memory Drop",
"level": 5,
"type": 1,
"att": "expbonus,10"
},
{
"id": 25,
"name": "Ruby",
"level": 50,
"type": 1,
"att": "maxhp,150"
},
{
"id": 23,
"name": "Seraph's Truth",
"level": 35,
"type": 1,
"att": "maxmp,75,dexterity,75"
},
{
"id": 24,
"name": "Seraph's Love",
"level": 40,
"type": 1,
"att": "maxmp,100,dexterity,100"
},
{
"id": 11,
"name": "Dragon's Tear",
"level": 35,
"type": 1,
"att": "strength,50"
},
{
"id": 20,
"name": "Angel's Love",
"level": 40,
"type": 1,
"att": "maxhp,100,strength,100"
},
{
"id": 18,
"name": "Angel's Rise",
"level": 30,
"type": 1,
"att": "maxhp,50,strength,50"
},
{
"id": 8,
"name": "Dragon's Plate",
"level": 30,
"type": 1,
"att": "defensepower,50"
},
{
"id": 5,
"name": "Magic Stone",
"level": 10,
"type": 1,
"att": "maxmp,25"
},
{
"id": 27,
"name": "Emerald",
"level": 50,
"type": 1,
"att": "strength,150"
},
{
"id": 26,
"name": "Pearl",
"level": 50,
"type": 1,
"att": "maxmp,150"
},
{
"id": 14,
"name": "Demon's Fall",
"level": 35,
"type": 1,
"att": "maxmp,-50,strength,50"
},
{
"id": 17,
"name": "Angel's Joy",
"level": 25,
"type": 1,
"att": "maxhp,25,strength,25"
},
{
"id": 7,
"name": "Dragon's Scale",
"level": 10,
"type": 1,
"att": "defensepower,25"
},
{
"id": 12,
"name": "Dragon's Wing",
"level": 35,
"type": 1,
"att": "dexterity,50"
},
{
"id": 1,
"name": "Life Pebble",
@ -111,27 +6,6 @@
"type": 1,
"att": "maxhp,10"
},
{
"id": 4,
"name": "Magic Pebble",
"level": 1,
"type": 1,
"att": "maxmp,10"
},
{
"id": 22,
"name": "Seraph's Rise",
"level": 30,
"type": 1,
"att": "maxmp,50,dexterity,50"
},
{
"id": 29,
"name": "Obsidian",
"level": 50,
"type": 1,
"att": "attackpower,150"
},
{
"id": 32,
"name": "Fortune Drop",
@ -139,27 +13,6 @@
"type": 1,
"att": "goldbonus,10"
},
{
"id": 30,
"name": "Diamond",
"level": 50,
"type": 1,
"att": "defensepower,150"
},
{
"id": 15,
"name": "Demon's Lie",
"level": 45,
"type": 1,
"att": "maxhp,-100,strength,100"
},
{
"id": 9,
"name": "Dragon's Claw",
"level": 10,
"type": 1,
"att": "attackpower,25"
},
{
"id": 28,
"name": "Topaz",
@ -168,46 +21,18 @@
"att": "dexterity,150"
},
{
"id": 6,
"name": "Magic Rock",
"level": 25,
"id": 26,
"name": "Pearl",
"level": 50,
"type": 1,
"att": "maxmp,50"
"att": "maxmp,150"
},
{
"id": 21,
"name": "Seraph's Joy",
"level": 25,
"type": 1,
"att": "maxmp,25,dexterity,25"
},
{
"id": 3,
"name": "Life Rock",
"level": 25,
"type": 1,
"att": "maxhp,50"
},
{
"id": 19,
"name": "Angel's Truth",
"level": 35,
"type": 1,
"att": "maxhp,75,strength,75"
},
{
"id": 13,
"name": "Demon's Sin",
"level": 35,
"type": 1,
"att": "maxhp,-50,strength,50"
},
{
"id": 2,
"name": "Life Stone",
"id": 7,
"name": "Dragon's Scale",
"level": 10,
"type": 1,
"att": "maxhp,25"
"att": "defensepower,25"
},
{
"id": 10,
@ -216,11 +41,186 @@
"type": 1,
"att": "attackpower,50"
},
{
"id": 6,
"name": "Magic Rock",
"level": 25,
"type": 1,
"att": "maxmp,50"
},
{
"id": 19,
"name": "Angel's Truth",
"level": 35,
"type": 1,
"att": "maxhp,75,strength,75"
},
{
"id": 20,
"name": "Angel's Love",
"level": 40,
"type": 1,
"att": "maxhp,100,strength,100"
},
{
"id": 8,
"name": "Dragon's Plate",
"level": 30,
"type": 1,
"att": "defensepower,50"
},
{
"id": 21,
"name": "Seraph's Joy",
"level": 25,
"type": 1,
"att": "maxmp,25,dexterity,25"
},
{
"id": 24,
"name": "Seraph's Love",
"level": 40,
"type": 1,
"att": "maxmp,100,dexterity,100"
},
{
"id": 18,
"name": "Angel's Rise",
"level": 30,
"type": 1,
"att": "maxhp,50,strength,50"
},
{
"id": 12,
"name": "Dragon's Wing",
"level": 35,
"type": 1,
"att": "dexterity,50"
},
{
"id": 15,
"name": "Demon's Lie",
"level": 45,
"type": 1,
"att": "maxhp,-100,strength,100"
},
{
"id": 11,
"name": "Dragon's Tear",
"level": 35,
"type": 1,
"att": "strength,50"
},
{
"id": 5,
"name": "Magic Stone",
"level": 10,
"type": 1,
"att": "maxmp,25"
},
{
"id": 14,
"name": "Demon's Fall",
"level": 35,
"type": 1,
"att": "maxmp,-50,strength,50"
},
{
"id": 29,
"name": "Obsidian",
"level": 50,
"type": 1,
"att": "attackpower,150"
},
{
"id": 17,
"name": "Angel's Joy",
"level": 25,
"type": 1,
"att": "maxhp,25,strength,25"
},
{
"id": 27,
"name": "Emerald",
"level": 50,
"type": 1,
"att": "strength,150"
},
{
"id": 13,
"name": "Demon's Sin",
"level": 35,
"type": 1,
"att": "maxhp,-50,strength,50"
},
{
"id": 9,
"name": "Dragon's Claw",
"level": 10,
"type": 1,
"att": "attackpower,25"
},
{
"id": 31,
"name": "Memory Drop",
"level": 5,
"type": 1,
"att": "expbonus,10"
},
{
"id": 4,
"name": "Magic Pebble",
"level": 1,
"type": 1,
"att": "maxmp,10"
},
{
"id": 2,
"name": "Life Stone",
"level": 10,
"type": 1,
"att": "maxhp,25"
},
{
"id": 25,
"name": "Ruby",
"level": 50,
"type": 1,
"att": "maxhp,150"
},
{
"id": 30,
"name": "Diamond",
"level": 50,
"type": 1,
"att": "defensepower,150"
},
{
"id": 3,
"name": "Life Rock",
"level": 25,
"type": 1,
"att": "maxhp,50"
},
{
"id": 23,
"name": "Seraph's Truth",
"level": 35,
"type": 1,
"att": "maxmp,75,dexterity,75"
},
{
"id": 16,
"name": "Demon's Hate",
"level": 45,
"type": 1,
"att": "maxmp,-100,strength,100"
},
{
"id": 22,
"name": "Seraph's Rise",
"level": 30,
"type": 1,
"att": "maxmp,50,dexterity,50"
}
]

View File

@ -1,28 +1,4 @@
[
{
"id": 3,
"type": 1,
"name": "Club",
"value": 40,
"att": 5,
"special": ""
},
{
"id": 5,
"type": 1,
"name": "Hatchet",
"value": 150,
"att": 12,
"special": ""
},
{
"id": 33,
"type": 3,
"name": "Destiny Aegis",
"value": 25000,
"att": 100,
"special": "maxhp,50"
},
{
"id": 10,
"type": 1,
@ -32,66 +8,26 @@
"special": ""
},
{
"id": 11,
"id": 16,
"type": 1,
"name": "Claymore",
"value": 2000,
"att": 60,
"special": ""
"name": "Destiny Blade",
"value": 50000,
"att": 250,
"special": "strength,50"
},
{
"id": 21,
"id": 26,
"type": 2,
"name": "Chain Mail",
"value": 300,
"att": 30,
"special": ""
},
{
"id": 14,
"type": 1,
"name": "Bright Sword",
"value": 6000,
"att": 100,
"name": "Bright Armor",
"value": 10000,
"att": 175,
"special": "expbonus,10"
},
{
"id": 13,
"id": 7,
"type": 1,
"name": "Dark Sword",
"value": 4500,
"att": 125,
"special": "expbonus,-10"
},
{
"id": 9,
"type": 1,
"name": "Broadsword",
"value": 800,
"att": 45,
"special": ""
},
{
"id": 30,
"type": 3,
"name": "Small Shield",
"value": 500,
"att": 10,
"special": ""
},
{
"id": 15,
"type": 1,
"name": "Magic Sword",
"value": 10000,
"att": 150,
"special": "maxmp,50"
},
{
"id": 20,
"type": 2,
"name": "Hard Leather Armor",
"value": 150,
"name": "Brand",
"value": 300,
"att": 25,
"special": ""
},
@ -104,19 +40,19 @@
"special": ""
},
{
"id": 26,
"type": 2,
"name": "Bright Armor",
"value": 10000,
"att": 175,
"special": "expbonus,10"
"id": 13,
"type": 1,
"name": "Dark Sword",
"value": 4500,
"att": 125,
"special": "expbonus,-10"
},
{
"id": 29,
"type": 3,
"name": "Buckler",
"value": 100,
"att": 4,
"id": 3,
"type": 1,
"name": "Club",
"value": 40,
"att": 5,
"special": ""
},
{
@ -128,51 +64,11 @@
"special": "maxmp,50"
},
{
"id": 28,
"type": 3,
"name": "Reed Shield",
"value": 50,
"att": 2,
"special": ""
},
{
"id": 8,
"type": 1,
"name": "Poleaxe",
"value": 500,
"att": 35,
"special": ""
},
{
"id": 12,
"type": 1,
"name": "Dark Axe",
"value": 3000,
"att": 100,
"special": "expbonus,-5"
},
{
"id": 2,
"type": 1,
"name": "Branch",
"value": 30,
"att": 4,
"special": ""
},
{
"id": 17,
"id": 21,
"type": 2,
"name": "Skivvies",
"value": 25,
"att": 2,
"special": "goldbonus,10"
},
{
"id": 32,
"type": 3,
"name": "Silver Shield",
"value": 10000,
"att": 60,
"name": "Chain Mail",
"value": 300,
"att": 30,
"special": ""
},
{
@ -183,6 +79,14 @@
"att": 2,
"special": ""
},
{
"id": 30,
"type": 3,
"name": "Small Shield",
"value": 500,
"att": 10,
"special": ""
},
{
"id": 31,
"type": 3,
@ -192,11 +96,123 @@
"special": ""
},
{
"id": 23,
"id": 4,
"type": 1,
"name": "Dagger",
"value": 90,
"att": 8,
"special": ""
},
{
"id": 8,
"type": 1,
"name": "Poleaxe",
"value": 500,
"att": 35,
"special": ""
},
{
"id": 5,
"type": 1,
"name": "Hatchet",
"value": 150,
"att": 12,
"special": ""
},
{
"id": 22,
"type": 2,
"name": "Iron Plate",
"value": 2000,
"name": "Bronze Plate",
"value": 900,
"att": 50,
"special": ""
},
{
"id": 17,
"type": 2,
"name": "Skivvies",
"value": 25,
"att": 2,
"special": "goldbonus,10"
},
{
"id": 27,
"type": 2,
"name": "Destiny Raiment",
"value": 50000,
"att": 200,
"special": "dexterity,50"
},
{
"id": 15,
"type": 1,
"name": "Magic Sword",
"value": 10000,
"att": 150,
"special": "maxmp,50"
},
{
"id": 9,
"type": 1,
"name": "Broadsword",
"value": 800,
"att": 45,
"special": ""
},
{
"id": 29,
"type": 3,
"name": "Buckler",
"value": 100,
"att": 4,
"special": ""
},
{
"id": 12,
"type": 1,
"name": "Dark Axe",
"value": 3000,
"att": 100,
"special": "expbonus,-5"
},
{
"id": 33,
"type": 3,
"name": "Destiny Aegis",
"value": 25000,
"att": 100,
"special": "maxhp,50"
},
{
"id": 6,
"type": 1,
"name": "Axe",
"value": 200,
"att": 16,
"special": ""
},
{
"id": 20,
"type": 2,
"name": "Hard Leather Armor",
"value": 150,
"att": 25,
"special": ""
},
{
"id": 11,
"type": 1,
"name": "Claymore",
"value": 2000,
"att": 60,
"special": ""
},
{
"id": 2,
"type": 1,
"name": "Branch",
"value": 30,
"att": 4,
"special": ""
},
{
@ -207,6 +223,38 @@
"att": 150,
"special": "expbonus,-10"
},
{
"id": 14,
"type": 1,
"name": "Bright Sword",
"value": 6000,
"att": 100,
"special": "expbonus,10"
},
{
"id": 23,
"type": 2,
"name": "Iron Plate",
"value": 2000,
"att": 100,
"special": ""
},
{
"id": 32,
"type": 3,
"name": "Silver Shield",
"value": 10000,
"att": 60,
"special": ""
},
{
"id": 28,
"type": 3,
"name": "Reed Shield",
"value": 50,
"att": 2,
"special": ""
},
{
"id": 18,
"type": 2,
@ -214,53 +262,5 @@
"value": 50,
"att": 5,
"special": ""
},
{
"id": 16,
"type": 1,
"name": "Destiny Blade",
"value": 50000,
"att": 250,
"special": "strength,50"
},
{
"id": 6,
"type": 1,
"name": "Axe",
"value": 200,
"att": 16,
"special": ""
},
{
"id": 27,
"type": 2,
"name": "Destiny Raiment",
"value": 50000,
"att": 200,
"special": "dexterity,50"
},
{
"id": 4,
"type": 1,
"name": "Dagger",
"value": 90,
"att": 8,
"special": ""
},
{
"id": 7,
"type": 1,
"name": "Brand",
"value": 300,
"att": 25,
"special": ""
},
{
"id": 22,
"type": 2,
"name": "Bronze Plate",
"value": 900,
"att": 50,
"special": ""
}
]

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,4 @@
[
{
"id": 13,
"name": "Nightmare",
"mp": 60,
"attribute": 13,
"type": 3
},
{
"id": 16,
"name": "Fury",
"mp": 30,
"attribute": 50,
"type": 4
},
{
"id": 7,
"name": "Pain",
"mp": 12,
"attribute": 35,
"type": 2
},
{
"id": 4,
"name": "Breath",
@ -27,97 +6,6 @@
"attribute": 100,
"type": 1
},
{
"id": 2,
"name": "Revive",
"mp": 10,
"attribute": 25,
"type": 1
},
{
"id": 15,
"name": "Rage",
"mp": 20,
"attribute": 25,
"type": 4
},
{
"id": 11,
"name": "Sleep",
"mp": 10,
"attribute": 5,
"type": 3
},
{
"id": 10,
"name": "Chaos",
"mp": 50,
"attribute": 130,
"type": 2
},
{
"id": 8,
"name": "Maim",
"mp": 25,
"attribute": 70,
"type": 2
},
{
"id": 6,
"name": "Hurt",
"mp": 5,
"attribute": 15,
"type": 2
},
{
"id": 17,
"name": "Ward",
"mp": 10,
"attribute": 10,
"type": 5
},
{
"id": 12,
"name": "Dream",
"mp": 30,
"attribute": 9,
"type": 3
},
{
"id": 19,
"name": "Barrier",
"mp": 30,
"attribute": 50,
"type": 5
},
{
"id": 5,
"name": "Gaia",
"mp": 75,
"attribute": 150,
"type": 1
},
{
"id": 3,
"name": "Life",
"mp": 25,
"attribute": 50,
"type": 1
},
{
"id": 1,
"name": "Heal",
"mp": 5,
"attribute": 10,
"type": 1
},
{
"id": 9,
"name": "Rend",
"mp": 40,
"attribute": 100,
"type": 2
},
{
"id": 14,
"name": "Craze",
@ -131,5 +19,117 @@
"mp": 20,
"attribute": 25,
"type": 5
},
{
"id": 5,
"name": "Gaia",
"mp": 75,
"attribute": 150,
"type": 1
},
{
"id": 10,
"name": "Chaos",
"mp": 50,
"attribute": 130,
"type": 2
},
{
"id": 6,
"name": "Hurt",
"mp": 5,
"attribute": 15,
"type": 2
},
{
"id": 3,
"name": "Life",
"mp": 25,
"attribute": 50,
"type": 1
},
{
"id": 8,
"name": "Maim",
"mp": 25,
"attribute": 70,
"type": 2
},
{
"id": 15,
"name": "Rage",
"mp": 20,
"attribute": 25,
"type": 4
},
{
"id": 19,
"name": "Barrier",
"mp": 30,
"attribute": 50,
"type": 5
},
{
"id": 1,
"name": "Heal",
"mp": 5,
"attribute": 10,
"type": 1
},
{
"id": 12,
"name": "Dream",
"mp": 30,
"attribute": 9,
"type": 3
},
{
"id": 7,
"name": "Pain",
"mp": 12,
"attribute": 35,
"type": 2
},
{
"id": 17,
"name": "Ward",
"mp": 10,
"attribute": 10,
"type": 5
},
{
"id": 9,
"name": "Rend",
"mp": 40,
"attribute": 100,
"type": 2
},
{
"id": 13,
"name": "Nightmare",
"mp": 60,
"attribute": 13,
"type": 3
},
{
"id": 2,
"name": "Revive",
"mp": 10,
"attribute": 25,
"type": 1
},
{
"id": 11,
"name": "Sleep",
"mp": 10,
"attribute": 5,
"type": 3
},
{
"id": 16,
"name": "Fury",
"mp": 30,
"attribute": 50,
"type": 4
}
]

View File

@ -1,4 +1,34 @@
[
{
"id": 1,
"name": "Midworld",
"x": 0,
"y": 0,
"inn_cost": 5,
"map_cost": 0,
"tp_cost": 0,
"shop_list": "1,2,3,17,18,19,28,29"
},
{
"id": 2,
"name": "Roma",
"x": 30,
"y": 30,
"inn_cost": 10,
"map_cost": 25,
"tp_cost": 5,
"shop_list": "2,3,4,18,19,29"
},
{
"id": 3,
"name": "Bris",
"x": 70,
"y": -70,
"inn_cost": 25,
"map_cost": 50,
"tp_cost": 15,
"shop_list": "2,3,4,5,18,19,20,29.30"
},
{
"id": 4,
"name": "Kalle",
@ -48,35 +78,5 @@
"map_cost": 9000,
"tp_cost": 160,
"shop_list": "16,27,33"
},
{
"id": 1,
"name": "Midworld",
"x": 0,
"y": 0,
"inn_cost": 5,
"map_cost": 0,
"tp_cost": 0,
"shop_list": "1,2,3,17,18,19,28,29"
},
{
"id": 2,
"name": "Roma",
"x": 30,
"y": 30,
"inn_cost": 10,
"map_cost": 25,
"tp_cost": 5,
"shop_list": "2,3,4,18,19,29"
},
{
"id": 3,
"name": "Bris",
"x": 70,
"y": -70,
"inn_cost": 25,
"map_cost": 50,
"tp_cost": 15,
"shop_list": "2,3,4,5,18,19,20,29.30"
}
]

View File

@ -1,27 +1,13 @@
// Package auth provides authentication and session management functionality.
// It includes secure session storage with in-memory caching and JSON persistence,
// user authentication against the database, and secure cookie handling.
// Package auth provides authentication functionality.
// It handles user authentication against the database and password verification.
package auth
import (
"dk/internal/models/users"
"dk/internal/password"
"dk/internal/session"
)
var Manager *AuthManager
type AuthManager struct {
store *session.Store
}
func Init(sessionsFilePath string) {
Manager = &AuthManager{
store: session.NewStore(sessionsFilePath),
}
}
func (am *AuthManager) Authenticate(usernameOrEmail, plainPassword string) (*users.User, error) {
func Authenticate(usernameOrEmail, plainPassword string) (*users.User, error) {
var user *users.User
var err error
@ -44,43 +30,8 @@ func (am *AuthManager) Authenticate(usernameOrEmail, plainPassword string) (*use
return user, nil
}
func (am *AuthManager) CreateSession(user *users.User) *session.Session {
sess := session.New(user.ID, user.Username, user.Email)
am.store.Save(sess)
return sess
}
func (am *AuthManager) GetSession(sessionID string) (*session.Session, bool) {
return am.store.Get(sessionID)
}
func (am *AuthManager) UpdateSession(sessionID string) bool {
sess, exists := am.store.Get(sessionID)
if !exists {
return false
}
sess.Touch()
am.store.Save(sess)
return true
}
func (am *AuthManager) DeleteSession(sessionID string) {
am.store.Delete(sessionID)
}
func (am *AuthManager) SessionStats() (total, active int) {
return am.store.Stats()
}
func (am *AuthManager) Close() error {
return am.store.Close()
}
var (
ErrInvalidCredentials = &AuthError{"invalid username/email or password"}
ErrSessionNotFound = &AuthError{"session not found"}
ErrSessionExpired = &AuthError{"session expired"}
)
type AuthError struct {

View File

@ -1,32 +0,0 @@
package auth
import (
"dk/internal/cookies"
"dk/internal/helpers"
"dk/internal/session"
"time"
"github.com/valyala/fasthttp"
)
const SessionCookieName = "dk_session"
func SetSessionCookie(ctx *fasthttp.RequestCtx, sessionID string) {
cookies.SetSecureCookie(ctx, cookies.CookieOptions{
Name: SessionCookieName,
Value: sessionID,
Path: "/",
Expires: time.Now().Add(session.DefaultExpiration),
HTTPOnly: true,
Secure: helpers.IsHTTPS(ctx),
SameSite: "lax",
})
}
func GetSessionCookie(ctx *fasthttp.RequestCtx) string {
return cookies.GetCookie(ctx, SessionCookieName)
}
func DeleteSessionCookie(ctx *fasthttp.RequestCtx) {
cookies.DeleteCookie(ctx, SessionCookieName)
}

View File

@ -1,113 +0,0 @@
package auth
import (
"dk/internal/router"
"dk/internal/session"
)
func SetFlashMessage(ctx router.Ctx, msgType, message string) bool {
sessionID := GetSessionCookie(ctx)
if sessionID == "" {
return false
}
sess, exists := Manager.GetSession(sessionID)
if !exists {
return false
}
sess.SetFlash("message", session.FlashMessage{
Type: msgType,
Message: message,
})
Manager.store.Save(sess)
return true
}
func GetFlashMessage(ctx router.Ctx) *session.FlashMessage {
sessionID := GetSessionCookie(ctx)
if sessionID == "" {
return nil
}
sess, exists := Manager.GetSession(sessionID)
if !exists {
return nil
}
value, exists := sess.GetFlash("message")
if !exists {
return nil
}
Manager.store.Save(sess)
if msg, ok := value.(session.FlashMessage); ok {
return &msg
}
if msgMap, ok := value.(map[string]interface{}); ok {
msg := &session.FlashMessage{}
if t, ok := msgMap["type"].(string); ok {
msg.Type = t
}
if m, ok := msgMap["message"].(string); ok {
msg.Message = m
}
return msg
}
return nil
}
func SetFormData(ctx router.Ctx, data map[string]string) bool {
sessionID := GetSessionCookie(ctx)
if sessionID == "" {
return false
}
sess, exists := Manager.GetSession(sessionID)
if !exists {
return false
}
sess.Set("form_data", data)
Manager.store.Save(sess)
return true
}
func GetFormData(ctx router.Ctx) map[string]string {
sessionID := GetSessionCookie(ctx)
if sessionID == "" {
return nil
}
sess, exists := Manager.GetSession(sessionID)
if !exists {
return nil
}
value, exists := sess.Get("form_data")
if !exists {
return nil
}
sess.Delete("form_data")
Manager.store.Save(sess)
if formData, ok := value.(map[string]string); ok {
return formData
}
if formMap, ok := value.(map[string]interface{}); ok {
result := make(map[string]string)
for k, v := range formMap {
if str, ok := v.(string); ok {
result[k] = str
}
}
return result
}
return nil
}

View File

@ -1,6 +1,7 @@
package cookies
import (
"bytes"
"time"
"github.com/valyala/fasthttp"
@ -75,3 +76,16 @@ func DeleteCookie(ctx *fasthttp.RequestCtx, name string) {
SameSite: "lax",
})
}
func IsHTTPS(ctx *fasthttp.RequestCtx) bool {
proto := string(ctx.Request.Header.Peek("X-Forwarded-Proto"))
if proto == "https" {
return true
}
if bytes.EqualFold(ctx.Request.URI().Scheme(), []byte("https")) {
return true
}
return ctx.IsTLS()
}

View File

@ -4,13 +4,13 @@
// # Basic Usage
//
// // Generate token and store in session
// token := csrf.GenerateToken(ctx, authManager)
// token := csrf.GenerateToken(ctx, sessionManager)
//
// // In templates - generate hidden input field
// hiddenField := csrf.HiddenField(ctx, authManager)
// hiddenField := csrf.HiddenField(ctx, sessionManager)
//
// // Verify form submission
// if !csrf.ValidateToken(ctx, authManager, formToken) {
// if !csrf.ValidateToken(ctx, sessionManager, formToken) {
// // Handle CSRF validation failure
// }
//
@ -27,7 +27,6 @@ import (
"fmt"
"time"
"dk/internal/auth"
"dk/internal/router"
"dk/internal/session"
@ -51,7 +50,7 @@ func GetCurrentSession(ctx router.Ctx) *session.Session {
}
// GenerateToken creates a new CSRF token and stores it in the session or cookie
func GenerateToken(ctx router.Ctx, authManager *auth.AuthManager) string {
func GenerateToken(ctx router.Ctx) string {
// Generate cryptographically secure random bytes
tokenBytes := make([]byte, TokenLength)
if _, err := rand.Read(tokenBytes); err != nil {
@ -73,7 +72,7 @@ func GenerateToken(ctx router.Ctx, authManager *auth.AuthManager) string {
}
// GetToken retrieves the current CSRF token from session or cookie, generating one if needed
func GetToken(ctx router.Ctx, authManager *auth.AuthManager) string {
func GetToken(ctx router.Ctx) string {
session := GetCurrentSession(ctx)
if session != nil {
@ -89,11 +88,11 @@ func GetToken(ctx router.Ctx, authManager *auth.AuthManager) string {
}
// Generate new token if none exists
return GenerateToken(ctx, authManager)
return GenerateToken(ctx)
}
// ValidateToken verifies a CSRF token against the stored session or cookie token
func ValidateToken(ctx router.Ctx, authManager *auth.AuthManager, submittedToken string) bool {
func ValidateToken(ctx router.Ctx, submittedToken string) bool {
if submittedToken == "" {
return false
}
@ -133,21 +132,21 @@ func GetStoredToken(sess *session.Session) string {
}
// RotateToken generates a new token and replaces the old one in the session
func RotateToken(ctx router.Ctx, authManager *auth.AuthManager) string {
func RotateToken(ctx router.Ctx) string {
session := GetCurrentSession(ctx)
if session == nil {
return ""
}
// Generate new token
newToken := GenerateToken(ctx, authManager)
newToken := GenerateToken(ctx)
return newToken
}
// HiddenField generates an HTML hidden input field with the CSRF token
func HiddenField(ctx router.Ctx, authManager *auth.AuthManager) string {
token := GetToken(ctx, authManager)
func HiddenField(ctx router.Ctx) string {
token := GetToken(ctx)
if token == "" {
return "" // No token available
}
@ -157,8 +156,8 @@ func HiddenField(ctx router.Ctx, authManager *auth.AuthManager) string {
}
// TokenMeta generates HTML meta tag for JavaScript access to CSRF token
func TokenMeta(ctx router.Ctx, authManager *auth.AuthManager) string {
token := GetToken(ctx, authManager)
func TokenMeta(ctx router.Ctx) string {
token := GetToken(ctx)
if token == "" {
return ""
}
@ -167,7 +166,7 @@ func TokenMeta(ctx router.Ctx, authManager *auth.AuthManager) string {
}
// ValidateFormToken is a convenience function to validate CSRF token from form data
func ValidateFormToken(ctx router.Ctx, authManager *auth.AuthManager) bool {
func ValidateFormToken(ctx router.Ctx) bool {
// Try to get token from form data
tokenBytes := ctx.PostArgs().Peek(TokenFieldName)
if len(tokenBytes) == 0 {
@ -179,7 +178,7 @@ func ValidateFormToken(ctx router.Ctx, authManager *auth.AuthManager) bool {
return false
}
return ValidateToken(ctx, authManager, string(tokenBytes))
return ValidateToken(ctx, string(tokenBytes))
}
// StoreTokenInCookie stores a CSRF token in a cookie for guest users

View File

@ -24,7 +24,7 @@ func TestGenerateToken(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.SetUserValue(SessionCtxKey, sess)
token := GenerateToken(ctx, nil)
token := GenerateToken(ctx)
if token == "" {
t.Error("Expected non-empty token")
@ -48,15 +48,15 @@ func TestValidateToken(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.SetUserValue(SessionCtxKey, sess)
if !ValidateToken(ctx, nil, "test-token") {
if !ValidateToken(ctx, "test-token") {
t.Error("Expected valid token to pass validation")
}
if ValidateToken(ctx, nil, "wrong-token") {
if ValidateToken(ctx, "wrong-token") {
t.Error("Expected invalid token to fail validation")
}
if ValidateToken(ctx, nil, "") {
if ValidateToken(ctx, "") {
t.Error("Expected empty token to fail validation")
}
}
@ -64,7 +64,7 @@ func TestValidateToken(t *testing.T) {
func TestValidateTokenNoSession(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
if ValidateToken(ctx, nil, "any-token") {
if ValidateToken(ctx, "any-token") {
t.Error("Expected validation to fail with no session")
}
}
@ -81,7 +81,7 @@ func TestHiddenField(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.SetUserValue(SessionCtxKey, sess)
field := HiddenField(ctx, nil)
field := HiddenField(ctx)
expected := `<input type="hidden" name="_csrf_token" value="test-token">`
if field != expected {
@ -92,7 +92,7 @@ func TestHiddenField(t *testing.T) {
func TestHiddenFieldNoSession(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
field := HiddenField(ctx, nil)
field := HiddenField(ctx)
if field == "" {
t.Error("Expected non-empty field for guest user with cookie-based token")
}
@ -110,7 +110,7 @@ func TestTokenMeta(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.SetUserValue(SessionCtxKey, sess)
meta := TokenMeta(ctx, nil)
meta := TokenMeta(ctx)
expected := `<meta name="csrf-token" content="test-token">`
if meta != expected {
@ -155,13 +155,13 @@ func TestValidateFormToken(t *testing.T) {
ctx.PostArgs().Set(TokenFieldName, "test-token")
if !ValidateFormToken(ctx, nil) {
if !ValidateFormToken(ctx) {
t.Error("Expected form token validation to pass")
}
ctx.PostArgs().Set(TokenFieldName, "wrong-token")
if ValidateFormToken(ctx, nil) {
if ValidateFormToken(ctx) {
t.Error("Expected form token validation to fail with wrong token")
}
}

View File

@ -1,7 +1,7 @@
package middleware
import (
"dk/internal/auth"
"dk/internal/cookies"
"dk/internal/models/users"
"dk/internal/router"
"dk/internal/session"
@ -9,21 +9,21 @@ import (
"github.com/valyala/fasthttp"
)
func Auth(authManager *auth.AuthManager) router.Middleware {
func Auth() router.Middleware {
return func(next router.Handler) router.Handler {
return func(ctx router.Ctx, params []string) {
sessionID := auth.GetSessionCookie(ctx)
sessionID := cookies.GetCookie(ctx, session.SessionCookieName)
if sessionID != "" {
if sess, exists := authManager.GetSession(sessionID); exists {
authManager.UpdateSession(sessionID)
if sess, exists := session.Get(sessionID); exists {
session.Update(sessionID)
user, err := users.Find(sess.UserID)
if err == nil && user != nil {
ctx.SetUserValue("session", sess)
ctx.SetUserValue("user", user)
auth.SetSessionCookie(ctx, sessionID)
session.SetSessionCookie(ctx, sessionID)
}
}
}
@ -91,21 +91,21 @@ func GetCurrentSession(ctx router.Ctx) *session.Session {
return nil
}
func Login(ctx router.Ctx, authManager *auth.AuthManager, user *users.User) {
sess := authManager.CreateSession(user)
auth.SetSessionCookie(ctx, sess.ID)
func Login(ctx router.Ctx, user *users.User) {
sess := session.Create(user.ID, user.Username, user.Email)
session.SetSessionCookie(ctx, sess.ID)
ctx.SetUserValue("session", sess)
ctx.SetUserValue("user", user)
}
func Logout(ctx router.Ctx, authManager *auth.AuthManager) {
sessionID := auth.GetSessionCookie(ctx)
func Logout(ctx router.Ctx) {
sessionID := cookies.GetCookie(ctx, session.SessionCookieName)
if sessionID != "" {
authManager.DeleteSession(sessionID)
session.Delete(sessionID)
}
auth.DeleteSessionCookie(ctx)
session.DeleteSessionCookie(ctx)
ctx.SetUserValue("session", nil)
ctx.SetUserValue("user", nil)

View File

@ -1,7 +1,6 @@
package middleware
import (
"dk/internal/auth"
"dk/internal/csrf"
"dk/internal/router"
"slices"
@ -20,7 +19,7 @@ type CSRFConfig struct {
}
// CSRF creates a CSRF protection middleware
func CSRF(authManager *auth.AuthManager, config ...CSRFConfig) router.Middleware {
func CSRF(config ...CSRFConfig) router.Middleware {
cfg := CSRFConfig{
SkipMethods: []string{"GET", "HEAD", "OPTIONS"},
FailureHandler: func(ctx router.Ctx) {
@ -68,7 +67,7 @@ func CSRF(authManager *auth.AuthManager, config ...CSRFConfig) router.Middleware
}
// Validate CSRF token for protected methods
if !csrf.ValidateFormToken(ctx, authManager) {
if !csrf.ValidateFormToken(ctx) {
cfg.FailureHandler(ctx)
return
}
@ -79,7 +78,7 @@ func CSRF(authManager *auth.AuthManager, config ...CSRFConfig) router.Middleware
}
// RequireCSRF is a stricter CSRF middleware that always validates tokens
func RequireCSRF(authManager *auth.AuthManager, failureHandler ...func(router.Ctx)) router.Middleware {
func RequireCSRF(failureHandler ...func(router.Ctx)) router.Middleware {
handler := func(ctx router.Ctx) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetContentType("text/plain")
@ -92,7 +91,7 @@ func RequireCSRF(authManager *auth.AuthManager, failureHandler ...func(router.Ct
return func(next router.Handler) router.Handler {
return func(ctx router.Ctx, params []string) {
if !csrf.ValidateFormToken(ctx, authManager) {
if !csrf.ValidateFormToken(ctx) {
handler(ctx)
return
}
@ -103,16 +102,16 @@ func RequireCSRF(authManager *auth.AuthManager, failureHandler ...func(router.Ct
}
// CSRFToken returns the current CSRF token for the request
func CSRFToken(ctx router.Ctx, authManager *auth.AuthManager) string {
return csrf.GetToken(ctx, authManager)
func CSRFToken(ctx router.Ctx) string {
return csrf.GetToken(ctx)
}
// CSRFHiddenField generates a hidden input field for forms
func CSRFHiddenField(ctx router.Ctx, authManager *auth.AuthManager) string {
return csrf.HiddenField(ctx, authManager)
func CSRFHiddenField(ctx router.Ctx) string {
return csrf.HiddenField(ctx)
}
// CSRFMeta generates a meta tag for JavaScript access
func CSRFMeta(ctx router.Ctx, authManager *auth.AuthManager) string {
return csrf.TokenMeta(ctx, authManager)
func CSRFMeta(ctx router.Ctx) string {
return csrf.TokenMeta(ctx)
}

View File

@ -10,6 +10,7 @@ import (
"dk/internal/models/users"
"dk/internal/password"
"dk/internal/router"
"dk/internal/session"
"dk/internal/template/components"
"github.com/valyala/fasthttp"
@ -37,12 +38,12 @@ func RegisterAuthRoutes(r *router.Router) {
func showLogin(ctx router.Ctx, _ []string) {
// Get flash message if any
var errorHTML string
if flash := auth.GetFlashMessage(ctx); flash != nil {
if flash := session.GetFlashMessage(ctx); flash != nil {
errorHTML = fmt.Sprintf(`<div style="color: red; margin-bottom: 1rem;">%s</div>`, flash.Message)
}
// Get form data if any (for preserving email/username on error)
formData := auth.GetFormData(ctx)
formData := session.GetFormData(ctx)
id := ""
if formData != nil {
id = formData["id"]
@ -56,7 +57,7 @@ func showLogin(ctx router.Ctx, _ []string) {
// processLogin handles login form submission
func processLogin(ctx router.Ctx, _ []string) {
if !csrf.ValidateFormToken(ctx, auth.Manager) {
if !csrf.ValidateFormToken(ctx) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.WriteString("CSRF validation failed")
return
@ -66,21 +67,21 @@ func processLogin(ctx router.Ctx, _ []string) {
userPassword := string(ctx.PostArgs().Peek("password"))
if email == "" || userPassword == "" {
auth.SetFlashMessage(ctx, "error", "Email and password are required")
auth.SetFormData(ctx, map[string]string{"id": email})
session.SetFlashMessage(ctx, "error", "Email and password are required")
session.SetFormData(ctx, map[string]string{"id": email})
ctx.Redirect("/login", fasthttp.StatusFound)
return
}
user, err := auth.Manager.Authenticate(email, userPassword)
user, err := auth.Authenticate(email, userPassword)
if err != nil {
auth.SetFlashMessage(ctx, "error", "Invalid email or password")
auth.SetFormData(ctx, map[string]string{"id": email})
session.SetFlashMessage(ctx, "error", "Invalid email or password")
session.SetFormData(ctx, map[string]string{"id": email})
ctx.Redirect("/login", fasthttp.StatusFound)
return
}
middleware.Login(ctx, auth.Manager, user)
middleware.Login(ctx, user)
// Transfer CSRF token from cookie to session for authenticated user
if cookieToken := csrf.GetTokenFromCookie(ctx); cookieToken != "" {
@ -96,12 +97,12 @@ func processLogin(ctx router.Ctx, _ []string) {
func showRegister(ctx router.Ctx, _ []string) {
// Get flash message if any
var errorHTML string
if flash := auth.GetFlashMessage(ctx); flash != nil {
if flash := session.GetFlashMessage(ctx); flash != nil {
errorHTML = fmt.Sprintf(`<div style="color: red; margin-bottom: 1rem;">%s</div>`, flash.Message)
}
// Get form data if any (for preserving values on error)
formData := auth.GetFormData(ctx)
formData := session.GetFormData(ctx)
username := ""
email := ""
if formData != nil {
@ -118,7 +119,7 @@ func showRegister(ctx router.Ctx, _ []string) {
// processRegister handles registration form submission
func processRegister(ctx router.Ctx, _ []string) {
if !csrf.ValidateFormToken(ctx, auth.Manager) {
if !csrf.ValidateFormToken(ctx) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.WriteString("CSRF validation failed")
return
@ -130,8 +131,8 @@ func processRegister(ctx router.Ctx, _ []string) {
confirmPassword := string(ctx.PostArgs().Peek("confirm_password"))
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
auth.SetFlashMessage(ctx, "error", err.Error())
auth.SetFormData(ctx, map[string]string{
session.SetFlashMessage(ctx, "error", err.Error())
session.SetFormData(ctx, map[string]string{
"username": username,
"email": email,
})
@ -140,8 +141,8 @@ func processRegister(ctx router.Ctx, _ []string) {
}
if _, err := users.ByUsername(username); err == nil {
auth.SetFlashMessage(ctx, "error", "Username already exists")
auth.SetFormData(ctx, map[string]string{
session.SetFlashMessage(ctx, "error", "Username already exists")
session.SetFormData(ctx, map[string]string{
"username": username,
"email": email,
})
@ -150,8 +151,8 @@ func processRegister(ctx router.Ctx, _ []string) {
}
if _, err := users.ByEmail(email); err == nil {
auth.SetFlashMessage(ctx, "error", "Email already registered")
auth.SetFormData(ctx, map[string]string{
session.SetFlashMessage(ctx, "error", "Email already registered")
session.SetFormData(ctx, map[string]string{
"username": username,
"email": email,
})
@ -167,8 +168,8 @@ func processRegister(ctx router.Ctx, _ []string) {
user.Auth = 1
if err := user.Insert(); err != nil {
auth.SetFlashMessage(ctx, "error", "Failed to create account")
auth.SetFormData(ctx, map[string]string{
session.SetFlashMessage(ctx, "error", "Failed to create account")
session.SetFormData(ctx, map[string]string{
"username": username,
"email": email,
})
@ -177,7 +178,7 @@ func processRegister(ctx router.Ctx, _ []string) {
}
// Auto-login after registration
middleware.Login(ctx, auth.Manager, user)
middleware.Login(ctx, user)
// Transfer CSRF token from cookie to session for authenticated user
if cookieToken := csrf.GetTokenFromCookie(ctx); cookieToken != "" {
@ -192,13 +193,13 @@ func processRegister(ctx router.Ctx, _ []string) {
// processLogout handles logout
func processLogout(ctx router.Ctx, params []string) {
// Validate CSRF token
if !csrf.ValidateFormToken(ctx, auth.Manager) {
if !csrf.ValidateFormToken(ctx) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.WriteString("CSRF validation failed")
return
}
middleware.Logout(ctx, auth.Manager)
middleware.Logout(ctx)
ctx.Redirect("/", fasthttp.StatusFound)
}

View File

@ -2,13 +2,13 @@ package routes
import (
"dk/internal/actions"
"dk/internal/auth"
"dk/internal/helpers"
"dk/internal/middleware"
"dk/internal/models/items"
"dk/internal/models/towns"
"dk/internal/models/users"
"dk/internal/router"
"dk/internal/session"
"dk/internal/template/components"
"slices"
"strconv"
@ -32,7 +32,7 @@ func RegisterTownRoutes(r *router.Router) {
group.Get("/", showTown)
group.Get("/inn", showInn)
group.WithMiddleware(middleware.CSRF(auth.Manager)).Post("/inn", rest)
group.WithMiddleware(middleware.CSRF()).Post("/inn", rest)
group.Get("/shop", showShop)
group.Get("/shop/buy/:id", buyItem)
group.Get("/maps", showMaps)
@ -50,7 +50,7 @@ func showTown(ctx router.Ctx, _ []string) {
func showInn(ctx router.Ctx, _ []string) {
var errorHTML string
if flash := auth.GetFlashMessage(ctx); flash != nil {
if flash := session.GetFlashMessage(ctx); flash != nil {
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + flash.Message + "</div>"
}
@ -68,7 +68,7 @@ func rest(ctx router.Ctx, _ []string) {
user := ctx.UserValue("user").(*users.User)
if user.Gold < town.InnCost {
auth.SetFlashMessage(ctx, "error", "You can't afford to stay here tonight.")
session.SetFlashMessage(ctx, "error", "You can't afford to stay here tonight.")
ctx.Redirect("/town/inn", 303)
return
}
@ -84,7 +84,7 @@ func rest(ctx router.Ctx, _ []string) {
func showShop(ctx router.Ctx, _ []string) {
var errorHTML string
if flash := auth.GetFlashMessage(ctx); flash != nil {
if flash := session.GetFlashMessage(ctx); flash != nil {
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + flash.Message + "</div>"
}
@ -111,28 +111,28 @@ func showShop(ctx router.Ctx, _ []string) {
func buyItem(ctx router.Ctx, params []string) {
id, err := strconv.Atoi(params[0])
if err != nil {
auth.SetFlashMessage(ctx, "error", "Error purchasing item; "+err.Error())
session.SetFlashMessage(ctx, "error", "Error purchasing item; "+err.Error())
ctx.Redirect("/town/shop", 302)
return
}
town := ctx.UserValue("town").(*towns.Town)
if !slices.Contains(town.GetShopItems(), id) {
auth.SetFlashMessage(ctx, "error", "The item doesn't exist in this shop.")
session.SetFlashMessage(ctx, "error", "The item doesn't exist in this shop.")
ctx.Redirect("/town/shop", 302)
return
}
item, err := items.Find(id)
if err != nil {
auth.SetFlashMessage(ctx, "error", "Error purchasing item; "+err.Error())
session.SetFlashMessage(ctx, "error", "Error purchasing item; "+err.Error())
ctx.Redirect("/town/shop", 302)
return
}
user := ctx.UserValue("user").(*users.User)
if user.Gold < item.Value {
auth.SetFlashMessage(ctx, "error", "You don't have enough gold to buy "+item.Name)
session.SetFlashMessage(ctx, "error", "You don't have enough gold to buy "+item.Name)
ctx.Redirect("/town/shop", 302)
return
}
@ -146,7 +146,7 @@ func buyItem(ctx router.Ctx, params []string) {
func showMaps(ctx router.Ctx, _ []string) {
var errorHTML string
if flash := auth.GetFlashMessage(ctx); flash != nil {
if flash := session.GetFlashMessage(ctx); flash != nil {
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + flash.Message + "</div>"
}
@ -188,21 +188,21 @@ func showMaps(ctx router.Ctx, _ []string) {
func buyMap(ctx router.Ctx, params []string) {
id, err := strconv.Atoi(params[0])
if err != nil {
auth.SetFlashMessage(ctx, "error", "Error purchasing map; "+err.Error())
session.SetFlashMessage(ctx, "error", "Error purchasing map; "+err.Error())
ctx.Redirect("/town/maps", 302)
return
}
mapped, err := towns.Find(id)
if err != nil {
auth.SetFlashMessage(ctx, "error", "Error purchasing map; "+err.Error())
session.SetFlashMessage(ctx, "error", "Error purchasing map; "+err.Error())
ctx.Redirect("/town/maps", 302)
return
}
user := ctx.UserValue("user").(*users.User)
if user.Gold < mapped.MapCost {
auth.SetFlashMessage(ctx, "error", "You don't have enough gold to buy the map to "+mapped.Name)
session.SetFlashMessage(ctx, "error", "You don't have enough gold to buy the map to "+mapped.Name)
ctx.Redirect("/town/maps", 302)
return
}

229
internal/session/manager.go Normal file
View File

@ -0,0 +1,229 @@
package session
import (
"dk/internal/cookies"
"dk/internal/router"
"time"
)
const SessionCookieName = "dk_session"
var Manager *SessionManager
type SessionManager struct {
store *Store
}
func Init(sessionsFilePath string) {
if Manager != nil {
panic("session manager already initialized")
}
Manager = &SessionManager{
store: NewStore(sessionsFilePath),
}
}
func GetManager() *SessionManager {
if Manager == nil {
panic("session manager not initialized")
}
return Manager
}
func (sm *SessionManager) Create(userID int, username, email string) *Session {
sess := New(userID, username, email)
sm.store.Save(sess)
return sess
}
func (sm *SessionManager) Get(sessionID string) (*Session, bool) {
return sm.store.Get(sessionID)
}
func (sm *SessionManager) GetFromContext(ctx router.Ctx) (*Session, bool) {
sessionID := cookies.GetCookie(ctx, SessionCookieName)
if sessionID == "" {
return nil, false
}
return sm.Get(sessionID)
}
func (sm *SessionManager) Update(sessionID string) bool {
sess, exists := sm.store.Get(sessionID)
if !exists {
return false
}
sess.Touch()
sm.store.Save(sess)
return true
}
func (sm *SessionManager) Delete(sessionID string) {
sm.store.Delete(sessionID)
}
func (sm *SessionManager) SetSessionCookie(ctx router.Ctx, sessionID string) {
cookies.SetSecureCookie(ctx, cookies.CookieOptions{
Name: SessionCookieName,
Value: sessionID,
Path: "/",
Expires: time.Now().Add(DefaultExpiration),
HTTPOnly: true,
Secure: cookies.IsHTTPS(ctx),
SameSite: "lax",
})
}
func (sm *SessionManager) DeleteSessionCookie(ctx router.Ctx) {
cookies.DeleteCookie(ctx, SessionCookieName)
}
func (sm *SessionManager) SetFlashMessage(ctx router.Ctx, msgType, message string) bool {
sess, exists := sm.GetFromContext(ctx)
if !exists {
return false
}
sess.SetFlash("message", FlashMessage{
Type: msgType,
Message: message,
})
sm.store.Save(sess)
return true
}
func (sm *SessionManager) GetFlashMessage(ctx router.Ctx) *FlashMessage {
sess, exists := sm.GetFromContext(ctx)
if !exists {
return nil
}
value, exists := sess.GetFlash("message")
if !exists {
return nil
}
sm.store.Save(sess)
if msg, ok := value.(FlashMessage); ok {
return &msg
}
if msgMap, ok := value.(map[string]interface{}); ok {
msg := &FlashMessage{}
if t, ok := msgMap["type"].(string); ok {
msg.Type = t
}
if m, ok := msgMap["message"].(string); ok {
msg.Message = m
}
return msg
}
return nil
}
func (sm *SessionManager) SetFormData(ctx router.Ctx, data map[string]string) bool {
sess, exists := sm.GetFromContext(ctx)
if !exists {
return false
}
sess.Set("form_data", data)
sm.store.Save(sess)
return true
}
func (sm *SessionManager) GetFormData(ctx router.Ctx) map[string]string {
sess, exists := sm.GetFromContext(ctx)
if !exists {
return nil
}
value, exists := sess.Get("form_data")
if !exists {
return nil
}
sess.Delete("form_data")
sm.store.Save(sess)
if formData, ok := value.(map[string]string); ok {
return formData
}
if formMap, ok := value.(map[string]interface{}); ok {
result := make(map[string]string)
for k, v := range formMap {
if str, ok := v.(string); ok {
result[k] = str
}
}
return result
}
return nil
}
func (sm *SessionManager) Stats() (total, active int) {
return sm.store.Stats()
}
func (sm *SessionManager) Close() error {
return sm.store.Close()
}
// Package-level convenience functions that use the global Manager
func Create(userID int, username, email string) *Session {
return Manager.Create(userID, username, email)
}
func Get(sessionID string) (*Session, bool) {
return Manager.Get(sessionID)
}
func GetFromContext(ctx router.Ctx) (*Session, bool) {
return Manager.GetFromContext(ctx)
}
func Update(sessionID string) bool {
return Manager.Update(sessionID)
}
func Delete(sessionID string) {
Manager.Delete(sessionID)
}
func SetSessionCookie(ctx router.Ctx, sessionID string) {
Manager.SetSessionCookie(ctx, sessionID)
}
func DeleteSessionCookie(ctx router.Ctx) {
Manager.DeleteSessionCookie(ctx)
}
func SetFlashMessage(ctx router.Ctx, msgType, message string) bool {
return Manager.SetFlashMessage(ctx, msgType, message)
}
func GetFlashMessage(ctx router.Ctx) *FlashMessage {
return Manager.GetFlashMessage(ctx)
}
func SetFormData(ctx router.Ctx, data map[string]string) bool {
return Manager.SetFormData(ctx, data)
}
func GetFormData(ctx router.Ctx) map[string]string {
return Manager.GetFormData(ctx)
}
func Stats() (total, active int) {
return Manager.Stats()
}
func Close() error {
return Manager.Close()
}

View File

@ -5,7 +5,6 @@ import (
"maps"
"strings"
"dk/internal/auth"
"dk/internal/csrf"
"dk/internal/middleware"
"dk/internal/router"
@ -14,8 +13,8 @@ import (
// 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 {
if template.Cache == nil || auth.Manager == nil {
return fmt.Errorf("template.Cache or auth.Manager not initialized")
if template.Cache == nil {
return fmt.Errorf("template.Cache not initialized")
}
tmpl, err := template.Cache.Load(tmplPath)
@ -26,7 +25,7 @@ func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[strin
data := map[string]any{
"_title": PageTitle(title),
"authenticated": middleware.IsAuthenticated(ctx),
"csrf": csrf.HiddenField(ctx, auth.Manager),
"csrf": csrf.HiddenField(ctx),
"_totaltime": middleware.GetRequestTime(ctx),
"_numqueries": 0,
"_version": "1.0.0",

View File

@ -1,7 +1,6 @@
package components
import (
"dk/internal/auth"
"dk/internal/csrf"
"dk/internal/middleware"
"dk/internal/router"
@ -15,7 +14,7 @@ func GenerateTopNav(ctx router.Ctx) string {
%s
<button class="img-button" type="submit"><img src="/assets/images/button_logout.gif" alt="Log Out" title="Log Out"></button>
</form>
<a href="/help"><img src="/assets/images/button_help.gif" alt="Help" title="Help"></a>`, csrf.HiddenField(ctx, auth.Manager))
<a href="/help"><img src="/assets/images/button_help.gif" alt="Help" title="Help"></a>`, csrf.HiddenField(ctx))
} else {
return `<a href="/login"><img src="/assets/images/button_login.gif" alt="Log In" title="Log In"></a>
<a href="/register"><img src="/assets/images/button_register.gif" alt="Register" title="Register"></a>

10
main.go
View File

@ -9,8 +9,8 @@ import (
"path/filepath"
"syscall"
"dk/internal/auth"
"dk/internal/middleware"
"dk/internal/session"
"dk/internal/models/babble"
"dk/internal/models/control"
"dk/internal/models/drops"
@ -166,12 +166,12 @@ func start(port string) error {
return fmt.Errorf("failed to load models: %w", err)
}
auth.Init("sessions.json") // Initialize auth.Manager
session.Init("sessions.json") // Initialize session.Manager
r := router.New()
r.Use(middleware.Timing())
r.Use(middleware.Auth(auth.Manager))
r.Use(middleware.CSRF(auth.Manager))
r.Use(middleware.Auth())
r.Use(middleware.CSRF())
r.Get("/", routes.Index)
r.Get("/explore", routes.Explore)
@ -238,7 +238,7 @@ func start(port string) error {
// Save sessions before shutdown
log.Println("Saving sessions...")
if err := auth.Manager.Close(); err != nil {
if err := session.Close(); err != nil {
log.Printf("Error saving sessions: %v", err)
}