forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package state
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "log"
8 "net/http"
9 "path/filepath"
10
11 "github.com/bluesky-social/indigo/atproto/identity"
12 "github.com/go-chi/chi/v5"
13 "github.com/sotangled/tangled/appview/auth"
14 "github.com/sotangled/tangled/appview/pages"
15 "github.com/sotangled/tangled/types"
16)
17
18func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
19 f, err := fullyResolvedRepo(r)
20 if err != nil {
21 log.Println("failed to fully resolve repo", err)
22 return
23 }
24
25 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s", f.Knot, f.OwnerDid(), f.RepoName))
26 if err != nil {
27 log.Println("failed to reach knotserver", err)
28 return
29 }
30 defer resp.Body.Close()
31
32 body, err := io.ReadAll(resp.Body)
33 if err != nil {
34 log.Fatalf("Error reading response body: %v", err)
35 return
36 }
37
38 var result types.RepoIndexResponse
39 err = json.Unmarshal(body, &result)
40 if err != nil {
41 log.Fatalf("Error unmarshalling response body: %v", err)
42 return
43 }
44
45 log.Println(resp.Status, result)
46
47 user := s.auth.GetUser(r)
48 s.pages.RepoIndexPage(w, pages.RepoIndexParams{
49 LoggedInUser: user,
50 RepoInfo: pages.RepoInfo{
51 OwnerDid: f.OwnerDid(),
52 OwnerHandle: f.OwnerHandle(),
53 Name: f.RepoName,
54 SettingsAllowed: settingsAllowed(s, user, f),
55 },
56 RepoIndexResponse: result,
57 })
58
59 return
60}
61
62func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
63 f, err := fullyResolvedRepo(r)
64 if err != nil {
65 log.Println("failed to fully resolve repo", err)
66 return
67 }
68
69 ref := chi.URLParam(r, "ref")
70 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/log/%s", f.Knot, f.OwnerDid(), f.RepoName, ref))
71 if err != nil {
72 log.Println("failed to reach knotserver", err)
73 return
74 }
75
76 body, err := io.ReadAll(resp.Body)
77 if err != nil {
78 log.Fatalf("Error reading response body: %v", err)
79 return
80 }
81
82 var result types.RepoLogResponse
83 err = json.Unmarshal(body, &result)
84 if err != nil {
85 log.Println("failed to parse json response", err)
86 return
87 }
88
89 user := s.auth.GetUser(r)
90 s.pages.RepoLog(w, pages.RepoLogParams{
91 LoggedInUser: user,
92 RepoInfo: pages.RepoInfo{
93 OwnerDid: f.OwnerDid(),
94 OwnerHandle: f.OwnerHandle(),
95 Name: f.RepoName,
96 SettingsAllowed: settingsAllowed(s, user, f),
97 },
98 RepoLogResponse: result,
99 })
100 return
101}
102
103func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
104 f, err := fullyResolvedRepo(r)
105 if err != nil {
106 log.Println("failed to fully resolve repo", err)
107 return
108 }
109
110 ref := chi.URLParam(r, "ref")
111 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/commit/%s", f.Knot, f.OwnerDid(), f.RepoName, ref))
112 if err != nil {
113 log.Println("failed to reach knotserver", err)
114 return
115 }
116
117 body, err := io.ReadAll(resp.Body)
118 if err != nil {
119 log.Fatalf("Error reading response body: %v", err)
120 return
121 }
122
123 var result types.RepoCommitResponse
124 err = json.Unmarshal(body, &result)
125 if err != nil {
126 log.Println("failed to parse response:", err)
127 return
128 }
129
130 user := s.auth.GetUser(r)
131 s.pages.RepoCommit(w, pages.RepoCommitParams{
132 LoggedInUser: user,
133 RepoInfo: pages.RepoInfo{
134 OwnerDid: f.OwnerDid(),
135 OwnerHandle: f.OwnerHandle(),
136 Name: f.RepoName,
137 SettingsAllowed: settingsAllowed(s, user, f),
138 },
139 RepoCommitResponse: result,
140 })
141 return
142}
143
144func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
145 f, err := fullyResolvedRepo(r)
146 if err != nil {
147 log.Println("failed to fully resolve repo", err)
148 return
149 }
150
151 ref := chi.URLParam(r, "ref")
152 treePath := chi.URLParam(r, "*")
153 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tree/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
154 if err != nil {
155 log.Println("failed to reach knotserver", err)
156 return
157 }
158
159 body, err := io.ReadAll(resp.Body)
160 if err != nil {
161 log.Fatalf("Error reading response body: %v", err)
162 return
163 }
164
165 var result types.RepoTreeResponse
166 err = json.Unmarshal(body, &result)
167 if err != nil {
168 log.Println("failed to parse response:", err)
169 return
170 }
171
172 log.Println(result)
173
174 user := s.auth.GetUser(r)
175 s.pages.RepoTree(w, pages.RepoTreeParams{
176 LoggedInUser: user,
177 RepoInfo: pages.RepoInfo{
178 OwnerDid: f.OwnerDid(),
179 OwnerHandle: f.OwnerHandle(),
180 Name: f.RepoName,
181 SettingsAllowed: settingsAllowed(s, user, f),
182 },
183 RepoTreeResponse: result,
184 })
185 return
186}
187
188func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
189 f, err := fullyResolvedRepo(r)
190 if err != nil {
191 log.Println("failed to get repo and knot", err)
192 return
193 }
194
195 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tags", f.Knot, f.OwnerDid(), f.RepoName))
196 if err != nil {
197 log.Println("failed to reach knotserver", err)
198 return
199 }
200
201 body, err := io.ReadAll(resp.Body)
202 if err != nil {
203 log.Fatalf("Error reading response body: %v", err)
204 return
205 }
206
207 var result types.RepoTagsResponse
208 err = json.Unmarshal(body, &result)
209 if err != nil {
210 log.Println("failed to parse response:", err)
211 return
212 }
213
214 user := s.auth.GetUser(r)
215 s.pages.RepoTags(w, pages.RepoTagsParams{
216 LoggedInUser: user,
217 RepoInfo: pages.RepoInfo{
218 OwnerDid: f.OwnerDid(),
219 OwnerHandle: f.OwnerHandle(),
220 Name: f.RepoName,
221 SettingsAllowed: settingsAllowed(s, user, f),
222 },
223 RepoTagsResponse: result,
224 })
225 return
226}
227
228func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
229 f, err := fullyResolvedRepo(r)
230 if err != nil {
231 log.Println("failed to get repo and knot", err)
232 return
233 }
234
235 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/branches", f.Knot, f.OwnerDid(), f.RepoName))
236 if err != nil {
237 log.Println("failed to reach knotserver", err)
238 return
239 }
240
241 body, err := io.ReadAll(resp.Body)
242 if err != nil {
243 log.Fatalf("Error reading response body: %v", err)
244 return
245 }
246
247 var result types.RepoBranchesResponse
248 err = json.Unmarshal(body, &result)
249 if err != nil {
250 log.Println("failed to parse response:", err)
251 return
252 }
253
254 log.Println(result)
255
256 user := s.auth.GetUser(r)
257 s.pages.RepoBranches(w, pages.RepoBranchesParams{
258 LoggedInUser: user,
259 RepoInfo: pages.RepoInfo{
260 OwnerDid: f.OwnerDid(),
261 OwnerHandle: f.OwnerHandle(),
262 Name: f.RepoName,
263 SettingsAllowed: settingsAllowed(s, user, f),
264 },
265 RepoBranchesResponse: result,
266 })
267 return
268}
269
270func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
271 f, err := fullyResolvedRepo(r)
272 if err != nil {
273 log.Println("failed to get repo and knot", err)
274 return
275 }
276
277 ref := chi.URLParam(r, "ref")
278 filePath := chi.URLParam(r, "*")
279 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/blob/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
280 if err != nil {
281 log.Println("failed to reach knotserver", err)
282 return
283 }
284
285 body, err := io.ReadAll(resp.Body)
286 if err != nil {
287 log.Fatalf("Error reading response body: %v", err)
288 return
289 }
290
291 var result types.RepoBlobResponse
292 err = json.Unmarshal(body, &result)
293 if err != nil {
294 log.Println("failed to parse response:", err)
295 return
296 }
297
298 user := s.auth.GetUser(r)
299 s.pages.RepoBlob(w, pages.RepoBlobParams{
300 LoggedInUser: user,
301 RepoInfo: pages.RepoInfo{
302 OwnerDid: f.OwnerDid(),
303 OwnerHandle: f.OwnerHandle(),
304 Name: f.RepoName,
305 SettingsAllowed: settingsAllowed(s, user, f),
306 },
307 RepoBlobResponse: result,
308 })
309 return
310}
311
312func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
313 f, err := fullyResolvedRepo(r)
314 if err != nil {
315 log.Println("failed to get repo and knot", err)
316 return
317 }
318
319 collaborator := r.FormValue("collaborator")
320 if collaborator == "" {
321 http.Error(w, "malformed form", http.StatusBadRequest)
322 return
323 }
324
325 collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
326 if err != nil {
327 w.Write([]byte("failed to resolve collaborator did to a handle"))
328 return
329 }
330 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
331
332 // TODO: create an atproto record for this
333
334 secret, err := s.db.GetRegistrationKey(f.Knot)
335 if err != nil {
336 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
337 return
338 }
339
340 ksClient, err := NewSignedClient(f.Knot, secret)
341 if err != nil {
342 log.Println("failed to create client to ", f.Knot)
343 return
344 }
345
346 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
347 if err != nil {
348 log.Printf("failed to make request to %s: %s", f.Knot, err)
349 return
350 }
351
352 if ksResp.StatusCode != http.StatusNoContent {
353 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
354 return
355 }
356
357 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo())
358 if err != nil {
359 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
360 return
361 }
362
363 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
364
365}
366
367func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
368 f, err := fullyResolvedRepo(r)
369 if err != nil {
370 log.Println("failed to get repo and knot", err)
371 return
372 }
373
374 switch r.Method {
375 case http.MethodGet:
376 // for now, this is just pubkeys
377 user := s.auth.GetUser(r)
378 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
379 if err != nil {
380 log.Println("failed to get collaborators", err)
381 }
382 log.Println(repoCollaborators)
383
384 isCollaboratorInviteAllowed := false
385 if user != nil {
386 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo())
387 if err == nil && ok {
388 isCollaboratorInviteAllowed = true
389 }
390 }
391
392 s.pages.RepoSettings(w, pages.RepoSettingsParams{
393 LoggedInUser: user,
394 RepoInfo: pages.RepoInfo{
395 OwnerDid: f.OwnerDid(),
396 OwnerHandle: f.OwnerHandle(),
397 Name: f.RepoName,
398 SettingsAllowed: settingsAllowed(s, user, f),
399 },
400 Collaborators: repoCollaborators,
401 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
402 })
403 }
404}
405
406type FullyResolvedRepo struct {
407 Knot string
408 OwnerId identity.Identity
409 RepoName string
410}
411
412func (f *FullyResolvedRepo) OwnerDid() string {
413 return f.OwnerId.DID.String()
414}
415
416func (f *FullyResolvedRepo) OwnerHandle() string {
417 return f.OwnerId.Handle.String()
418}
419
420func (f *FullyResolvedRepo) OwnerSlashRepo() string {
421 return filepath.Join(f.OwnerDid(), f.RepoName)
422}
423
424func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
425 repoName := chi.URLParam(r, "repo")
426 knot, ok := r.Context().Value("knot").(string)
427 if !ok {
428 log.Println("malformed middleware")
429 return nil, fmt.Errorf("malformed middleware")
430 }
431 id, ok := r.Context().Value("resolvedId").(identity.Identity)
432 if !ok {
433 log.Println("malformed middleware")
434 return nil, fmt.Errorf("malformed middleware")
435 }
436
437 return &FullyResolvedRepo{
438 Knot: knot,
439 OwnerId: id,
440 RepoName: repoName,
441 }, nil
442}
443
444func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool {
445 settingsAllowed := false
446 if u != nil {
447 ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo())
448 if err == nil && ok {
449 settingsAllowed = true
450 }
451 }
452
453 return settingsAllowed
454}