[WIP] music platform user data scraper
teal-fm atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396package session
import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "log" "net/http" "sync" "time"
"github.com/teal-fm/piper/db" "github.com/teal-fm/piper/db/apikey")
// Session session/session.go. Web session for Pipertype Session struct { ID string UserID int64 ATProtoSessionID string CreatedAt time.Time ExpiresAt time.Time}
type Manager struct { db *db.DB sessions map[string]*Session // use in memory cache if necessary ApiKeyMgr *apikey.Manager mu sync.RWMutex}
func NewSessionManager(database *db.DB) *Manager {
_, err := database.Exec(` CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id INTEGER NOT NULL, at_proto_session_id TEXT NOT NULL, created_at TIMESTAMP, expires_at TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) )`)
if err != nil { log.Printf("Error creating sessions table: %v", err) }
apiKeyMgr := apikey.NewApiKeyManager(database)
sm := &Manager{ db: database, sessions: make(map[string]*Session), ApiKeyMgr: apiKeyMgr, }
go func() { ticker := time.NewTicker(time.Hour) defer ticker.Stop() for range ticker.C { sm.cleanupExpiredSessions() } }()
return sm}
// cleanupExpiredSessions removes expired sessions from the in-memory map and the databasefunc (sm *Manager) cleanupExpiredSessions() { now := time.Now().UTC()
sm.mu.Lock() for id, session := range sm.sessions { if now.After(session.ExpiresAt) { delete(sm.sessions, id) } } sm.mu.Unlock()
if sm.db != nil { _, err := sm.db.Exec("DELETE FROM sessions WHERE expires_at < ?", now) if err != nil { log.Printf("Error deleting expired sessions from database: %v", err) } }}
// CreateSession create a new session for a userfunc (sm *Manager) CreateSession(userID int64, atProtoSessionId string) *Session { sm.mu.Lock() defer sm.mu.Unlock()
// random session id b := make([]byte, 32) //This probably should panic rand.Read(b) sessionID := base64.URLEncoding.EncodeToString(b)
now := time.Now().UTC() expiresAt := now.Add(24 * time.Hour) // 24-hour session
session := &Session{ ID: sessionID, UserID: userID, ATProtoSessionID: atProtoSessionId, CreatedAt: now, ExpiresAt: expiresAt, }
// store session in memory sm.sessions[sessionID] = session
// store session in database if available if sm.db != nil { _, err := sm.db.Exec(` INSERT INTO sessions (id, user_id, at_proto_session_id, created_at, expires_at) VALUES (?, ?, ?, ?, ?)`, sessionID, userID, atProtoSessionId, now, expiresAt)
if err != nil { log.Printf("Error storing session in database: %v", err) } }
return session}
// GetSession retrieve a session by IDfunc (sm *Manager) GetSession(sessionID string) (*Session, bool) { // First check in-memory cache sm.mu.RLock() session, exists := sm.sessions[sessionID] sm.mu.RUnlock()
if exists { // Check if session is expired if time.Now().UTC().After(session.ExpiresAt) { sm.DeleteSession(sessionID) return nil, false } return session, true }
// if not in memory and we have a database, check there if sm.db != nil { session = &Session{ID: sessionID}
err := sm.db.QueryRow(` SELECT user_id, at_proto_session_id, created_at, expires_at FROM sessions WHERE id = ?`, sessionID).Scan( &session.UserID, &session.ATProtoSessionID, &session.CreatedAt, &session.ExpiresAt)
if err != nil { return nil, false }
if time.Now().UTC().After(session.ExpiresAt) { sm.DeleteSession(sessionID) return nil, false }
// add to in-memory cache sm.mu.Lock() sm.sessions[sessionID] = session sm.mu.Unlock()
return session, true }
return nil, false}
// DeleteSession remove a sessionfunc (sm *Manager) DeleteSession(sessionID string) { sm.mu.Lock() delete(sm.sessions, sessionID) sm.mu.Unlock()
if sm.db != nil { _, err := sm.db.Exec("DELETE FROM sessions WHERE id = ?", sessionID) if err != nil { log.Printf("Error deleting session from database: %v", err) } }}
// SetSessionCookie set a session cookie for the userfunc (sm *Manager) SetSessionCookie(w http.ResponseWriter, session *Session) { cookie := &http.Cookie{ Name: "session", Value: session.ID, Path: "/", HttpOnly: true, Secure: false, Expires: session.ExpiresAt, } http.SetCookie(w, cookie)}
// ClearSessionCookie clears the session cookiefunc (sm *Manager) ClearSessionCookie(w http.ResponseWriter) { cookie := &http.Cookie{ Name: "session", Value: "", Path: "/", HttpOnly: true, Secure: false, MaxAge: -1, } http.SetCookie(w, cookie)}
func (sm *Manager) GetAPIKeyManager() *apikey.Manager { return sm.ApiKeyMgr}
func (sm *Manager) CreateAPIKey(userID int64, name string, validityDays int) (*apikey.ApiKey, error) { return sm.ApiKeyMgr.CreateApiKey(userID, name, validityDays)}
// WithAuth middleware that checks if a user is authenticated via cookies or API keyfunc WithAuth(handler http.HandlerFunc, sm *Manager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // first: check API keys apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r) if apiKeyErr == nil && apiKeyStr != "" { apiKey, valid := sm.ApiKeyMgr.GetApiKey(apiKeyStr) if valid { ctx := WithUserID(r.Context(), apiKey.UserID) r = r.WithContext(ctx)
// set a flag for api requests ctx = WithAPIRequest(r.Context(), true) r = r.WithContext(ctx)
handler(w, r) return } }
// if not found, check cookies for session value cookie, err := r.Cookie("session") if err != nil { http.Redirect(w, r, "/", http.StatusSeeOther) return }
session, exists := sm.GetSession(cookie.Value) if !exists { http.Redirect(w, r, "/", http.StatusSeeOther) return }
ctx := WithUserID(r.Context(), session.UserID) r = r.WithContext(ctx)
handler(w, r) }}
// WithPossibleAuth middleware that checks if a user is authenticated but doesn't error out if notfunc WithPossibleAuth(handler http.HandlerFunc, sm *Manager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() authenticated := false
apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r) if apiKeyErr == nil && apiKeyStr != "" { apiKey, valid := sm.ApiKeyMgr.GetApiKey(apiKeyStr) if valid { ctx = WithUserID(ctx, apiKey.UserID) ctx = WithAPIRequest(ctx, true) authenticated = true r = r.WithContext(WithAuthStatus(ctx, authenticated)) handler(w, r) return } }
if !authenticated { cookie, err := r.Cookie("session") if err == nil { session, exists := sm.GetSession(cookie.Value) if exists { ctx = WithUserID(ctx, session.UserID) authenticated = true } } }
r = r.WithContext(WithAuthStatus(ctx, authenticated)) handler(w, r) }}
// WithAPIAuth middleware that only accepts API keysfunc WithAPIAuth(handler http.HandlerFunc, sm *Manager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r) if apiKeyErr != nil || apiKeyStr == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, err := w.Write([]byte(`{"error": "API key is required"}`)) if err != nil { log.Printf("Error writing error response: %v", err) return } return }
apiKey, valid := sm.ApiKeyMgr.GetApiKey(apiKeyStr) if !valid { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, err := w.Write([]byte(`{"error": "Invalid or expired API key"}`)) if err != nil { log.Printf("Error writing error response: %v", err) return } return }
ctx := WithUserID(r.Context(), apiKey.UserID) ctx = WithAPIRequest(ctx, true) r = r.WithContext(ctx)
handler(w, r) }}
func (sm *Manager) HandleDebug(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, ok := GetUserID(ctx) if !ok { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, err := w.Write([]byte(`{"error": "User ID not found in context"}`)) if err != nil { log.Printf("Error writing error response: %v", err) return } return }
res, err := sm.db.DebugViewUserInformation(userID) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) _, err := w.Write([]byte(fmt.Sprintf(`{"error": "Failed to retrieve user information: %v"}`, err))) if err != nil { log.Printf("Error writing error response: %v", err) return } return }
w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) err = json.NewEncoder(w).Encode(res) if err != nil { log.Printf("Error encoding JSON response: %v", err) return }}
type contextKey int
const ( userIDKey contextKey = iota apiRequestKey authStatusKey)
func WithUserID(ctx context.Context, userID int64) context.Context { return context.WithValue(ctx, userIDKey, userID)}
func GetUserID(ctx context.Context) (int64, bool) { userID, ok := ctx.Value(userIDKey).(int64) return userID, ok}
func WithAuthStatus(ctx context.Context, isAuthed bool) context.Context { return context.WithValue(ctx, authStatusKey, isAuthed)}
func WithAPIRequest(ctx context.Context, isAPI bool) context.Context { return context.WithValue(ctx, apiRequestKey, isAPI)}
func IsAPIRequest(ctx context.Context) bool { isAPI, ok := ctx.Value(apiRequestKey).(bool) return ok && isAPI}