A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at codeberg-source 222 lines 5.6 kB view raw
1package db 2 3import ( 4 "context" 5 "crypto/rand" 6 "database/sql" 7 "encoding/base64" 8 "fmt" 9 "log/slog" 10 "net/http" 11 "time" 12) 13 14// Session represents a user session 15// Compatible with pkg/appview/session.Session 16type Session struct { 17 ID string 18 DID string 19 Handle string 20 PDSEndpoint string 21 OAuthSessionID string // Links to oauth_sessions.session_id 22 ExpiresAt time.Time 23} 24 25// SessionStoreInterface defines the session storage interface 26// Both db.SessionStore and session.Store implement this 27type SessionStoreInterface interface { 28 Create(did, handle, pdsEndpoint string, duration time.Duration) (string, error) 29 CreateWithOAuth(did, handle, pdsEndpoint, oauthSessionID string, duration time.Duration) (string, error) 30 Get(id string) (*Session, bool) 31 Delete(id string) 32 Cleanup() 33} 34 35// SessionStore manages user sessions with SQLite persistence 36type SessionStore struct { 37 db *sql.DB 38} 39 40// NewSessionStore creates a new SQLite-backed session store 41func NewSessionStore(db *sql.DB) *SessionStore { 42 return &SessionStore{db: db} 43} 44 45// Create creates a new session and returns the session ID 46func (s *SessionStore) Create(did, handle, pdsEndpoint string, duration time.Duration) (string, error) { 47 return s.CreateWithOAuth(did, handle, pdsEndpoint, "", duration) 48} 49 50// CreateWithOAuth creates a new session with OAuth sessionID and returns the session ID 51func (s *SessionStore) CreateWithOAuth(did, handle, pdsEndpoint, oauthSessionID string, duration time.Duration) (string, error) { 52 // Generate random session ID 53 b := make([]byte, 32) 54 if _, err := rand.Read(b); err != nil { 55 return "", fmt.Errorf("failed to generate session ID: %w", err) 56 } 57 58 sessionID := base64.URLEncoding.EncodeToString(b) 59 expiresAt := time.Now().Add(duration) 60 61 _, err := s.db.Exec(` 62 INSERT INTO ui_sessions (id, did, handle, pds_endpoint, oauth_session_id, expires_at, created_at) 63 VALUES (?, ?, ?, ?, ?, ?, datetime('now')) 64 `, sessionID, did, handle, pdsEndpoint, oauthSessionID, expiresAt) 65 66 if err != nil { 67 return "", fmt.Errorf("failed to create session: %w", err) 68 } 69 70 return sessionID, nil 71} 72 73// Get retrieves a session by ID 74func (s *SessionStore) Get(id string) (*Session, bool) { 75 var sess Session 76 77 err := s.db.QueryRow(` 78 SELECT id, did, handle, pds_endpoint, oauth_session_id, expires_at 79 FROM ui_sessions 80 WHERE id = ? 81 `, id).Scan(&sess.ID, &sess.DID, &sess.Handle, &sess.PDSEndpoint, &sess.OAuthSessionID, &sess.ExpiresAt) 82 83 if err == sql.ErrNoRows { 84 return nil, false 85 } 86 if err != nil { 87 slog.Warn("Failed to query session", "error", err) 88 return nil, false 89 } 90 91 // Check if expired 92 if time.Now().After(sess.ExpiresAt) { 93 return nil, false 94 } 95 96 return &sess, true 97} 98 99// Extend extends a session's expiration time 100func (s *SessionStore) Extend(id string, duration time.Duration) error { 101 expiresAt := time.Now().Add(duration) 102 103 result, err := s.db.Exec(` 104 UPDATE ui_sessions 105 SET expires_at = ? 106 WHERE id = ? 107 `, expiresAt, id) 108 109 if err != nil { 110 return fmt.Errorf("failed to extend session: %w", err) 111 } 112 113 rows, _ := result.RowsAffected() 114 if rows == 0 { 115 return fmt.Errorf("session not found: %s", id) 116 } 117 118 return nil 119} 120 121// Delete removes a session 122func (s *SessionStore) Delete(id string) { 123 _, err := s.db.Exec(` 124 DELETE FROM ui_sessions WHERE id = ? 125 `, id) 126 127 if err != nil { 128 slog.Warn("Failed to delete session", "error", err) 129 } 130} 131 132// DeleteByDID removes all sessions for a given DID 133// This is useful when OAuth refresh fails and we need to force re-authentication 134func (s *SessionStore) DeleteByDID(did string) { 135 result, err := s.db.Exec(` 136 DELETE FROM ui_sessions WHERE did = ? 137 `, did) 138 139 if err != nil { 140 slog.Warn("Failed to delete sessions for DID", "did", did, "error", err) 141 return 142 } 143 144 deleted, _ := result.RowsAffected() 145 if deleted > 0 { 146 slog.Info("Deleted UI sessions for DID due to OAuth failure", "count", deleted, "did", did) 147 } 148} 149 150// Cleanup removes expired sessions 151func (s *SessionStore) Cleanup() { 152 result, err := s.db.Exec(` 153 DELETE FROM ui_sessions 154 WHERE expires_at < datetime('now') 155 `) 156 157 if err != nil { 158 slog.Warn("Failed to cleanup sessions", "error", err) 159 return 160 } 161 162 deleted, _ := result.RowsAffected() 163 if deleted > 0 { 164 slog.Info("Cleaned up expired UI sessions", "count", deleted) 165 } 166} 167 168// CleanupContext is a context-aware version of Cleanup for background workers 169func (s *SessionStore) CleanupContext(ctx context.Context) error { 170 result, err := s.db.ExecContext(ctx, ` 171 DELETE FROM ui_sessions 172 WHERE expires_at < datetime('now') 173 `) 174 175 if err != nil { 176 return fmt.Errorf("failed to cleanup sessions: %w", err) 177 } 178 179 deleted, _ := result.RowsAffected() 180 if deleted > 0 { 181 slog.Info("Cleaned up expired UI sessions", "count", deleted) 182 } 183 184 return nil 185} 186 187// Cookie helper functions (compatible with pkg/appview/session package) 188 189// SetCookie sets the session cookie 190func SetCookie(w http.ResponseWriter, sessionID string, maxAge int) { 191 http.SetCookie(w, &http.Cookie{ 192 Name: "atcr_session", 193 Value: sessionID, 194 Path: "/", 195 MaxAge: maxAge, 196 HttpOnly: true, 197 Secure: true, 198 SameSite: http.SameSiteLaxMode, 199 }) 200} 201 202// ClearCookie clears the session cookie 203func ClearCookie(w http.ResponseWriter) { 204 http.SetCookie(w, &http.Cookie{ 205 Name: "atcr_session", 206 Value: "", 207 Path: "/", 208 MaxAge: -1, 209 HttpOnly: true, 210 Secure: true, 211 SameSite: http.SameSiteLaxMode, 212 }) 213} 214 215// GetSessionID gets session ID from cookie 216func GetSessionID(r *http.Request) (string, bool) { 217 cookie, err := r.Cookie("atcr_session") 218 if err != nil { 219 return "", false 220 } 221 return cookie.Value, true 222}