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