- Replace events+expenses with flat purchases table - Add warranty_months, return_days, product_name fields - Remove currency conversion, CSV/PDF reporting, event filing - Simplify auth (no onboarding/department/profile) - Update AI extraction prompts for product/warranty info - Update all branding: templates, install.sh, Makefile, service file
100 lines
2.4 KiB
Go
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
|
|
}
|