From 75ddca3e7d931f92927218f7c97131236f0ef51e Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Mon, 18 May 2026 13:02:06 -0500 Subject: [PATCH] migrated from cup session to core/session.go, Contains the new SessionUser struct and the CreateAuthTokenHashedCacheKey utility function --- context.go | 1 + core.go | 9 ++++ session.go | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 session.go diff --git a/context.go b/context.go index 9015159..028ac5a 100644 --- a/context.go +++ b/context.go @@ -34,6 +34,7 @@ type Context struct { GetMailer func() *Mailer GetEventsManager func() *EventsManager GetLogger func() *logger.Logger + GetSession func() *SessionUser } // TODO enhance diff --git a/core.go b/core.go index 4163e2d..3af6adf 100644 --- a/core.go +++ b/core.go @@ -277,6 +277,7 @@ func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Ha GetMailer: resolveMailer(), GetEventsManager: resolveEventsManager(), GetLogger: resolveLogger(), + GetSession: getSession(), } ctx.prepare(ctx) @@ -685,6 +686,14 @@ func resolveLogger() func() *logger.Logger { return f } +// getSession returns a function that creates and returns a new SessionUser instance when invoked. +func getSession() func() *SessionUser { + f := func() *SessionUser { + return &SessionUser{} + } + return f +} + // MakeDirs creates the specified directories under the base path with the provided permissions (0766). func (app *App) MakeDirs(dirs ...string) { o := syscall.Umask(0) diff --git a/session.go b/session.go new file mode 100644 index 0000000..16888de --- /dev/null +++ b/session.go @@ -0,0 +1,146 @@ +// Copyright (c) 2026 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "crypto/md5" + "encoding/json" + "fmt" + "sync" + "time" +) + +// SessionUser handles user session data management with thread-safe operations. +// Sessions are stored in the cache (Redis) and are tied to an authenticated user +// via JWT tokens stored in cookies. +type SessionUser struct { + mu sync.RWMutex + context *Context + userID uint + hashedSessionKey string + authenticated bool + sessionStart time.Time + values map[string]interface{} +} + +// Init initializes the session by validating the user's JWT token from the cookie. +// It checks the cached token, verifies it matches, and loads any existing session data. +// Returns true if the session was successfully established. +func (s *SessionUser) Init(c *Context) bool { + s.context = c + + // get cookie + usercookie, err := c.GetCookie() + if err != nil || usercookie.Token == "" { + return false + } + + payload, err := c.GetJWT().DecodeToken(usercookie.Token) + if err != nil { + return false + } + + userID := uint(c.CastToInt(payload["userID"])) + userAgent := c.GetUserAgent() + + // verify token against cached value + hashedCacheKey := CreateAuthTokenHashedCacheKey(userID, userAgent) + cachedToken, err := c.GetCache().Get(hashedCacheKey) + if err != nil || cachedToken != usercookie.Token { + return false + } + + // session established - load session data from cache + userAgent = c.GetUserAgent() + sessionKey := fmt.Sprintf("sess_%v", userAgent) + s.hashedSessionKey = CreateAuthTokenHashedCacheKey(userID, sessionKey) + s.values = make(map[string]interface{}) + s.authenticated = true + s.userID = userID + + value, _ := c.GetCache().Get(s.hashedSessionKey) + if len(value) > 0 { + _ = json.Unmarshal([]byte(value), &s.values) + } + + return true +} + +// Set stores a value in the session and persists it to the cache. +func (s *SessionUser) Set(key string, value interface{}) error { + s.mu.Lock() + s.values[key] = value + s.mu.Unlock() + return s.Save() +} + +// Get retrieves a value from the session. Returns the value and a boolean indicating if the key exists. +func (s *SessionUser) Get(key string) (interface{}, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + val, ok := s.values[key] + return val, ok +} + +// Delete removes a specific key from the session and persists the change. +// Returns the deleted value if it existed. +func (s *SessionUser) Delete(key string) interface{} { + s.mu.RLock() + v, ok := s.values[key] + s.mu.RUnlock() + if ok { + s.mu.Lock() + delete(s.values, key) + s.mu.Unlock() + } + s.Save() + return v +} + +// Flush deletes all session data from the cache. +func (s *SessionUser) Flush() error { + s.mu.Lock() + defer s.mu.Unlock() + if s.hashedSessionKey != "" { + _ = s.context.GetCache().Delete(s.hashedSessionKey) + } + s.values = make(map[string]interface{}) + s.authenticated = false + return nil +} + +// Save persists the current session values to the cache. +func (s *SessionUser) Save() error { + s.mu.RLock() + defer s.mu.RUnlock() + + if len(s.values) > 0 { + buf, err := json.Marshal(&s.values) + if err != nil { + return err + } + return s.context.GetCache().Set(s.hashedSessionKey, string(buf)) + } + + _ = s.context.GetCache().Delete(s.hashedSessionKey) + return nil +} + +// GetUserID returns the user ID of the authenticated session user. +func (s *SessionUser) GetUserID() uint { + return s.userID +} + +// IsAuthenticated returns whether the session has been successfully authenticated. +func (s *SessionUser) IsAuthenticated() bool { + return s.authenticated +} + +// CreateAuthTokenHashedCacheKey generates a hashed cache key used to store JWT tokens and session data. +// The key is based on the user ID and user agent, hashed with MD5. +func CreateAuthTokenHashedCacheKey(userID uint, userAgent string) string { + cacheKey := fmt.Sprintf("userid:_%v_useragent:_%v_jwt_token", userID, userAgent) + return fmt.Sprintf("%x", md5.Sum([]byte(cacheKey))) +}