Files
at-container-registry/pkg/appview/session/session.go
T

201 lines
3.8 KiB
Go

package session
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"sync"
"time"
)
// Session represents a user session
type Session struct {
ID string
DID string
Handle string
PDSEndpoint string
ExpiresAt time.Time
}
// Store manages user sessions
type Store struct {
mu sync.RWMutex
sessions map[string]*Session
filePath string
}
// NewStore creates a new session store with file persistence
func NewStore(filePath string) *Store {
store := &Store{
sessions: make(map[string]*Session),
filePath: filePath,
}
// Load existing sessions from file
if err := store.load(); err != nil {
fmt.Printf("Warning: Failed to load sessions from %s: %v\n", filePath, err)
}
return store
}
// load reads sessions from disk
func (s *Store) load() error {
if s.filePath == "" {
return nil
}
data, err := os.ReadFile(s.filePath)
if err != nil {
if os.IsNotExist(err) {
return nil // File doesn't exist yet, that's fine
}
return err
}
var sessions map[string]*Session
if err := json.Unmarshal(data, &sessions); err != nil {
return err
}
// Filter out expired sessions
now := time.Now()
for id, sess := range sessions {
if now.Before(sess.ExpiresAt) {
s.sessions[id] = sess
}
}
fmt.Printf("Loaded %d active sessions from disk\n", len(s.sessions))
return nil
}
// save writes sessions to disk
func (s *Store) save() error {
if s.filePath == "" {
return nil
}
data, err := json.Marshal(s.sessions)
if err != nil {
return err
}
return os.WriteFile(s.filePath, data, 0600)
}
// Create creates a new session and returns the session ID
func (s *Store) Create(did, handle, pdsEndpoint string, duration time.Duration) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
// Generate random session ID
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
sess := &Session{
ID: base64.URLEncoding.EncodeToString(b),
DID: did,
Handle: handle,
PDSEndpoint: pdsEndpoint,
ExpiresAt: time.Now().Add(duration),
}
s.sessions[sess.ID] = sess
// Save to disk
if err := s.save(); err != nil {
fmt.Printf("Warning: Failed to save sessions to disk: %v\n", err)
}
return sess.ID, nil
}
// Get retrieves a session by ID
func (s *Store) Get(id string) (*Session, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
sess, ok := s.sessions[id]
if !ok || time.Now().After(sess.ExpiresAt) {
return nil, false
}
return sess, true
}
// Delete removes a session
func (s *Store) Delete(id string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.sessions, id)
// Save to disk
if err := s.save(); err != nil {
fmt.Printf("Warning: Failed to save sessions to disk: %v\n", err)
}
}
// Cleanup removes expired sessions
func (s *Store) Cleanup() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
deleted := 0
for id, sess := range s.sessions {
if now.After(sess.ExpiresAt) {
delete(s.sessions, id)
deleted++
}
}
if deleted > 0 {
// Save to disk
if err := s.save(); err != nil {
fmt.Printf("Warning: Failed to save sessions to disk: %v\n", err)
}
}
}
// SetCookie sets the session cookie
func SetCookie(w http.ResponseWriter, sessionID string, maxAge int) {
http.SetCookie(w, &http.Cookie{
Name: "atcr_session",
Value: sessionID,
Path: "/",
MaxAge: maxAge,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
}
// ClearCookie clears the session cookie
func ClearCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "atcr_session",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
}
// GetSessionID gets session ID from cookie
func GetSessionID(r *http.Request) (string, bool) {
cookie, err := r.Cookie("atcr_session")
if err != nil {
return "", false
}
return cookie.Value, true
}