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 == "" || targetBranch == "" {
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 pullCount, err := db.GetPullCount(s.db, f.RepoAt)
994 if err != nil {
995 log.Println("failed to get issue count for ", f.RepoAt)
996 }
997
998 knot := f.Knot
999 if knot == "knot1.tangled.sh" {
1000 knot = "tangled.sh"
1001 }
1002
1003 return pages.RepoInfo{
1004 OwnerDid: f.OwnerDid(),
1005 OwnerHandle: f.OwnerHandle(),
1006 Name: f.RepoName,
1007 RepoAt: f.RepoAt,
1008 Description: f.Description,
1009 IsStarred: isStarred,
1010 Knot: knot,
1011 Roles: rolesInRepo(s, u, f),
1012 Stats: db.RepoStats{
1013 StarCount: starCount,
1014 IssueCount: issueCount,
1015 PullCount: pullCount,
1016 },
1017 }
1018}
1019
1020func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
1021 user := s.auth.GetUser(r)
1022 f, err := fullyResolvedRepo(r)
1023 if err != nil {
1024 log.Println("failed to get repo and knot", err)
1025 return
1026 }
1027
1028 issueId := chi.URLParam(r, "issue")
1029 issueIdInt, err := strconv.Atoi(issueId)
1030 if err != nil {
1031 http.Error(w, "bad issue id", http.StatusBadRequest)
1032 log.Println("failed to parse issue id", err)
1033 return
1034 }
1035
1036 issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt)
1037 if err != nil {
1038 log.Println("failed to get issue and comments", err)
1039 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
1040 return
1041 }
1042
1043 issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid)
1044 if err != nil {
1045 log.Println("failed to resolve issue owner", err)
1046 }
1047
1048 identsToResolve := make([]string, len(comments))
1049 for i, comment := range comments {
1050 identsToResolve[i] = comment.OwnerDid
1051 }
1052 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
1053 didHandleMap := make(map[string]string)
1054 for _, identity := range resolvedIds {
1055 if !identity.Handle.IsInvalidHandle() {
1056 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1057 } else {
1058 didHandleMap[identity.DID.String()] = identity.DID.String()
1059 }
1060 }
1061
1062 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
1063 LoggedInUser: user,
1064 RepoInfo: f.RepoInfo(s, user),
1065 Issue: *issue,
1066 Comments: comments,
1067
1068 IssueOwnerHandle: issueOwnerIdent.Handle.String(),
1069 DidHandleMap: didHandleMap,
1070 })
1071
1072}
1073
1074func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
1075 user := s.auth.GetUser(r)
1076 f, err := fullyResolvedRepo(r)
1077 if err != nil {
1078 log.Println("failed to get repo and knot", err)
1079 return
1080 }
1081
1082 issueId := chi.URLParam(r, "issue")
1083 issueIdInt, err := strconv.Atoi(issueId)
1084 if err != nil {
1085 http.Error(w, "bad issue id", http.StatusBadRequest)
1086 log.Println("failed to parse issue id", err)
1087 return
1088 }
1089
1090 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
1091 if err != nil {
1092 log.Println("failed to get issue", err)
1093 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1094 return
1095 }
1096
1097 collaborators, err := f.Collaborators(r.Context(), s)
1098 if err != nil {
1099 log.Println("failed to fetch repo collaborators: %w", err)
1100 }
1101 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
1102 return user.Did == collab.Did
1103 })
1104 isIssueOwner := user.Did == issue.OwnerDid
1105
1106 // TODO: make this more granular
1107 if isIssueOwner || isCollaborator {
1108
1109 closed := tangled.RepoIssueStateClosed
1110
1111 client, _ := s.auth.AuthorizedClient(r)
1112 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1113 Collection: tangled.RepoIssueStateNSID,
1114 Repo: issue.OwnerDid,
1115 Rkey: s.TID(),
1116 Record: &lexutil.LexiconTypeDecoder{
1117 Val: &tangled.RepoIssueState{
1118 Issue: issue.IssueAt,
1119 State: &closed,
1120 },
1121 },
1122 })
1123
1124 if err != nil {
1125 log.Println("failed to update issue state", err)
1126 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1127 return
1128 }
1129
1130 err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
1131 if err != nil {
1132 log.Println("failed to close issue", err)
1133 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
1134 return
1135 }
1136
1137 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
1138 return
1139 } else {
1140 log.Println("user is not permitted to close issue")
1141 http.Error(w, "for biden", http.StatusUnauthorized)
1142 return
1143 }
1144}
1145
1146func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
1147 user := s.auth.GetUser(r)
1148 f, err := fullyResolvedRepo(r)
1149 if err != nil {
1150 log.Println("failed to get repo and knot", err)
1151 return
1152 }
1153
1154 issueId := chi.URLParam(r, "issue")
1155 issueIdInt, err := strconv.Atoi(issueId)
1156 if err != nil {
1157 http.Error(w, "bad issue id", http.StatusBadRequest)
1158 log.Println("failed to parse issue id", err)
1159 return
1160 }
1161
1162 if user.Did == f.OwnerDid() {
1163 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
1164 if err != nil {
1165 log.Println("failed to reopen issue", err)
1166 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
1167 return
1168 }
1169 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
1170 return
1171 } else {
1172 log.Println("user is not the owner of the repo")
1173 http.Error(w, "forbidden", http.StatusUnauthorized)
1174 return
1175 }
1176}
1177
1178func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
1179 user := s.auth.GetUser(r)
1180 f, err := fullyResolvedRepo(r)
1181 if err != nil {
1182 log.Println("failed to get repo and knot", err)
1183 return
1184 }
1185
1186 issueId := chi.URLParam(r, "issue")
1187 issueIdInt, err := strconv.Atoi(issueId)
1188 if err != nil {
1189 http.Error(w, "bad issue id", http.StatusBadRequest)
1190 log.Println("failed to parse issue id", err)
1191 return
1192 }
1193
1194 switch r.Method {
1195 case http.MethodPost:
1196 body := r.FormValue("body")
1197 if body == "" {
1198 s.pages.Notice(w, "issue", "Body is required")
1199 return
1200 }
1201
1202 commentId := rand.IntN(1000000)
1203
1204 err := db.NewComment(s.db, &db.Comment{
1205 OwnerDid: user.Did,
1206 RepoAt: f.RepoAt,
1207 Issue: issueIdInt,
1208 CommentId: commentId,
1209 Body: body,
1210 })
1211 if err != nil {
1212 log.Println("failed to create comment", err)
1213 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1214 return
1215 }
1216
1217 createdAt := time.Now().Format(time.RFC3339)
1218 commentIdInt64 := int64(commentId)
1219 ownerDid := user.Did
1220 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
1221 if err != nil {
1222 log.Println("failed to get issue at", err)
1223 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1224 return
1225 }
1226
1227 atUri := f.RepoAt.String()
1228 client, _ := s.auth.AuthorizedClient(r)
1229 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1230 Collection: tangled.RepoIssueCommentNSID,
1231 Repo: user.Did,
1232 Rkey: s.TID(),
1233 Record: &lexutil.LexiconTypeDecoder{
1234 Val: &tangled.RepoIssueComment{
1235 Repo: &atUri,
1236 Issue: issueAt,
1237 CommentId: &commentIdInt64,
1238 Owner: &ownerDid,
1239 Body: &body,
1240 CreatedAt: &createdAt,
1241 },
1242 },
1243 })
1244 if err != nil {
1245 log.Println("failed to create comment", err)
1246 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1247 return
1248 }
1249
1250 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
1251 return
1252 }
1253}
1254
1255func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
1256 params := r.URL.Query()
1257 state := params.Get("state")
1258 isOpen := true
1259 switch state {
1260 case "open":
1261 isOpen = true
1262 case "closed":
1263 isOpen = false
1264 default:
1265 isOpen = true
1266 }
1267
1268 user := s.auth.GetUser(r)
1269 f, err := fullyResolvedRepo(r)
1270 if err != nil {
1271 log.Println("failed to get repo and knot", err)
1272 return
1273 }
1274
1275 issues, err := db.GetIssues(s.db, f.RepoAt, isOpen)
1276 if err != nil {
1277 log.Println("failed to get issues", err)
1278 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
1279 return
1280 }
1281
1282 identsToResolve := make([]string, len(issues))
1283 for i, issue := range issues {
1284 identsToResolve[i] = issue.OwnerDid
1285 }
1286 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
1287 didHandleMap := make(map[string]string)
1288 for _, identity := range resolvedIds {
1289 if !identity.Handle.IsInvalidHandle() {
1290 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1291 } else {
1292 didHandleMap[identity.DID.String()] = identity.DID.String()
1293 }
1294 }
1295
1296 s.pages.RepoIssues(w, pages.RepoIssuesParams{
1297 LoggedInUser: s.auth.GetUser(r),
1298 RepoInfo: f.RepoInfo(s, user),
1299 Issues: issues,
1300 DidHandleMap: didHandleMap,
1301 FilteringByOpen: isOpen,
1302 })
1303 return
1304}
1305
1306func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
1307 user := s.auth.GetUser(r)
1308
1309 f, err := fullyResolvedRepo(r)
1310 if err != nil {
1311 log.Println("failed to get repo and knot", err)
1312 return
1313 }
1314
1315 switch r.Method {
1316 case http.MethodGet:
1317 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
1318 LoggedInUser: user,
1319 RepoInfo: f.RepoInfo(s, user),
1320 })
1321 case http.MethodPost:
1322 title := r.FormValue("title")
1323 body := r.FormValue("body")
1324
1325 if title == "" || body == "" {
1326 s.pages.Notice(w, "issues", "Title and body are required")
1327 return
1328 }
1329
1330 tx, err := s.db.BeginTx(r.Context(), nil)
1331 if err != nil {
1332 s.pages.Notice(w, "issues", "Failed to create issue, try again later")
1333 return
1334 }
1335
1336 err = db.NewIssue(tx, &db.Issue{
1337 RepoAt: f.RepoAt,
1338 Title: title,
1339 Body: body,
1340 OwnerDid: user.Did,
1341 })
1342 if err != nil {
1343 log.Println("failed to create issue", err)
1344 s.pages.Notice(w, "issues", "Failed to create issue.")
1345 return
1346 }
1347
1348 issueId, err := db.GetIssueId(s.db, f.RepoAt)
1349 if err != nil {
1350 log.Println("failed to get issue id", err)
1351 s.pages.Notice(w, "issues", "Failed to create issue.")
1352 return
1353 }
1354
1355 client, _ := s.auth.AuthorizedClient(r)
1356 atUri := f.RepoAt.String()
1357 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1358 Collection: tangled.RepoIssueNSID,
1359 Repo: user.Did,
1360 Rkey: s.TID(),
1361 Record: &lexutil.LexiconTypeDecoder{
1362 Val: &tangled.RepoIssue{
1363 Repo: atUri,
1364 Title: title,
1365 Body: &body,
1366 Owner: user.Did,
1367 IssueId: int64(issueId),
1368 },
1369 },
1370 })
1371 if err != nil {
1372 log.Println("failed to create issue", err)
1373 s.pages.Notice(w, "issues", "Failed to create issue.")
1374 return
1375 }
1376
1377 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
1378 if err != nil {
1379 log.Println("failed to set issue at", err)
1380 s.pages.Notice(w, "issues", "Failed to create issue.")
1381 return
1382 }
1383
1384 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
1385 return
1386 }
1387}
1388
1389func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
1390 user := s.auth.GetUser(r)
1391 params := r.URL.Query()
1392
1393 state := db.PullOpen
1394 switch params.Get("state") {
1395 case "closed":
1396 state = db.PullClosed
1397 case "merged":
1398 state = db.PullMerged
1399 }
1400
1401 f, err := fullyResolvedRepo(r)
1402 if err != nil {
1403 log.Println("failed to get repo and knot", err)
1404 return
1405 }
1406
1407 pulls, err := db.GetPulls(s.db, f.RepoAt, state)
1408 if err != nil {
1409 log.Println("failed to get pulls", err)
1410 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
1411 return
1412 }
1413
1414 identsToResolve := make([]string, len(pulls))
1415 for i, pull := range pulls {
1416 identsToResolve[i] = pull.OwnerDid
1417 }
1418 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
1419 didHandleMap := make(map[string]string)
1420 for _, identity := range resolvedIds {
1421 if !identity.Handle.IsInvalidHandle() {
1422 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1423 } else {
1424 didHandleMap[identity.DID.String()] = identity.DID.String()
1425 }
1426 }
1427
1428 s.pages.RepoPulls(w, pages.RepoPullsParams{
1429 LoggedInUser: s.auth.GetUser(r),
1430 RepoInfo: f.RepoInfo(s, user),
1431 Pulls: pulls,
1432 DidHandleMap: didHandleMap,
1433 FilteringBy: state,
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}