A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
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}