A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at refactor 158 lines 4.7 kB view raw
1// Package middleware provides HTTP middleware for AppView, including 2// authentication (session-based for web UI, token-based for registry), 3// identity resolution (handle/DID to PDS endpoint), and hold discovery 4// for routing blobs to storage endpoints. 5package middleware 6 7import ( 8 "context" 9 "database/sql" 10 "net/http" 11 "net/url" 12 13 "atcr.io/pkg/appview/db" 14 "atcr.io/pkg/auth" 15 "atcr.io/pkg/auth/oauth" 16) 17 18type contextKey string 19 20const userKey contextKey = "user" 21 22// WebAuthDeps contains dependencies for web auth middleware 23type WebAuthDeps struct { 24 SessionStore *db.SessionStore 25 Database *sql.DB 26 Refresher *oauth.Refresher 27 DefaultHoldDID string 28} 29 30// RequireAuth is middleware that requires authentication 31func RequireAuth(store *db.SessionStore, database *sql.DB) func(http.Handler) http.Handler { 32 return RequireAuthWithDeps(WebAuthDeps{ 33 SessionStore: store, 34 Database: database, 35 }) 36} 37 38// RequireAuthWithDeps is middleware that requires authentication and creates UserContext 39func RequireAuthWithDeps(deps WebAuthDeps) func(http.Handler) http.Handler { 40 return func(next http.Handler) http.Handler { 41 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 42 sessionID, ok := getSessionID(r) 43 if !ok { 44 // Build return URL with query parameters preserved 45 returnTo := r.URL.Path 46 if r.URL.RawQuery != "" { 47 returnTo = r.URL.Path + "?" + r.URL.RawQuery 48 } 49 http.Redirect(w, r, "/auth/oauth/login?return_to="+url.QueryEscape(returnTo), http.StatusFound) 50 return 51 } 52 53 sess, ok := deps.SessionStore.Get(sessionID) 54 if !ok { 55 // Build return URL with query parameters preserved 56 returnTo := r.URL.Path 57 if r.URL.RawQuery != "" { 58 returnTo = r.URL.Path + "?" + r.URL.RawQuery 59 } 60 http.Redirect(w, r, "/auth/oauth/login?return_to="+url.QueryEscape(returnTo), http.StatusFound) 61 return 62 } 63 64 // Look up full user from database to get avatar 65 user, err := db.GetUserByDID(deps.Database, sess.DID) 66 if err != nil || user == nil { 67 // Fallback to session data if DB lookup fails 68 user = &db.User{ 69 DID: sess.DID, 70 Handle: sess.Handle, 71 PDSEndpoint: sess.PDSEndpoint, 72 } 73 } 74 75 ctx := r.Context() 76 ctx = context.WithValue(ctx, userKey, user) 77 78 // Create UserContext for authenticated users (enables EnsureUserSetup) 79 if deps.Refresher != nil { 80 userCtx := auth.NewUserContext(sess.DID, auth.AuthMethodOAuth, r.Method, &auth.Dependencies{ 81 Refresher: deps.Refresher, 82 DefaultHoldDID: deps.DefaultHoldDID, 83 }) 84 userCtx.SetPDS(sess.Handle, sess.PDSEndpoint) 85 userCtx.EnsureUserSetup() 86 ctx = auth.WithUserContext(ctx, userCtx) 87 } 88 89 next.ServeHTTP(w, r.WithContext(ctx)) 90 }) 91 } 92} 93 94// OptionalAuth is middleware that optionally includes user if authenticated 95func OptionalAuth(store *db.SessionStore, database *sql.DB) func(http.Handler) http.Handler { 96 return OptionalAuthWithDeps(WebAuthDeps{ 97 SessionStore: store, 98 Database: database, 99 }) 100} 101 102// OptionalAuthWithDeps is middleware that optionally includes user and UserContext if authenticated 103func OptionalAuthWithDeps(deps WebAuthDeps) func(http.Handler) http.Handler { 104 return func(next http.Handler) http.Handler { 105 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 106 sessionID, ok := getSessionID(r) 107 if ok { 108 if sess, ok := deps.SessionStore.Get(sessionID); ok { 109 // Look up full user from database to get avatar 110 user, err := db.GetUserByDID(deps.Database, sess.DID) 111 if err != nil || user == nil { 112 // Fallback to session data if DB lookup fails 113 user = &db.User{ 114 DID: sess.DID, 115 Handle: sess.Handle, 116 PDSEndpoint: sess.PDSEndpoint, 117 } 118 } 119 120 ctx := r.Context() 121 ctx = context.WithValue(ctx, userKey, user) 122 123 // Create UserContext for authenticated users (enables EnsureUserSetup) 124 if deps.Refresher != nil { 125 userCtx := auth.NewUserContext(sess.DID, auth.AuthMethodOAuth, r.Method, &auth.Dependencies{ 126 Refresher: deps.Refresher, 127 DefaultHoldDID: deps.DefaultHoldDID, 128 }) 129 userCtx.SetPDS(sess.Handle, sess.PDSEndpoint) 130 userCtx.EnsureUserSetup() 131 ctx = auth.WithUserContext(ctx, userCtx) 132 } 133 134 r = r.WithContext(ctx) 135 } 136 } 137 next.ServeHTTP(w, r) 138 }) 139 } 140} 141 142// getSessionID gets session ID from cookie 143func getSessionID(r *http.Request) (string, bool) { 144 cookie, err := r.Cookie("atcr_session") 145 if err != nil { 146 return "", false 147 } 148 return cookie.Value, true 149} 150 151// GetUser retrieves the user from the request context 152func GetUser(r *http.Request) *db.User { 153 user, ok := r.Context().Value(userKey).(*db.User) 154 if !ok { 155 return nil 156 } 157 return user 158}