// 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))) }