inboxer/internal/auth/session.go
cclohmar ca970104ee chore: initial commit — ExpenseFlow AI-Powered Expense Tracker
- Passwordless email OTP authentication
- Event-based expense tracking with HTMX UI
- AI receipt extraction via DeepSeek Vision API
- CSV/PDF report generation with email filing
- PWA with service worker and manifest
- Mobile-first responsive design
- SQLite database with auto-migration
2026-05-29 19:43:30 +00:00

100 lines
2.4 KiB
Go

// Package auth provides authentication and session management for ExpenseFlow.
package auth
import (
"crypto/rand"
"encoding/hex"
"log"
"sync"
"time"
)
// sessionData represents the data stored for each session.
type sessionData struct {
userID string
expiresAt time.Time
}
// SessionStore is an in-memory, thread-safe session store that maps
// session tokens to user sessions with expiration handling.
type SessionStore struct {
mu sync.RWMutex
sessions map[string]sessionData
}
// NewSessionStore creates and returns a new empty SessionStore.
func NewSessionStore() *SessionStore {
return &SessionStore{
sessions: make(map[string]sessionData),
}
}
// Generate creates a new session for the given userID with a 24-hour TTL.
// It returns a cryptographically secure random hex-encoded token string.
func (s *SessionStore) Generate(userID string) (string, error) {
token, err := generateRandomToken()
if err != nil {
log.Printf("auth: failed to generate session token: %v", err)
return "", err
}
s.mu.Lock()
s.sessions[token] = sessionData{
userID: userID,
expiresAt: time.Now().Add(24 * time.Hour),
}
s.mu.Unlock()
return token, nil
}
// Get returns the userID associated with the given token if the session
// exists and has not expired. Returns "", false otherwise.
func (s *SessionStore) Get(token string) (string, bool) {
s.mu.RLock()
data, ok := s.sessions[token]
s.mu.RUnlock()
if !ok {
return "", false
}
if time.Now().After(data.expiresAt) {
// Session is expired; clean it up.
s.Delete(token)
return "", false
}
return data.userID, true
}
// Delete removes the session identified by the given token.
func (s *SessionStore) Delete(token string) {
s.mu.Lock()
delete(s.sessions, token)
s.mu.Unlock()
}
// Cleanup removes all expired sessions from the store. This method is
// safe to call periodically from a background goroutine.
func (s *SessionStore) Cleanup() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for token, data := range s.sessions {
if now.After(data.expiresAt) {
delete(s.sessions, token)
}
}
}
// generateRandomToken creates a 32-byte cryptographically random token
// and returns its hex-encoded representation (64 hex characters).
func generateRandomToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}