mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-25 03:34:14 +00:00
201 lines
3.8 KiB
Go
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
|
|
}
|