package api import ( "net/http" "strings" "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" ) // requireAuth extracts the authenticated DID from the request. // Supports both cookie-based sessions (web) and Bearer token auth (mobile). // Returns 401 JSON error if not authenticated. func (h *Handlers) requireAuth(w http.ResponseWriter, r *http.Request) (syntax.DID, bool) { // Try cookie-based session first (web browser). didStr, _ := h.Auth.Sessions.Get(r) // Fall back to Bearer token (mobile app). if didStr == "" { if authHeader := r.Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer ") { token := strings.TrimPrefix(authHeader, "Bearer ") if d, _, err := h.Tokens.Validate(token); err == nil { didStr = d } } } if didStr == "" { writeError(w, http.StatusUnauthorized, "not authenticated") return "", false } did, err := syntax.ParseDID(didStr) if err != nil { writeError(w, http.StatusUnauthorized, "invalid session") return "", false } return did, true } // requireSession extracts both the DID and the full OAuth session. // Needed for handlers that must talk to the user's PDS. // Supports both cookie and Bearer token auth. // Returns 401 JSON error if not authenticated. func (h *Handlers) requireSession(w http.ResponseWriter, r *http.Request) (syntax.DID, *oauth.ClientSession, bool) { // Try cookie-based session first. did, sess, err := h.Auth.ResumeSession(r) if err == nil { return did, sess, true } // Fall back to Bearer token — extract DID + session ID and resume. if authHeader := r.Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer ") { token := strings.TrimPrefix(authHeader, "Bearer ") if d, sid, err := h.Tokens.Validate(token); err == nil { parsedDID, err := syntax.ParseDID(d) if err == nil { sess, err := h.Auth.OAuth.ResumeSession(r.Context(), parsedDID, sid) if err == nil { return parsedDID, sess, true } } } } writeError(w, http.StatusUnauthorized, "not authenticated") return "", nil, false } // JSONMiddleware sets common headers for all API responses and handles CORS // for web-based clients (Expo web dev server runs on a different origin). func JSONMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") // CORS: allow requests from any origin (the mobile app and Expo web // dev server both need cross-origin access). origin := r.Header.Get("Origin") if origin != "" { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Max-Age", "86400") } // Handle preflight. if r.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } // lookupPDS resolves a user's PDS host from cached OAuth session data. func (h *Handlers) lookupPDS(r *http.Request, did syntax.DID) string { if h.DB == nil { return "https://bsky.social" } var host string err := h.DB.QueryRowContext(r.Context(), ` SELECT data ->> 'host_url' FROM oauth_sessions WHERE did = ? ORDER BY updated_at DESC LIMIT 1 `, did.String()).Scan(&host) if err == nil && host != "" { return host } return "https://bsky.social" } // resolveHandle returns the best-known handle for a DID (from users table or // passed-through as the DID string). func (h *Handlers) resolveHandle(r *http.Request, did string) string { var handle string _ = h.DB.QueryRowContext(r.Context(), `SELECT handle FROM users WHERE did = ?`, did).Scan(&handle) if handle != "" { return handle } // Check if identity can resolve it. dir := identity.DefaultDirectory() ident, err := dir.LookupDID(r.Context(), syntax.DID(did)) if err == nil && ident.Handle != "" { return ident.Handle.String() } return did }