// Package session tracks logged-in browser sessions in memory. There is no // persistence and no signing: state lives only for the life of the process, // so a restart already invalidates every session, and signing would protect // nothing that a restart doesn't already. package session import ( "crypto/rand" "encoding/hex" "sync" "time" ) // CookieName is the session cookie set on a successful login. const CookieName = "dockmv_session" // TTL is how long a session survives without activity. Every successful // lookup slides the expiry forward by this much. const TTL = 7 * 24 * time.Hour // Session is one logged-in browser tab's worth of state. type Session struct { ID string UserID string Username string ExpiresAt time.Time } // Manager holds every live session. type Manager struct { mu sync.Mutex byID map[string]*Session } // NewManager returns an empty session store. func NewManager() *Manager { return &Manager{byID: map[string]*Session{}} } // Create starts a new session for a logged-in account. func (m *Manager) Create(userID, username string) (*Session, error) { id, err := newSessionID() if err != nil { return nil, err } s := &Session{ID: id, UserID: userID, Username: username, ExpiresAt: time.Now().Add(TTL)} m.mu.Lock() m.byID[id] = s m.mu.Unlock() return s, nil } // Get returns the session for id, sliding its expiry forward. ok is false for // an unknown or expired id. func (m *Manager) Get(id string) (Session, bool) { m.mu.Lock() defer m.mu.Unlock() s, ok := m.byID[id] if !ok { return Session{}, false } if time.Now().After(s.ExpiresAt) { delete(m.byID, id) return Session{}, false } s.ExpiresAt = time.Now().Add(TTL) return *s, true } // Delete ends one session, e.g. on logout. func (m *Manager) Delete(id string) { m.mu.Lock() delete(m.byID, id) m.mu.Unlock() } // DeleteAllForUser ends every session belonging to an account, e.g. when the // account itself is deleted. func (m *Manager) DeleteAllForUser(userID string) { m.mu.Lock() for id, s := range m.byID { if s.UserID == userID { delete(m.byID, id) } } m.mu.Unlock() } func newSessionID() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }