Monorepo for Tangled
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 5b01f9975e7ec4e828e57202bd7cb87ff4e137df 364 lines 10 kB view raw
1package middleware 2 3import ( 4 "context" 5 "fmt" 6 "log" 7 "net/http" 8 "net/url" 9 "slices" 10 "strconv" 11 "strings" 12 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "github.com/go-chi/chi/v5" 16 "tangled.org/core/appview/db" 17 "tangled.org/core/appview/oauth" 18 "tangled.org/core/appview/pages" 19 "tangled.org/core/appview/pagination" 20 "tangled.org/core/appview/reporesolver" 21 "tangled.org/core/appview/state/userutil" 22 "tangled.org/core/idresolver" 23 "tangled.org/core/orm" 24 "tangled.org/core/rbac" 25) 26 27type Middleware struct { 28 oauth *oauth.OAuth 29 db *db.DB 30 enforcer *rbac.Enforcer 31 repoResolver *reporesolver.RepoResolver 32 idResolver *idresolver.Resolver 33 pages *pages.Pages 34} 35 36func New(oauth *oauth.OAuth, db *db.DB, enforcer *rbac.Enforcer, repoResolver *reporesolver.RepoResolver, idResolver *idresolver.Resolver, pages *pages.Pages) Middleware { 37 return Middleware{ 38 oauth: oauth, 39 db: db, 40 enforcer: enforcer, 41 repoResolver: repoResolver, 42 idResolver: idResolver, 43 pages: pages, 44 } 45} 46 47type middlewareFunc func(http.Handler) http.Handler 48 49func AuthMiddleware(o *oauth.OAuth) middlewareFunc { 50 return func(next http.Handler) http.Handler { 51 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 returnURL := "/" 53 if u, err := url.Parse(r.Header.Get("Referer")); err == nil { 54 returnURL = u.RequestURI() 55 } 56 57 loginURL := fmt.Sprintf("/login?return_url=%s", url.QueryEscape(returnURL)) 58 59 redirectFunc := func(w http.ResponseWriter, r *http.Request) { 60 http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) 61 } 62 if r.Header.Get("HX-Request") == "true" { 63 redirectFunc = func(w http.ResponseWriter, _ *http.Request) { 64 w.Header().Set("HX-Redirect", loginURL) 65 w.WriteHeader(http.StatusOK) 66 } 67 } 68 69 sess, err := o.ResumeSession(r) 70 if err != nil { 71 log.Println("failed to resume session, redirecting...", "err", err, "url", r.URL.String()) 72 redirectFunc(w, r) 73 return 74 } 75 76 if sess == nil { 77 log.Printf("session is nil, redirecting...") 78 redirectFunc(w, r) 79 return 80 } 81 82 next.ServeHTTP(w, r) 83 }) 84 } 85} 86 87func Paginate(next http.Handler) http.Handler { 88 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 89 page := pagination.FirstPage() 90 91 offsetVal := r.URL.Query().Get("offset") 92 if offsetVal != "" { 93 offset, err := strconv.Atoi(offsetVal) 94 if err != nil { 95 log.Println("invalid offset") 96 } else { 97 page.Offset = offset 98 } 99 } 100 101 limitVal := r.URL.Query().Get("limit") 102 if limitVal != "" { 103 limit, err := strconv.Atoi(limitVal) 104 if err != nil { 105 log.Println("invalid limit") 106 } else { 107 page.Limit = limit 108 } 109 } 110 111 ctx := pagination.IntoContext(r.Context(), page) 112 next.ServeHTTP(w, r.WithContext(ctx)) 113 }) 114} 115 116func (mw Middleware) knotRoleMiddleware(group string) middlewareFunc { 117 return func(next http.Handler) http.Handler { 118 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 119 // requires auth also 120 actor := mw.oauth.GetMultiAccountUser(r) 121 if actor == nil { 122 // we need a logged in user 123 log.Printf("not logged in, redirecting") 124 http.Error(w, "Forbiden", http.StatusUnauthorized) 125 return 126 } 127 domain := chi.URLParam(r, "domain") 128 if domain == "" { 129 http.Error(w, "malformed url", http.StatusBadRequest) 130 return 131 } 132 133 ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Active.Did, group, domain) 134 if err != nil || !ok { 135 log.Printf("%s does not have perms of a %s in domain %s", actor.Active.Did, group, domain) 136 http.Error(w, "Forbiden", http.StatusUnauthorized) 137 return 138 } 139 140 next.ServeHTTP(w, r) 141 }) 142 } 143} 144 145func (mw Middleware) KnotOwner() middlewareFunc { 146 return mw.knotRoleMiddleware("server:owner") 147} 148 149func (mw Middleware) RepoPermissionMiddleware(requiredPerm string) middlewareFunc { 150 return func(next http.Handler) http.Handler { 151 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 152 // requires auth also 153 actor := mw.oauth.GetMultiAccountUser(r) 154 if actor == nil { 155 // we need a logged in user 156 log.Printf("not logged in, redirecting") 157 http.Error(w, "Forbiden", http.StatusUnauthorized) 158 return 159 } 160 f, err := mw.repoResolver.Resolve(r) 161 if err != nil { 162 http.Error(w, "malformed url", http.StatusBadRequest) 163 return 164 } 165 166 ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.RepoIdentifier(), requiredPerm) 167 if err != nil || !ok { 168 log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.RepoIdentifier()) 169 http.Error(w, "Forbiden", http.StatusUnauthorized) 170 return 171 } 172 173 next.ServeHTTP(w, r) 174 }) 175 } 176} 177 178func (mw Middleware) ResolveIdent() middlewareFunc { 179 excluded := []string{"favicon.ico"} 180 181 return func(next http.Handler) http.Handler { 182 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 183 didOrHandle := chi.URLParam(req, "user") 184 didOrHandle = strings.TrimPrefix(didOrHandle, "@") 185 186 if slices.Contains(excluded, didOrHandle) { 187 next.ServeHTTP(w, req) 188 return 189 } 190 191 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 192 if err != nil { 193 if h, parseErr := syntax.ParseHandle(didOrHandle); parseErr == nil { 194 if did, lookupErr := db.GetDidByPreferredHandle(mw.db, h); lookupErr == nil { 195 id, err = mw.idResolver.ResolveIdent(req.Context(), string(did)) 196 } 197 } 198 } 199 if err != nil { 200 log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err) 201 mw.pages.Error404(w) 202 return 203 } 204 205 ctx := context.WithValue(req.Context(), "resolvedId", *id) 206 207 next.ServeHTTP(w, req.WithContext(ctx)) 208 }) 209 } 210} 211 212func (mw Middleware) ResolveRepo() middlewareFunc { 213 return func(next http.Handler) http.Handler { 214 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 215 repoName := chi.URLParam(req, "repo") 216 repoName = strings.TrimSuffix(repoName, ".git") 217 218 id, ok := req.Context().Value("resolvedId").(identity.Identity) 219 if !ok { 220 log.Println("malformed middleware") 221 w.WriteHeader(http.StatusInternalServerError) 222 return 223 } 224 225 repo, err := db.GetRepo( 226 mw.db, 227 orm.FilterEq("did", id.DID.String()), 228 orm.FilterEq("name", repoName), 229 ) 230 if err != nil { 231 log.Println("failed to resolve repo", "err", err) 232 w.WriteHeader(http.StatusNotFound) 233 mw.pages.ErrorKnot404(w) 234 return 235 } 236 237 ctx := context.WithValue(req.Context(), "repo", repo) 238 next.ServeHTTP(w, req.WithContext(ctx)) 239 }) 240 } 241} 242 243// middleware that is tacked on top of /{user}/{repo}/pulls/{pull} 244func (mw Middleware) ResolvePull() middlewareFunc { 245 return func(next http.Handler) http.Handler { 246 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 247 f, err := mw.repoResolver.Resolve(r) 248 if err != nil { 249 log.Println("failed to fully resolve repo", err) 250 w.WriteHeader(http.StatusNotFound) 251 mw.pages.ErrorKnot404(w) 252 return 253 } 254 255 prId := chi.URLParam(r, "pull") 256 prIdInt, err := strconv.Atoi(prId) 257 if err != nil { 258 log.Println("failed to parse pr id", err) 259 mw.pages.Error404(w) 260 return 261 } 262 263 pr, err := db.GetPull(mw.db, f.RepoAt(), prIdInt) 264 if err != nil { 265 log.Println("failed to get pull and comments", err) 266 mw.pages.Error404(w) 267 return 268 } 269 270 ctx := context.WithValue(r.Context(), "pull", pr) 271 272 if pr.IsStacked() { 273 stack, err := db.GetStack(mw.db, pr.StackId) 274 if err != nil { 275 log.Println("failed to get stack", err) 276 return 277 } 278 abandonedPulls, err := db.GetAbandonedPulls(mw.db, pr.StackId) 279 if err != nil { 280 log.Println("failed to get abandoned pulls", err) 281 return 282 } 283 284 ctx = context.WithValue(ctx, "stack", stack) 285 ctx = context.WithValue(ctx, "abandonedPulls", abandonedPulls) 286 } 287 288 next.ServeHTTP(w, r.WithContext(ctx)) 289 }) 290 } 291} 292 293// middleware that is tacked on top of /{user}/{repo}/issues/{issue} 294func (mw Middleware) ResolveIssue(next http.Handler) http.Handler { 295 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 296 f, err := mw.repoResolver.Resolve(r) 297 if err != nil { 298 log.Println("failed to fully resolve repo", err) 299 w.WriteHeader(http.StatusNotFound) 300 mw.pages.ErrorKnot404(w) 301 return 302 } 303 304 issueIdStr := chi.URLParam(r, "issue") 305 issueId, err := strconv.Atoi(issueIdStr) 306 if err != nil { 307 log.Println("failed to fully resolve issue ID", err) 308 mw.pages.Error404(w) 309 return 310 } 311 312 issue, err := db.GetIssue(mw.db, f.RepoAt(), issueId) 313 if err != nil { 314 log.Println("failed to get issues", "err", err) 315 mw.pages.Error404(w) 316 return 317 } 318 319 ctx := context.WithValue(r.Context(), "issue", issue) 320 next.ServeHTTP(w, r.WithContext(ctx)) 321 }) 322} 323 324// this should serve the go-import meta tag even if the path is technically 325// a 404 like tangled.sh/oppi.li/go-git/v5 326// 327// we're keeping the tangled.sh go-import tag too to maintain backward 328// compatiblity for modules that still point there. they will be redirected 329// to fetch source from tangled.org 330func (mw Middleware) GoImport() middlewareFunc { 331 return func(next http.Handler) http.Handler { 332 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 333 f, err := mw.repoResolver.Resolve(r) 334 if err != nil { 335 log.Println("failed to fully resolve repo", err) 336 w.WriteHeader(http.StatusNotFound) 337 mw.pages.ErrorKnot404(w) 338 return 339 } 340 341 fullName := reporesolver.GetBaseRepoPath(r, f) 342 343 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 344 if r.URL.Query().Get("go-get") == "1" { 345 modulePath := userutil.FlattenDid(fullName) 346 if strings.Contains(modulePath, ":") { 347 modulePath = userutil.FlattenDid(f.Did) + "/" + f.Name 348 } 349 html := fmt.Sprintf( 350 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/> 351<meta name="go-import" content="tangled.org/%s git https://tangled.org/%s"/>`, 352 modulePath, fullName, 353 modulePath, fullName, 354 ) 355 w.Header().Set("Content-Type", "text/html") 356 w.Write([]byte(html)) 357 return 358 } 359 } 360 361 next.ServeHTTP(w, r) 362 }) 363 } 364}