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 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
397
398}
399
400func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
401 f, err := fullyResolvedRepo(r)
402 if err != nil {
403 log.Println("failed to get repo and knot", err)
404 return
405 }
406
407 switch r.Method {
408 case http.MethodGet:
409 // for now, this is just pubkeys
410 user := s.auth.GetUser(r)
411 repoCollaborators, err := f.Collaborators(r.Context(), s)
412 if err != nil {
413 log.Println("failed to get collaborators", err)
414 }
415
416 isCollaboratorInviteAllowed := false
417 if user != nil {
418 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo())
419 if err == nil && ok {
420 isCollaboratorInviteAllowed = true
421 }
422 }
423
424 s.pages.RepoSettings(w, pages.RepoSettingsParams{
425 LoggedInUser: user,
426 RepoInfo: pages.RepoInfo{
427 OwnerDid: f.OwnerDid(),
428 OwnerHandle: f.OwnerHandle(),
429 Name: f.RepoName,
430 SettingsAllowed: settingsAllowed(s, user, f),
431 },
432 Collaborators: repoCollaborators,
433 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
434 })
435 }
436}
437
438type FullyResolvedRepo struct {
439 Knot string
440 OwnerId identity.Identity
441 RepoName string
442}
443
444func (f *FullyResolvedRepo) OwnerDid() string {
445 return f.OwnerId.DID.String()
446}
447
448func (f *FullyResolvedRepo) OwnerHandle() string {
449 return f.OwnerId.Handle.String()
450}
451
452func (f *FullyResolvedRepo) OwnerSlashRepo() string {
453 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
454 return p
455}
456
457func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) {
458 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
459 if err != nil {
460 return nil, err
461 }
462
463 var collaborators []pages.Collaborator
464 for _, item := range repoCollaborators {
465 // currently only two roles: owner and member
466 var role string
467 if item[3] == "repo:owner" {
468 role = "owner"
469 } else if item[3] == "repo:collaborator" {
470 role = "collaborator"
471 } else {
472 continue
473 }
474
475 did := item[0]
476
477 var handle string
478 id, err := s.resolver.ResolveIdent(ctx, did)
479 if err != nil {
480 handle = ""
481 } else {
482 handle = string(id.Handle)
483 }
484
485 c := pages.Collaborator{
486 Did: did,
487 Handle: handle,
488 Role: role,
489 }
490 collaborators = append(collaborators, c)
491 }
492
493 return collaborators, nil
494}
495
496func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
497 repoName := chi.URLParam(r, "repo")
498 knot, ok := r.Context().Value("knot").(string)
499 if !ok {
500 log.Println("malformed middleware")
501 return nil, fmt.Errorf("malformed middleware")
502 }
503 id, ok := r.Context().Value("resolvedId").(identity.Identity)
504 if !ok {
505 log.Println("malformed middleware")
506 return nil, fmt.Errorf("malformed middleware")
507 }
508
509 return &FullyResolvedRepo{
510 Knot: knot,
511 OwnerId: id,
512 RepoName: repoName,
513 }, nil
514}
515
516func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool {
517 settingsAllowed := false
518 if u != nil {
519 ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo())
520 if err == nil && ok {
521 settingsAllowed = true
522 } else {
523 log.Println(err, ok)
524 }
525 }
526
527 return settingsAllowed
528}