1package state
2
3import (
4 "context"
5 "log"
6 "net/http"
7 "strconv"
8 "strings"
9 "time"
10
11 "slices"
12
13 "github.com/bluesky-social/indigo/atproto/identity"
14 "github.com/go-chi/chi/v5"
15 "tangled.sh/tangled.sh/core/appview/db"
16 "tangled.sh/tangled.sh/core/appview/middleware"
17)
18
19func knotRoleMiddleware(s *State, group string) middleware.Middleware {
20 return func(next http.Handler) http.Handler {
21 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22 // requires auth also
23 actor := s.auth.GetUser(r)
24 if actor == nil {
25 // we need a logged in user
26 log.Printf("not logged in, redirecting")
27 http.Error(w, "Forbiden", http.StatusUnauthorized)
28 return
29 }
30 domain := chi.URLParam(r, "domain")
31 if domain == "" {
32 http.Error(w, "malformed url", http.StatusBadRequest)
33 return
34 }
35
36 ok, err := s.enforcer.E.HasGroupingPolicy(actor.Did, group, domain)
37 if err != nil || !ok {
38 // we need a logged in user
39 log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain)
40 http.Error(w, "Forbiden", http.StatusUnauthorized)
41 return
42 }
43
44 next.ServeHTTP(w, r)
45 })
46 }
47}
48
49func KnotOwner(s *State) middleware.Middleware {
50 return knotRoleMiddleware(s, "server:owner")
51}
52
53func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware {
54 return func(next http.Handler) http.Handler {
55 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56 // requires auth also
57 actor := s.auth.GetUser(r)
58 if actor == nil {
59 // we need a logged in user
60 log.Printf("not logged in, redirecting")
61 http.Error(w, "Forbiden", http.StatusUnauthorized)
62 return
63 }
64 f, err := fullyResolvedRepo(r)
65 if err != nil {
66 http.Error(w, "malformed url", http.StatusBadRequest)
67 return
68 }
69
70 ok, err := s.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
71 if err != nil || !ok {
72 // we need a logged in user
73 log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.OwnerSlashRepo())
74 http.Error(w, "Forbiden", http.StatusUnauthorized)
75 return
76 }
77
78 next.ServeHTTP(w, r)
79 })
80 }
81}
82
83func StripLeadingAt(next http.Handler) http.Handler {
84 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
85 path := req.URL.EscapedPath()
86 if strings.HasPrefix(path, "/@") {
87 req.URL.RawPath = "/" + strings.TrimPrefix(path, "/@")
88 }
89 next.ServeHTTP(w, req)
90 })
91}
92
93func ResolveIdent(s *State) middleware.Middleware {
94 excluded := []string{"favicon.ico"}
95
96 return func(next http.Handler) http.Handler {
97 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
98 didOrHandle := chi.URLParam(req, "user")
99 if slices.Contains(excluded, didOrHandle) {
100 next.ServeHTTP(w, req)
101 return
102 }
103
104 id, err := s.resolver.ResolveIdent(req.Context(), didOrHandle)
105 if err != nil {
106 // invalid did or handle
107 log.Println("failed to resolve did/handle:", err)
108 w.WriteHeader(http.StatusNotFound)
109 return
110 }
111
112 ctx := context.WithValue(req.Context(), "resolvedId", *id)
113
114 next.ServeHTTP(w, req.WithContext(ctx))
115 })
116 }
117}
118
119func ResolveRepo(s *State) middleware.Middleware {
120 return func(next http.Handler) http.Handler {
121 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
122 repoName := chi.URLParam(req, "repo")
123 id, ok := req.Context().Value("resolvedId").(identity.Identity)
124 if !ok {
125 log.Println("malformed middleware")
126 w.WriteHeader(http.StatusInternalServerError)
127 return
128 }
129
130 repo, err := db.GetRepo(s.db, id.DID.String(), repoName)
131 if err != nil {
132 // invalid did or handle
133 log.Println("failed to resolve repo")
134 w.WriteHeader(http.StatusNotFound)
135 return
136 }
137
138 ctx := context.WithValue(req.Context(), "knot", repo.Knot)
139 ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
140 ctx = context.WithValue(ctx, "repoDescription", repo.Description)
141 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
142 next.ServeHTTP(w, req.WithContext(ctx))
143 })
144 }
145}
146
147// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
148func ResolvePull(s *State) middleware.Middleware {
149 return func(next http.Handler) http.Handler {
150 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151 f, err := fullyResolvedRepo(r)
152 if err != nil {
153 log.Println("failed to fully resolve repo", err)
154 http.Error(w, "invalid repo url", http.StatusNotFound)
155 return
156 }
157
158 prId := chi.URLParam(r, "pull")
159 prIdInt, err := strconv.Atoi(prId)
160 if err != nil {
161 http.Error(w, "bad pr id", http.StatusBadRequest)
162 log.Println("failed to parse pr id", err)
163 return
164 }
165
166 pr, err := db.GetPull(s.db, f.RepoAt, prIdInt)
167 if err != nil {
168 log.Println("failed to get pull and comments", err)
169 return
170 }
171
172 ctx := context.WithValue(r.Context(), "pull", pr)
173
174 next.ServeHTTP(w, r.WithContext(ctx))
175 })
176 }
177}