Files
2025-10-24 01:05:19 -05:00

106 lines
3.0 KiB
Go

// 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"
// 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 {
// 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)
return
}
sess, ok := store.Get(sessionID)
if !ok {
// 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)
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
}