diff --git a/internal/handlers/account/login.go b/internal/handlers/account/login.go index 357bba9..a858439 100644 --- a/internal/handlers/account/login.go +++ b/internal/handlers/account/login.go @@ -12,6 +12,7 @@ import ( "synlotto-website/internal/logging" "synlotto-website/internal/models" "synlotto-website/internal/platform/bootstrap" + "synlotto-website/internal/platform/sessionkeys" auditlogStorage "synlotto-website/internal/storage/auditlog" usersStorage "synlotto-website/internal/storage/users" ) @@ -74,6 +75,8 @@ func LoginPost(c *gin.Context) { _ = sm.RenewToken(r.Context()) sm.Put(r.Context(), "user_id", user.Id) + sm.Put(r.Context(), sessionkeys.Username, user.Username) + sm.Put(r.Context(), sessionkeys.IsAdmin, user.IsAdmin) sm.Put(r.Context(), "last_activity", time.Now().UTC()) sm.Put(r.Context(), "flash", "Welcome back, "+user.Username+"!") diff --git a/internal/http/error/errors.go b/internal/http/error/errors.go index e8a135c..6d1790f 100644 --- a/internal/http/error/errors.go +++ b/internal/http/error/errors.go @@ -6,37 +6,78 @@ import ( "os" templateHelpers "synlotto-website/internal/helpers/template" + "synlotto-website/internal/models" + "synlotto-website/internal/platform/sessionkeys" "github.com/alexedwards/scs/v2" "github.com/gin-gonic/gin" ) -// RenderStatus renders web/templates/error/.html inside layout.html. +// RenderStatus renders web/templates/error/.html inside layout.html, +// using ONLY session data (no DB) so 404/500 pages don't crash and still +// look "logged in" when a session exists. func RenderStatus(c *gin.Context, sessions *scs.SessionManager, status int) { - // Base context - ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) + // Synthesize minimal TemplateData from session only + var data models.TemplateData - // Flash (SCS) - if f := sessions.PopString(c.Request.Context(), "flash"); f != "" { - ctx["Flash"] = f + ctx := c.Request.Context() + + // Read minimal user snapshot from session + var uid int64 + if v := sessions.Get(ctx, sessionkeys.UserID); v != nil { + switch t := v.(type) { + case int64: + uid = t + case int: + uid = int64(t) + } + } + if uid > 0 { + // username and is_admin are optional but make navbar correct + var uname string + if v := sessions.Get(ctx, sessionkeys.Username); v != nil { + if s, ok := v.(string); ok { + uname = s + } + } + var isAdmin bool + if v := sessions.Get(ctx, sessionkeys.IsAdmin); v != nil { + if b, ok := v.(bool); ok { + isAdmin = b + } + } + + // Build a lightweight user; avoids DB lookups in error paths + data.User = &models.User{ + Id: uid, + Username: uname, + IsAdmin: isAdmin, + } + data.IsAdmin = isAdmin } - // Use your finalized paths + // Turn into the template context map (adds site meta, funcs, etc.) + ctxMap := templateHelpers.TemplateContext(c.Writer, c.Request, data) + + // Flash (SCS) + if f := sessions.PopString(ctx, sessionkeys.Flash); f != "" { + ctxMap["Flash"] = f + } + + // Template paths (layout-first) pagePath := fmt.Sprintf("web/templates/error/%d.html", status) if _, err := os.Stat(pagePath); err != nil { c.String(status, http.StatusText(status)) return } - - // Keep your "layout first" load order tmpl := templateHelpers.LoadTemplateFiles( "web/templates/layout.html", pagePath, ) c.Status(status) - if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { + if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctxMap); err != nil { c.String(status, http.StatusText(status)) } } diff --git a/internal/http/middleware/auth.go b/internal/http/middleware/auth.go index 08530eb..f97a7d8 100644 --- a/internal/http/middleware/auth.go +++ b/internal/http/middleware/auth.go @@ -21,6 +21,7 @@ func AuthMiddleware() gin.HandlerFunc { if v := sm.Get(ctx, sessionkeys.LastActivity); v != nil { if last, ok := v.(time.Time); ok && time.Since(last) > sm.Lifetime { + // don't destroy here; just rotate and bounce to login with a flash _ = sm.RenewToken(ctx) sm.Put(ctx, sessionkeys.Flash, "Your session has timed out.") c.Redirect(http.StatusSeeOther, "/account/login") @@ -29,7 +30,10 @@ func AuthMiddleware() gin.HandlerFunc { } } - sm.Put(ctx, sessionkeys.LastActivity, time.Now().UTC()) + // if logged in, update last activity + if sm.Exists(ctx, sessionkeys.UserID) { + sm.Put(ctx, sessionkeys.LastActivity, time.Now().UTC()) + } c.Next() } } @@ -66,8 +70,7 @@ func RememberMiddleware(app *bootstrap.App) gin.HandlerFunc { } if sessionHelper.HashVerifier(verifier) != hash { - // Tampered token – revoke for safety. - _ = sessionHelper.RevokeToken(app.DB, selector) + _ = sessionHelper.RevokeToken(app.DB, selector) // tampered c.Next() return } @@ -76,6 +79,9 @@ func RememberMiddleware(app *bootstrap.App) gin.HandlerFunc { _ = sm.RenewToken(ctx) sm.Put(ctx, sessionkeys.UserID, userID) sm.Put(ctx, sessionkeys.LastActivity, time.Now().UTC()) + // (Optional) if you can look up username/is_admin here, also set: + // sm.Put(ctx, sessionkeys.Username, uname) + // sm.Put(ctx, sessionkeys.IsAdmin, isAdmin) c.Next() } @@ -86,8 +92,10 @@ func RequireAuth() gin.HandlerFunc { return func(c *gin.Context) { app := c.MustGet("app").(*bootstrap.App) sm := app.SessionManager + ctx := c.Request.Context() - if sm.GetInt(c.Request.Context(), sessionkeys.UserID) == 0 { + // ✅ Use Exists to be robust to int vs int64 storage + if !sm.Exists(ctx, sessionkeys.UserID) { c.Redirect(http.StatusSeeOther, "/account/login") c.Abort() return diff --git a/internal/platform/sessionkeys/keys.go b/internal/platform/sessionkeys/keys.go index 84e18c8..9d1d946 100644 --- a/internal/platform/sessionkeys/keys.go +++ b/internal/platform/sessionkeys/keys.go @@ -1,8 +1,9 @@ package sessionkeys -//ToDo: Is this just putting in "user_id" rather than the users ID? const ( UserID = "user_id" + Username = "username" + IsAdmin = "is_admin" LastActivity = "last_activity" Flash = "flash" ) diff --git a/internal/storage/migrations/0001_initial_create.up.sql b/internal/storage/migrations/0001_initial_create.up.sql index ba36b41..245fe21 100644 --- a/internal/storage/migrations/0001_initial_create.up.sql +++ b/internal/storage/migrations/0001_initial_create.up.sql @@ -217,14 +217,17 @@ CREATE TABLE IF NOT EXISTS audit_log ( ON UPDATE CASCADE ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- AUDIT LOGIN (new) CREATE TABLE IF NOT EXISTS audit_login ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(191), - success TINYINT(1), - ip VARCHAR(64), - user_agent VARCHAR(255), - timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NULL, + username VARCHAR(191) NOT NULL, + success TINYINT(1) NOT NULL, + ip VARCHAR(64) NOT NULL, + user_agent VARCHAR(255), + timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY idx_audit_login_user_id (user_id), + CONSTRAINT fk_audit_login_user + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- SYNDICATES