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