// Package middleware provides HTTP middleware for AppView, including // authentication (session-based for web UI, token-based for registry), // identity resolution (handle/DID to PDS endpoint), and hold discovery // for routing blobs to storage endpoints. package middleware import ( "context" "database/sql" "net/http" "net/url" "atcr.io/pkg/appview/db" ) type contextKey string const userKey contextKey = "user" // handleUnauthenticated handles unauthenticated requests appropriately. // For HTMX requests, returns 401 to avoid swapping login page content into the DOM. // For regular requests, redirects to the login page. func handleUnauthenticated(w http.ResponseWriter, r *http.Request) { // HTMX requests should get a 401 to trigger client-side handling // instead of swapping login page content into the current page if r.Header.Get("HX-Request") == "true" { w.Header().Set("HX-Redirect", "/auth/oauth/login?return_to="+url.QueryEscape(r.URL.Path)) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Build return URL with query parameters preserved returnTo := r.URL.Path if r.URL.RawQuery != "" { returnTo = r.URL.Path + "?" + r.URL.RawQuery } http.Redirect(w, r, "/auth/oauth/login?return_to="+url.QueryEscape(returnTo), http.StatusFound) } // RequireAuth is middleware that requires authentication func RequireAuth(store *db.SessionStore, database *sql.DB) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sessionID, ok := getSessionID(r) if !ok { handleUnauthenticated(w, r) return } sess, ok := store.Get(sessionID) if !ok { handleUnauthenticated(w, r) return } // Look up full user from database to get avatar user, err := db.GetUserByDID(database, sess.DID) if err != nil || user == nil { // Fallback to session data if DB lookup fails user = &db.User{ DID: sess.DID, Handle: sess.Handle, PDSEndpoint: sess.PDSEndpoint, } } ctx := context.WithValue(r.Context(), userKey, user) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // OptionalAuth is middleware that optionally includes user if authenticated func OptionalAuth(store *db.SessionStore, database *sql.DB) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sessionID, ok := getSessionID(r) if ok { if sess, ok := store.Get(sessionID); ok { // Look up full user from database to get avatar user, err := db.GetUserByDID(database, sess.DID) if err != nil || user == nil { // Fallback to session data if DB lookup fails user = &db.User{ DID: sess.DID, Handle: sess.Handle, PDSEndpoint: sess.PDSEndpoint, } } ctx := context.WithValue(r.Context(), userKey, user) r = r.WithContext(ctx) } } next.ServeHTTP(w, r) }) } } // 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 } // GetUser retrieves the user from the request context func GetUser(r *http.Request) *db.User { user, ok := r.Context().Value(userKey).(*db.User) if !ok { return nil } return user } // WithUser returns a new request with the user set in the context. // This is primarily useful for testing. func WithUser(r *http.Request, user *db.User) *http.Request { ctx := context.WithValue(r.Context(), userKey, user) return r.WithContext(ctx) }