this repo has no description
1package state
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "log"
9 "math/rand/v2"
10 "net/http"
11 "path"
12 "slices"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/bluesky-social/indigo/atproto/identity"
18 "github.com/bluesky-social/indigo/atproto/syntax"
19 securejoin "github.com/cyphar/filepath-securejoin"
20 "github.com/go-chi/chi/v5"
21 "github.com/sotangled/tangled/api/tangled"
22 "github.com/sotangled/tangled/appview/auth"
23 "github.com/sotangled/tangled/appview/db"
24 "github.com/sotangled/tangled/appview/pages"
25 "github.com/sotangled/tangled/types"
26
27 comatproto "github.com/bluesky-social/indigo/api/atproto"
28 lexutil "github.com/bluesky-social/indigo/lex/util"
29)
30
31func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
32 ref := chi.URLParam(r, "ref")
33 f, err := fullyResolvedRepo(r)
34 if err != nil {
35 log.Println("failed to fully resolve repo", err)
36 return
37 }
38
39 us, err := NewUnsignedClient(f.Knot, s.config.Dev)
40 if err != nil {
41 log.Printf("failed to create unsigned client for %s", f.Knot)
42 s.pages.Error503(w)
43 return
44 }
45
46 resp, err := us.Index(f.OwnerDid(), f.RepoName, ref)
47 if err != nil {
48 s.pages.Error503(w)
49 log.Println("failed to reach knotserver", err)
50 return
51 }
52 defer resp.Body.Close()
53
54 body, err := io.ReadAll(resp.Body)
55 if err != nil {
56 log.Printf("Error reading response body: %v", err)
57 return
58 }
59
60 var result types.RepoIndexResponse
61 err = json.Unmarshal(body, &result)
62 if err != nil {
63 log.Printf("Error unmarshalling response body: %v", err)
64 return
65 }
66
67 tagMap := make(map[string][]string)
68 for _, tag := range result.Tags {
69 hash := tag.Hash
70 tagMap[hash] = append(tagMap[hash], tag.Name)
71 }
72
73 for _, branch := range result.Branches {
74 hash := branch.Hash
75 tagMap[hash] = append(tagMap[hash], branch.Name)
76 }
77
78 user := s.auth.GetUser(r)
79 s.pages.RepoIndexPage(w, pages.RepoIndexParams{
80 LoggedInUser: user,
81 RepoInfo: f.RepoInfo(s, user),
82 TagMap: tagMap,
83 RepoIndexResponse: result,
84 })
85
86 return
87}
88
89func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
90 f, err := fullyResolvedRepo(r)
91 if err != nil {
92 log.Println("failed to fully resolve repo", err)
93 return
94 }
95
96 page := 1
97 if r.URL.Query().Get("page") != "" {
98 page, err = strconv.Atoi(r.URL.Query().Get("page"))
99 if err != nil {
100 page = 1
101 }
102 }
103
104 ref := chi.URLParam(r, "ref")
105
106 protocol := "http"
107 if !s.config.Dev {
108 protocol = "https"
109 }
110
111 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/log/%s?page=%d&per_page=30", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, page))
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.Printf("error reading response body: %v", err)
120 return
121 }
122
123 var repolog types.RepoLogResponse
124 err = json.Unmarshal(body, &repolog)
125 if err != nil {
126 log.Println("failed to parse json response", err)
127 return
128 }
129
130 user := s.auth.GetUser(r)
131 s.pages.RepoLog(w, pages.RepoLogParams{
132 LoggedInUser: user,
133 RepoInfo: f.RepoInfo(s, user),
134 RepoLogResponse: repolog,
135 })
136 return
137}
138
139func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
140 f, err := fullyResolvedRepo(r)
141 if err != nil {
142 log.Println("failed to get repo and knot", err)
143 w.WriteHeader(http.StatusBadRequest)
144 return
145 }
146
147 user := s.auth.GetUser(r)
148 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
149 RepoInfo: f.RepoInfo(s, user),
150 })
151 return
152}
153
154func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {
155 f, err := fullyResolvedRepo(r)
156 if err != nil {
157 log.Println("failed to get repo and knot", err)
158 w.WriteHeader(http.StatusBadRequest)
159 return
160 }
161
162 repoAt := f.RepoAt
163 rkey := repoAt.RecordKey().String()
164 if rkey == "" {
165 log.Println("invalid aturi for repo", err)
166 w.WriteHeader(http.StatusInternalServerError)
167 return
168 }
169
170 user := s.auth.GetUser(r)
171
172 switch r.Method {
173 case http.MethodGet:
174 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
175 RepoInfo: f.RepoInfo(s, user),
176 })
177 return
178 case http.MethodPut:
179 user := s.auth.GetUser(r)
180 newDescription := r.FormValue("description")
181 client, _ := s.auth.AuthorizedClient(r)
182
183 // optimistic update
184 err = db.UpdateDescription(s.db, string(repoAt), newDescription)
185 if err != nil {
186 log.Println("failed to perferom update-description query", err)
187 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
188 return
189 }
190
191 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
192 //
193 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
194 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey)
195 if err != nil {
196 // failed to get record
197 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
198 return
199 }
200 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
201 Collection: tangled.RepoNSID,
202 Repo: user.Did,
203 Rkey: rkey,
204 SwapRecord: ex.Cid,
205 Record: &lexutil.LexiconTypeDecoder{
206 Val: &tangled.Repo{
207 Knot: f.Knot,
208 Name: f.RepoName,
209 Owner: user.Did,
210 AddedAt: &f.AddedAt,
211 Description: &newDescription,
212 },
213 },
214 })
215
216 if err != nil {
217 log.Println("failed to perferom update-description query", err)
218 // failed to get record
219 s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
220 return
221 }
222
223 newRepoInfo := f.RepoInfo(s, user)
224 newRepoInfo.Description = newDescription
225
226 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
227 RepoInfo: newRepoInfo,
228 })
229 return
230 }
231}
232
233func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) {
234 user := s.auth.GetUser(r)
235 f, err := fullyResolvedRepo(r)
236 if err != nil {
237 log.Println("failed to get repo and knot", err)
238 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
239 return
240 }
241
242 prId := chi.URLParam(r, "pull")
243 prIdInt, err := strconv.Atoi(prId)
244 if err != nil {
245 http.Error(w, "bad pr id", http.StatusBadRequest)
246 log.Println("failed to parse pr id", err)
247 return
248 }
249
250 patch := r.FormValue("patch")
251 if patch == "" {
252 s.pages.Notice(w, "pull-error", "Patch is required.")
253 return
254 }
255
256 // Get pull information before updating to get the atproto record URI
257 pull, _, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
258 if err != nil {
259 log.Println("failed to get pull information", err)
260 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
261 return
262 }
263
264 // Start a transaction for database operations
265 tx, err := s.db.BeginTx(r.Context(), nil)
266 if err != nil {
267 log.Println("failed to start transaction", err)
268 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
269 return
270 }
271
272 // Set up deferred rollback that will be overridden by commit if successful
273 defer tx.Rollback()
274
275 // Update patch in the database within transaction
276 err = db.EditPatch(tx, f.RepoAt, prIdInt, patch)
277 if err != nil {
278 log.Println("failed to update patch", err)
279 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
280 return
281 }
282
283 // Update the atproto record
284 client, _ := s.auth.AuthorizedClient(r)
285 pullAt := pull.PullAt
286
287 // Get the existing record first
288 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pullAt.RecordKey().String())
289 if err != nil {
290 log.Println("failed to get existing pull record", err)
291 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
292 return
293 }
294
295 // Update the record
296 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
297 Collection: tangled.RepoPullNSID,
298 Repo: user.Did,
299 Rkey: pullAt.RecordKey().String(),
300 SwapRecord: ex.Cid,
301 Record: &lexutil.LexiconTypeDecoder{
302 Val: &tangled.RepoPull{
303 Title: pull.Title,
304 PullId: int64(pull.PullId),
305 TargetRepo: string(f.RepoAt),
306 TargetBranch: pull.TargetBranch,
307 Patch: patch,
308 },
309 },
310 })
311
312 if err != nil {
313 log.Println("failed to update pull record in atproto", err)
314 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
315 return
316 }
317
318 // Commit the transaction now that both operations have succeeded
319 err = tx.Commit()
320 if err != nil {
321 log.Println("failed to commit transaction", err)
322 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
323 return
324 }
325
326 targetBranch := pull.TargetBranch
327
328 // Perform merge check
329 secret, err := db.GetRegistrationKey(s.db, f.Knot)
330 if err != nil {
331 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
332 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
333 return
334 }
335
336 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
337 if err != nil {
338 log.Printf("failed to create signed client for %s", f.Knot)
339 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
340 return
341 }
342
343 resp, err := ksClient.MergeCheck([]byte(patch), user.Did, f.RepoName, targetBranch)
344 if err != nil {
345 log.Println("failed to check mergeability", err)
346 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
347 return
348 }
349
350 respBody, err := io.ReadAll(resp.Body)
351 if err != nil {
352 log.Println("failed to read knotserver response body")
353 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
354 return
355 }
356
357 var mergeCheckResponse types.MergeCheckResponse
358 err = json.Unmarshal(respBody, &mergeCheckResponse)
359 if err != nil {
360 log.Println("failed to unmarshal merge check response", err)
361 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
362 return
363 }
364
365 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, prIdInt))
366 return
367}
368
369func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
370 user := s.auth.GetUser(r)
371 f, err := fullyResolvedRepo(r)
372 if err != nil {
373 log.Println("failed to get repo and knot", err)
374 return
375 }
376
377 switch r.Method {
378 case http.MethodGet:
379 us, err := NewUnsignedClient(f.Knot, s.config.Dev)
380 if err != nil {
381 log.Printf("failed to create unsigned client for %s", f.Knot)
382 s.pages.Error503(w)
383 return
384 }
385
386 resp, err := us.Branches(f.OwnerDid(), f.RepoName)
387 if err != nil {
388 log.Println("failed to reach knotserver", err)
389 return
390 }
391
392 body, err := io.ReadAll(resp.Body)
393 if err != nil {
394 log.Printf("Error reading response body: %v", err)
395 return
396 }
397
398 var result types.RepoBranchesResponse
399 err = json.Unmarshal(body, &result)
400 if err != nil {
401 log.Println("failed to parse response:", err)
402 return
403 }
404
405 s.pages.RepoNewPull(w, pages.RepoNewPullParams{
406 LoggedInUser: user,
407 RepoInfo: f.RepoInfo(s, user),
408 Branches: result.Branches,
409 })
410 case http.MethodPost:
411 title := r.FormValue("title")
412 body := r.FormValue("body")
413 targetBranch := r.FormValue("targetBranch")
414 patch := r.FormValue("patch")
415
416 if title == "" || body == "" || patch == "" {
417 s.pages.Notice(w, "pull", "Title, body and patch diff are required.")
418 return
419 }
420
421 tx, err := s.db.BeginTx(r.Context(), nil)
422 if err != nil {
423 log.Println("failed to start tx")
424 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
425 return
426 }
427
428 defer func() {
429 tx.Rollback()
430 err = s.enforcer.E.LoadPolicy()
431 if err != nil {
432 log.Println("failed to rollback policies")
433 }
434 }()
435
436 err = db.NewPull(tx, &db.Pull{
437 Title: title,
438 Body: body,
439 TargetBranch: targetBranch,
440 Patch: patch,
441 OwnerDid: user.Did,
442 RepoAt: f.RepoAt,
443 })
444 if err != nil {
445 log.Println("failed to create pull request", err)
446 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
447 return
448 }
449 client, _ := s.auth.AuthorizedClient(r)
450 pullId, err := db.NextPullId(s.db, f.RepoAt)
451 if err != nil {
452 log.Println("failed to get pull id", err)
453 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
454 return
455 }
456
457 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
458 Collection: tangled.RepoPullNSID,
459 Repo: user.Did,
460 Rkey: s.TID(),
461 Record: &lexutil.LexiconTypeDecoder{
462 Val: &tangled.RepoPull{
463 Title: title,
464 PullId: int64(pullId),
465 TargetRepo: string(f.RepoAt),
466 TargetBranch: targetBranch,
467 Patch: patch,
468 },
469 },
470 })
471
472 err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri)
473 if err != nil {
474 log.Println("failed to get pull id", err)
475 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
476 return
477 }
478
479 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
480 return
481 }
482}
483
484func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
485 user := s.auth.GetUser(r)
486 f, err := fullyResolvedRepo(r)
487 if err != nil {
488 log.Println("failed to get repo and knot", err)
489 return
490 }
491
492 prId := chi.URLParam(r, "pull")
493 prIdInt, err := strconv.Atoi(prId)
494 if err != nil {
495 http.Error(w, "bad pr id", http.StatusBadRequest)
496 log.Println("failed to parse pr id", err)
497 return
498 }
499
500 pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
501 if err != nil {
502 log.Println("failed to get pr and comments", err)
503 s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.")
504 return
505 }
506
507 pullOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), pr.OwnerDid)
508 if err != nil {
509 log.Println("failed to resolve pull owner", err)
510 }
511
512 identsToResolve := make([]string, len(comments))
513 for i, comment := range comments {
514 identsToResolve[i] = comment.OwnerDid
515 }
516 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
517 didHandleMap := make(map[string]string)
518 for _, identity := range resolvedIds {
519 if !identity.Handle.IsInvalidHandle() {
520 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
521 } else {
522 didHandleMap[identity.DID.String()] = identity.DID.String()
523 }
524 }
525
526 secret, err := db.GetRegistrationKey(s.db, f.Knot)
527 if err != nil {
528 log.Printf("failed to get registration key for %s", f.Knot)
529 s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.")
530 return
531 }
532
533 var mergeCheckResponse types.MergeCheckResponse
534 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
535 if err == nil {
536 resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch)
537 if err != nil {
538 log.Println("failed to check for mergeability:", err)
539 } else {
540 respBody, err := io.ReadAll(resp.Body)
541 if err != nil {
542 log.Println("failed to read merge check response body")
543 } else {
544 err = json.Unmarshal(respBody, &mergeCheckResponse)
545 if err != nil {
546 log.Println("failed to unmarshal merge check response", err)
547 }
548 }
549 }
550 } else {
551 log.Printf("failed to setup signed client for %s; ignoring...", f.Knot)
552 }
553
554 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
555 LoggedInUser: user,
556 RepoInfo: f.RepoInfo(s, user),
557 Pull: *pr,
558 Comments: comments,
559 PullOwnerHandle: pullOwnerIdent.Handle.String(),
560 DidHandleMap: didHandleMap,
561 MergeCheck: mergeCheckResponse,
562 })
563}
564
565func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
566 f, err := fullyResolvedRepo(r)
567 if err != nil {
568 log.Println("failed to fully resolve repo", err)
569 return
570 }
571 ref := chi.URLParam(r, "ref")
572 protocol := "http"
573 if !s.config.Dev {
574 protocol = "https"
575 }
576 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref))
577 if err != nil {
578 log.Println("failed to reach knotserver", err)
579 return
580 }
581
582 body, err := io.ReadAll(resp.Body)
583 if err != nil {
584 log.Printf("Error reading response body: %v", err)
585 return
586 }
587
588 var result types.RepoCommitResponse
589 err = json.Unmarshal(body, &result)
590 if err != nil {
591 log.Println("failed to parse response:", err)
592 return
593 }
594
595 user := s.auth.GetUser(r)
596 s.pages.RepoCommit(w, pages.RepoCommitParams{
597 LoggedInUser: user,
598 RepoInfo: f.RepoInfo(s, user),
599 RepoCommitResponse: result,
600 })
601 return
602}
603
604func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
605 f, err := fullyResolvedRepo(r)
606 if err != nil {
607 log.Println("failed to fully resolve repo", err)
608 return
609 }
610
611 ref := chi.URLParam(r, "ref")
612 treePath := chi.URLParam(r, "*")
613 protocol := "http"
614 if !s.config.Dev {
615 protocol = "https"
616 }
617 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
618 if err != nil {
619 log.Println("failed to reach knotserver", err)
620 return
621 }
622
623 body, err := io.ReadAll(resp.Body)
624 if err != nil {
625 log.Printf("Error reading response body: %v", err)
626 return
627 }
628
629 var result types.RepoTreeResponse
630 err = json.Unmarshal(body, &result)
631 if err != nil {
632 log.Println("failed to parse response:", err)
633 return
634 }
635
636 user := s.auth.GetUser(r)
637
638 var breadcrumbs [][]string
639 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)})
640 if treePath != "" {
641 for idx, elem := range strings.Split(treePath, "/") {
642 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
643 }
644 }
645
646 baseTreeLink := path.Join(f.OwnerDid(), f.RepoName, "tree", ref, treePath)
647 baseBlobLink := path.Join(f.OwnerDid(), f.RepoName, "blob", ref, treePath)
648
649 s.pages.RepoTree(w, pages.RepoTreeParams{
650 LoggedInUser: user,
651 BreadCrumbs: breadcrumbs,
652 BaseTreeLink: baseTreeLink,
653 BaseBlobLink: baseBlobLink,
654 RepoInfo: f.RepoInfo(s, user),
655 RepoTreeResponse: result,
656 })
657 return
658}
659
660func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
661 f, err := fullyResolvedRepo(r)
662 if err != nil {
663 log.Println("failed to get repo and knot", err)
664 return
665 }
666
667 protocol := "http"
668 if !s.config.Dev {
669 protocol = "https"
670 }
671
672 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tags", protocol, f.Knot, f.OwnerDid(), f.RepoName))
673 if err != nil {
674 log.Println("failed to reach knotserver", err)
675 return
676 }
677
678 body, err := io.ReadAll(resp.Body)
679 if err != nil {
680 log.Printf("Error reading response body: %v", err)
681 return
682 }
683
684 var result types.RepoTagsResponse
685 err = json.Unmarshal(body, &result)
686 if err != nil {
687 log.Println("failed to parse response:", err)
688 return
689 }
690
691 user := s.auth.GetUser(r)
692 s.pages.RepoTags(w, pages.RepoTagsParams{
693 LoggedInUser: user,
694 RepoInfo: f.RepoInfo(s, user),
695 RepoTagsResponse: result,
696 })
697 return
698}
699
700func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
701 f, err := fullyResolvedRepo(r)
702 if err != nil {
703 log.Println("failed to get repo and knot", err)
704 return
705 }
706
707 us, err := NewUnsignedClient(f.Knot, s.config.Dev)
708 if err != nil {
709 log.Println("failed to create unsigned client", err)
710 return
711 }
712
713 resp, err := us.Branches(f.OwnerDid(), f.RepoName)
714 if err != nil {
715 log.Println("failed to reach knotserver", err)
716 return
717 }
718
719 body, err := io.ReadAll(resp.Body)
720 if err != nil {
721 log.Printf("Error reading response body: %v", err)
722 return
723 }
724
725 var result types.RepoBranchesResponse
726 err = json.Unmarshal(body, &result)
727 if err != nil {
728 log.Println("failed to parse response:", err)
729 return
730 }
731
732 user := s.auth.GetUser(r)
733 s.pages.RepoBranches(w, pages.RepoBranchesParams{
734 LoggedInUser: user,
735 RepoInfo: f.RepoInfo(s, user),
736 RepoBranchesResponse: result,
737 })
738 return
739}
740
741func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
742 f, err := fullyResolvedRepo(r)
743 if err != nil {
744 log.Println("failed to get repo and knot", err)
745 return
746 }
747
748 ref := chi.URLParam(r, "ref")
749 filePath := chi.URLParam(r, "*")
750 protocol := "http"
751 if !s.config.Dev {
752 protocol = "https"
753 }
754 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
755 if err != nil {
756 log.Println("failed to reach knotserver", err)
757 return
758 }
759
760 body, err := io.ReadAll(resp.Body)
761 if err != nil {
762 log.Printf("Error reading response body: %v", err)
763 return
764 }
765
766 var result types.RepoBlobResponse
767 err = json.Unmarshal(body, &result)
768 if err != nil {
769 log.Println("failed to parse response:", err)
770 return
771 }
772
773 var breadcrumbs [][]string
774 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)})
775 if filePath != "" {
776 for idx, elem := range strings.Split(filePath, "/") {
777 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
778 }
779 }
780
781 user := s.auth.GetUser(r)
782 s.pages.RepoBlob(w, pages.RepoBlobParams{
783 LoggedInUser: user,
784 RepoInfo: f.RepoInfo(s, user),
785 RepoBlobResponse: result,
786 BreadCrumbs: breadcrumbs,
787 })
788 return
789}
790
791func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
792 f, err := fullyResolvedRepo(r)
793 if err != nil {
794 log.Println("failed to get repo and knot", err)
795 return
796 }
797
798 collaborator := r.FormValue("collaborator")
799 if collaborator == "" {
800 http.Error(w, "malformed form", http.StatusBadRequest)
801 return
802 }
803
804 collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
805 if err != nil {
806 w.Write([]byte("failed to resolve collaborator did to a handle"))
807 return
808 }
809 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
810
811 // TODO: create an atproto record for this
812
813 secret, err := db.GetRegistrationKey(s.db, f.Knot)
814 if err != nil {
815 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
816 return
817 }
818
819 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
820 if err != nil {
821 log.Println("failed to create client to ", f.Knot)
822 return
823 }
824
825 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
826 if err != nil {
827 log.Printf("failed to make request to %s: %s", f.Knot, err)
828 return
829 }
830
831 if ksResp.StatusCode != http.StatusNoContent {
832 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
833 return
834 }
835
836 tx, err := s.db.BeginTx(r.Context(), nil)
837 if err != nil {
838 log.Println("failed to start tx")
839 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
840 return
841 }
842 defer func() {
843 tx.Rollback()
844 err = s.enforcer.E.LoadPolicy()
845 if err != nil {
846 log.Println("failed to rollback policies")
847 }
848 }()
849
850 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo())
851 if err != nil {
852 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
853 return
854 }
855
856 err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
857 if err != nil {
858 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
859 return
860 }
861
862 err = tx.Commit()
863 if err != nil {
864 log.Println("failed to commit changes", err)
865 http.Error(w, err.Error(), http.StatusInternalServerError)
866 return
867 }
868
869 err = s.enforcer.E.SavePolicy()
870 if err != nil {
871 log.Println("failed to update ACLs", err)
872 http.Error(w, err.Error(), http.StatusInternalServerError)
873 return
874 }
875
876 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
877
878}
879
880func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
881 f, err := fullyResolvedRepo(r)
882 if err != nil {
883 log.Println("failed to get repo and knot", err)
884 return
885 }
886
887 switch r.Method {
888 case http.MethodGet:
889 // for now, this is just pubkeys
890 user := s.auth.GetUser(r)
891 repoCollaborators, err := f.Collaborators(r.Context(), s)
892 if err != nil {
893 log.Println("failed to get collaborators", err)
894 }
895
896 isCollaboratorInviteAllowed := false
897 if user != nil {
898 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo())
899 if err == nil && ok {
900 isCollaboratorInviteAllowed = true
901 }
902 }
903
904 s.pages.RepoSettings(w, pages.RepoSettingsParams{
905 LoggedInUser: user,
906 RepoInfo: f.RepoInfo(s, user),
907 Collaborators: repoCollaborators,
908 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
909 })
910 }
911}
912
913type FullyResolvedRepo struct {
914 Knot string
915 OwnerId identity.Identity
916 RepoName string
917 RepoAt syntax.ATURI
918 Description string
919 AddedAt string
920}
921
922func (f *FullyResolvedRepo) OwnerDid() string {
923 return f.OwnerId.DID.String()
924}
925
926func (f *FullyResolvedRepo) OwnerHandle() string {
927 return f.OwnerId.Handle.String()
928}
929
930func (f *FullyResolvedRepo) OwnerSlashRepo() string {
931 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
932 return p
933}
934
935func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) {
936 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
937 if err != nil {
938 return nil, err
939 }
940
941 var collaborators []pages.Collaborator
942 for _, item := range repoCollaborators {
943 // currently only two roles: owner and member
944 var role string
945 if item[3] == "repo:owner" {
946 role = "owner"
947 } else if item[3] == "repo:collaborator" {
948 role = "collaborator"
949 } else {
950 continue
951 }
952
953 did := item[0]
954
955 c := pages.Collaborator{
956 Did: did,
957 Handle: "",
958 Role: role,
959 }
960 collaborators = append(collaborators, c)
961 }
962
963 // populate all collborators with handles
964 identsToResolve := make([]string, len(collaborators))
965 for i, collab := range collaborators {
966 identsToResolve[i] = collab.Did
967 }
968
969 resolvedIdents := s.resolver.ResolveIdents(ctx, identsToResolve)
970 for i, resolved := range resolvedIdents {
971 if resolved != nil {
972 collaborators[i].Handle = resolved.Handle.String()
973 }
974 }
975
976 return collaborators, nil
977}
978
979func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo {
980 isStarred := false
981 if u != nil {
982 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
983 }
984
985 starCount, err := db.GetStarCount(s.db, f.RepoAt)
986 if err != nil {
987 log.Println("failed to get star count for ", f.RepoAt)
988 }
989 issueCount, err := db.GetIssueCount(s.db, f.RepoAt)
990 if err != nil {
991 log.Println("failed to get issue count for ", f.RepoAt)
992 }
993
994 knot := f.Knot
995 if knot == "knot1.tangled.sh" {
996 knot = "tangled.sh"
997 }
998
999 return pages.RepoInfo{
1000 OwnerDid: f.OwnerDid(),
1001 OwnerHandle: f.OwnerHandle(),
1002 Name: f.RepoName,
1003 RepoAt: f.RepoAt,
1004 Description: f.Description,
1005 IsStarred: isStarred,
1006 Knot: knot,
1007 Roles: rolesInRepo(s, u, f),
1008 Stats: db.RepoStats{
1009 StarCount: starCount,
1010 IssueCount: issueCount,
1011 },
1012 }
1013}
1014
1015func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
1016 user := s.auth.GetUser(r)
1017 f, err := fullyResolvedRepo(r)
1018 if err != nil {
1019 log.Println("failed to get repo and knot", err)
1020 return
1021 }
1022
1023 issueId := chi.URLParam(r, "issue")
1024 issueIdInt, err := strconv.Atoi(issueId)
1025 if err != nil {
1026 http.Error(w, "bad issue id", http.StatusBadRequest)
1027 log.Println("failed to parse issue id", err)
1028 return
1029 }
1030
1031 issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt)
1032 if err != nil {
1033 log.Println("failed to get issue and comments", err)
1034 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
1035 return
1036 }
1037
1038 issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid)
1039 if err != nil {
1040 log.Println("failed to resolve issue owner", err)
1041 }
1042
1043 identsToResolve := make([]string, len(comments))
1044 for i, comment := range comments {
1045 identsToResolve[i] = comment.OwnerDid
1046 }
1047 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
1048 didHandleMap := make(map[string]string)
1049 for _, identity := range resolvedIds {
1050 if !identity.Handle.IsInvalidHandle() {
1051 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1052 } else {
1053 didHandleMap[identity.DID.String()] = identity.DID.String()
1054 }
1055 }
1056
1057 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
1058 LoggedInUser: user,
1059 RepoInfo: f.RepoInfo(s, user),
1060 Issue: *issue,
1061 Comments: comments,
1062
1063 IssueOwnerHandle: issueOwnerIdent.Handle.String(),
1064 DidHandleMap: didHandleMap,
1065 })
1066
1067}
1068
1069func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
1070 user := s.auth.GetUser(r)
1071 f, err := fullyResolvedRepo(r)
1072 if err != nil {
1073 log.Println("failed to get repo and knot", err)
1074 return
1075 }
1076
1077 issueId := chi.URLParam(r, "issue")
1078 issueIdInt, err := strconv.Atoi(issueId)
1079 if err != nil {
1080 http.Error(w, "bad issue id", http.StatusBadRequest)
1081 log.Println("failed to parse issue id", err)
1082 return
1083 }
1084
1085 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
1086 if err != nil {
1087 log.Println("failed to get issue", err)
1088 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1089 return
1090 }
1091
1092 collaborators, err := f.Collaborators(r.Context(), s)
1093 if err != nil {
1094 log.Println("failed to fetch repo collaborators: %w", err)
1095 }
1096 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
1097 return user.Did == collab.Did
1098 })
1099 isIssueOwner := user.Did == issue.OwnerDid
1100
1101 // TODO: make this more granular
1102 if isIssueOwner || isCollaborator {
1103
1104 closed := tangled.RepoIssueStateClosed
1105
1106 client, _ := s.auth.AuthorizedClient(r)
1107 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1108 Collection: tangled.RepoIssueStateNSID,
1109 Repo: user.Did,
1110 Rkey: s.TID(),
1111 Record: &lexutil.LexiconTypeDecoder{
1112 Val: &tangled.RepoIssueState{
1113 Issue: issue.IssueAt,
1114 State: &closed,
1115 },
1116 },
1117 })
1118
1119 if err != nil {
1120 log.Println("failed to update issue state", err)
1121 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1122 return
1123 }
1124
1125 err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
1126 if err != nil {
1127 log.Println("failed to close issue", err)
1128 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1129 return
1130 }
1131
1132 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
1133 return
1134 } else {
1135 log.Println("user is not permitted to close issue")
1136 http.Error(w, "for biden", http.StatusUnauthorized)
1137 return
1138 }
1139}
1140
1141func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
1142 user := s.auth.GetUser(r)
1143 f, err := fullyResolvedRepo(r)
1144 if err != nil {
1145 log.Println("failed to get repo and knot", err)
1146 return
1147 }
1148
1149 issueId := chi.URLParam(r, "issue")
1150 issueIdInt, err := strconv.Atoi(issueId)
1151 if err != nil {
1152 http.Error(w, "bad issue id", http.StatusBadRequest)
1153 log.Println("failed to parse issue id", err)
1154 return
1155 }
1156
1157 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
1158 if err != nil {
1159 log.Println("failed to get issue", err)
1160 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1161 return
1162 }
1163
1164 collaborators, err := f.Collaborators(r.Context(), s)
1165 if err != nil {
1166 log.Println("failed to fetch repo collaborators: %w", err)
1167 }
1168 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
1169 return user.Did == collab.Did
1170 })
1171 isIssueOwner := user.Did == issue.OwnerDid
1172
1173 if isCollaborator || isIssueOwner {
1174 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
1175 if err != nil {
1176 log.Println("failed to reopen issue", err)
1177 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
1178 return
1179 }
1180 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
1181 return
1182 } else {
1183 log.Println("user is not the owner of the repo")
1184 http.Error(w, "forbidden", http.StatusUnauthorized)
1185 return
1186 }
1187}
1188
1189func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
1190 user := s.auth.GetUser(r)
1191 f, err := fullyResolvedRepo(r)
1192 if err != nil {
1193 log.Println("failed to get repo and knot", err)
1194 return
1195 }
1196
1197 issueId := chi.URLParam(r, "issue")
1198 issueIdInt, err := strconv.Atoi(issueId)
1199 if err != nil {
1200 http.Error(w, "bad issue id", http.StatusBadRequest)
1201 log.Println("failed to parse issue id", err)
1202 return
1203 }
1204
1205 switch r.Method {
1206 case http.MethodPost:
1207 body := r.FormValue("body")
1208 if body == "" {
1209 s.pages.Notice(w, "issue", "Body is required")
1210 return
1211 }
1212
1213 commentId := rand.IntN(1000000)
1214
1215 err := db.NewComment(s.db, &db.Comment{
1216 OwnerDid: user.Did,
1217 RepoAt: f.RepoAt,
1218 Issue: issueIdInt,
1219 CommentId: commentId,
1220 Body: body,
1221 })
1222 if err != nil {
1223 log.Println("failed to create comment", err)
1224 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1225 return
1226 }
1227
1228 createdAt := time.Now().Format(time.RFC3339)
1229 commentIdInt64 := int64(commentId)
1230 ownerDid := user.Did
1231 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
1232 if err != nil {
1233 log.Println("failed to get issue at", err)
1234 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1235 return
1236 }
1237
1238 atUri := f.RepoAt.String()
1239 client, _ := s.auth.AuthorizedClient(r)
1240 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1241 Collection: tangled.RepoIssueCommentNSID,
1242 Repo: user.Did,
1243 Rkey: s.TID(),
1244 Record: &lexutil.LexiconTypeDecoder{
1245 Val: &tangled.RepoIssueComment{
1246 Repo: &atUri,
1247 Issue: issueAt,
1248 CommentId: &commentIdInt64,
1249 Owner: &ownerDid,
1250 Body: &body,
1251 CreatedAt: &createdAt,
1252 },
1253 },
1254 })
1255 if err != nil {
1256 log.Println("failed to create comment", err)
1257 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1258 return
1259 }
1260
1261 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
1262 return
1263 }
1264}
1265
1266func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
1267 params := r.URL.Query()
1268 state := params.Get("state")
1269 isOpen := true
1270 switch state {
1271 case "open":
1272 isOpen = true
1273 case "closed":
1274 isOpen = false
1275 default:
1276 isOpen = true
1277 }
1278
1279 user := s.auth.GetUser(r)
1280 f, err := fullyResolvedRepo(r)
1281 if err != nil {
1282 log.Println("failed to get repo and knot", err)
1283 return
1284 }
1285
1286 issues, err := db.GetIssues(s.db, f.RepoAt, isOpen)
1287 if err != nil {
1288 log.Println("failed to get issues", err)
1289 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
1290 return
1291 }
1292
1293 identsToResolve := make([]string, len(issues))
1294 for i, issue := range issues {
1295 identsToResolve[i] = issue.OwnerDid
1296 }
1297 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
1298 didHandleMap := make(map[string]string)
1299 for _, identity := range resolvedIds {
1300 if !identity.Handle.IsInvalidHandle() {
1301 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1302 } else {
1303 didHandleMap[identity.DID.String()] = identity.DID.String()
1304 }
1305 }
1306
1307 s.pages.RepoIssues(w, pages.RepoIssuesParams{
1308 LoggedInUser: s.auth.GetUser(r),
1309 RepoInfo: f.RepoInfo(s, user),
1310 Issues: issues,
1311 DidHandleMap: didHandleMap,
1312 FilteringByOpen: isOpen,
1313 })
1314 return
1315}
1316
1317func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
1318 user := s.auth.GetUser(r)
1319
1320 f, err := fullyResolvedRepo(r)
1321 if err != nil {
1322 log.Println("failed to get repo and knot", err)
1323 return
1324 }
1325
1326 switch r.Method {
1327 case http.MethodGet:
1328 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
1329 LoggedInUser: user,
1330 RepoInfo: f.RepoInfo(s, user),
1331 })
1332 case http.MethodPost:
1333 title := r.FormValue("title")
1334 body := r.FormValue("body")
1335
1336 if title == "" || body == "" {
1337 s.pages.Notice(w, "issues", "Title and body are required")
1338 return
1339 }
1340
1341 tx, err := s.db.BeginTx(r.Context(), nil)
1342 if err != nil {
1343 s.pages.Notice(w, "issues", "Failed to create issue, try again later")
1344 return
1345 }
1346
1347 err = db.NewIssue(tx, &db.Issue{
1348 RepoAt: f.RepoAt,
1349 Title: title,
1350 Body: body,
1351 OwnerDid: user.Did,
1352 })
1353 if err != nil {
1354 log.Println("failed to create issue", err)
1355 s.pages.Notice(w, "issues", "Failed to create issue.")
1356 return
1357 }
1358
1359 issueId, err := db.GetIssueId(s.db, f.RepoAt)
1360 if err != nil {
1361 log.Println("failed to get issue id", err)
1362 s.pages.Notice(w, "issues", "Failed to create issue.")
1363 return
1364 }
1365
1366 client, _ := s.auth.AuthorizedClient(r)
1367 atUri := f.RepoAt.String()
1368 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1369 Collection: tangled.RepoIssueNSID,
1370 Repo: user.Did,
1371 Rkey: s.TID(),
1372 Record: &lexutil.LexiconTypeDecoder{
1373 Val: &tangled.RepoIssue{
1374 Repo: atUri,
1375 Title: title,
1376 Body: &body,
1377 Owner: user.Did,
1378 IssueId: int64(issueId),
1379 },
1380 },
1381 })
1382 if err != nil {
1383 log.Println("failed to create issue", err)
1384 s.pages.Notice(w, "issues", "Failed to create issue.")
1385 return
1386 }
1387
1388 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
1389 if err != nil {
1390 log.Println("failed to set issue at", err)
1391 s.pages.Notice(w, "issues", "Failed to create issue.")
1392 return
1393 }
1394
1395 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
1396 return
1397 }
1398}
1399
1400func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
1401 user := s.auth.GetUser(r)
1402 f, err := fullyResolvedRepo(r)
1403 if err != nil {
1404 log.Println("failed to get repo and knot", err)
1405 return
1406 }
1407
1408 pulls, err := db.GetPulls(s.db, f.RepoAt)
1409 if err != nil {
1410 log.Println("failed to get pulls", err)
1411 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
1412 return
1413 }
1414
1415 identsToResolve := make([]string, len(pulls))
1416 for i, pull := range pulls {
1417 identsToResolve[i] = pull.OwnerDid
1418 }
1419 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
1420 didHandleMap := make(map[string]string)
1421 for _, identity := range resolvedIds {
1422 if !identity.Handle.IsInvalidHandle() {
1423 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1424 } else {
1425 didHandleMap[identity.DID.String()] = identity.DID.String()
1426 }
1427 }
1428
1429 s.pages.RepoPulls(w, pages.RepoPullsParams{
1430 LoggedInUser: s.auth.GetUser(r),
1431 RepoInfo: f.RepoInfo(s, user),
1432 Pulls: pulls,
1433 DidHandleMap: didHandleMap,
1434 })
1435 return
1436}
1437
1438func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
1439 repoName := chi.URLParam(r, "repo")
1440 knot, ok := r.Context().Value("knot").(string)
1441 if !ok {
1442 log.Println("malformed middleware")
1443 return nil, fmt.Errorf("malformed middleware")
1444 }
1445 id, ok := r.Context().Value("resolvedId").(identity.Identity)
1446 if !ok {
1447 log.Println("malformed middleware")
1448 return nil, fmt.Errorf("malformed middleware")
1449 }
1450
1451 repoAt, ok := r.Context().Value("repoAt").(string)
1452 if !ok {
1453 log.Println("malformed middleware")
1454 return nil, fmt.Errorf("malformed middleware")
1455 }
1456
1457 parsedRepoAt, err := syntax.ParseATURI(repoAt)
1458 if err != nil {
1459 log.Println("malformed repo at-uri")
1460 return nil, fmt.Errorf("malformed middleware")
1461 }
1462
1463 // pass through values from the middleware
1464 description, ok := r.Context().Value("repoDescription").(string)
1465 addedAt, ok := r.Context().Value("repoAddedAt").(string)
1466
1467 return &FullyResolvedRepo{
1468 Knot: knot,
1469 OwnerId: id,
1470 RepoName: repoName,
1471 RepoAt: parsedRepoAt,
1472 Description: description,
1473 AddedAt: addedAt,
1474 }, nil
1475}
1476
1477func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
1478 if u != nil {
1479 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo())
1480 return pages.RolesInRepo{r}
1481 } else {
1482 return pages.RolesInRepo{}
1483 }
1484}