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: issue.OwnerDid, 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 if user.Did == f.OwnerDid() { 1158 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) 1159 if err != nil { 1160 log.Println("failed to reopen issue", err) 1161 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 1162 return 1163 } 1164 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1165 return 1166 } else { 1167 log.Println("user is not the owner of the repo") 1168 http.Error(w, "forbidden", http.StatusUnauthorized) 1169 return 1170 } 1171} 1172 1173func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1174 user := s.auth.GetUser(r) 1175 f, err := fullyResolvedRepo(r) 1176 if err != nil { 1177 log.Println("failed to get repo and knot", err) 1178 return 1179 } 1180 1181 issueId := chi.URLParam(r, "issue") 1182 issueIdInt, err := strconv.Atoi(issueId) 1183 if err != nil { 1184 http.Error(w, "bad issue id", http.StatusBadRequest) 1185 log.Println("failed to parse issue id", err) 1186 return 1187 } 1188 1189 switch r.Method { 1190 case http.MethodPost: 1191 body := r.FormValue("body") 1192 if body == "" { 1193 s.pages.Notice(w, "issue", "Body is required") 1194 return 1195 } 1196 1197 commentId := rand.IntN(1000000) 1198 1199 err := db.NewComment(s.db, &db.Comment{ 1200 OwnerDid: user.Did, 1201 RepoAt: f.RepoAt, 1202 Issue: issueIdInt, 1203 CommentId: commentId, 1204 Body: body, 1205 }) 1206 if err != nil { 1207 log.Println("failed to create comment", err) 1208 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1209 return 1210 } 1211 1212 createdAt := time.Now().Format(time.RFC3339) 1213 commentIdInt64 := int64(commentId) 1214 ownerDid := user.Did 1215 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) 1216 if err != nil { 1217 log.Println("failed to get issue at", err) 1218 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1219 return 1220 } 1221 1222 atUri := f.RepoAt.String() 1223 client, _ := s.auth.AuthorizedClient(r) 1224 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1225 Collection: tangled.RepoIssueCommentNSID, 1226 Repo: user.Did, 1227 Rkey: s.TID(), 1228 Record: &lexutil.LexiconTypeDecoder{ 1229 Val: &tangled.RepoIssueComment{ 1230 Repo: &atUri, 1231 Issue: issueAt, 1232 CommentId: &commentIdInt64, 1233 Owner: &ownerDid, 1234 Body: &body, 1235 CreatedAt: &createdAt, 1236 }, 1237 }, 1238 }) 1239 if err != nil { 1240 log.Println("failed to create comment", err) 1241 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1242 return 1243 } 1244 1245 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 1246 return 1247 } 1248} 1249 1250func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 1251 params := r.URL.Query() 1252 state := params.Get("state") 1253 isOpen := true 1254 switch state { 1255 case "open": 1256 isOpen = true 1257 case "closed": 1258 isOpen = false 1259 default: 1260 isOpen = true 1261 } 1262 1263 user := s.auth.GetUser(r) 1264 f, err := fullyResolvedRepo(r) 1265 if err != nil { 1266 log.Println("failed to get repo and knot", err) 1267 return 1268 } 1269 1270 issues, err := db.GetIssues(s.db, f.RepoAt, isOpen) 1271 if err != nil { 1272 log.Println("failed to get issues", err) 1273 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 1274 return 1275 } 1276 1277 identsToResolve := make([]string, len(issues)) 1278 for i, issue := range issues { 1279 identsToResolve[i] = issue.OwnerDid 1280 } 1281 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1282 didHandleMap := make(map[string]string) 1283 for _, identity := range resolvedIds { 1284 if !identity.Handle.IsInvalidHandle() { 1285 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1286 } else { 1287 didHandleMap[identity.DID.String()] = identity.DID.String() 1288 } 1289 } 1290 1291 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1292 LoggedInUser: s.auth.GetUser(r), 1293 RepoInfo: f.RepoInfo(s, user), 1294 Issues: issues, 1295 DidHandleMap: didHandleMap, 1296 FilteringByOpen: isOpen, 1297 }) 1298 return 1299} 1300 1301func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1302 user := s.auth.GetUser(r) 1303 1304 f, err := fullyResolvedRepo(r) 1305 if err != nil { 1306 log.Println("failed to get repo and knot", err) 1307 return 1308 } 1309 1310 switch r.Method { 1311 case http.MethodGet: 1312 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 1313 LoggedInUser: user, 1314 RepoInfo: f.RepoInfo(s, user), 1315 }) 1316 case http.MethodPost: 1317 title := r.FormValue("title") 1318 body := r.FormValue("body") 1319 1320 if title == "" || body == "" { 1321 s.pages.Notice(w, "issues", "Title and body are required") 1322 return 1323 } 1324 1325 tx, err := s.db.BeginTx(r.Context(), nil) 1326 if err != nil { 1327 s.pages.Notice(w, "issues", "Failed to create issue, try again later") 1328 return 1329 } 1330 1331 err = db.NewIssue(tx, &db.Issue{ 1332 RepoAt: f.RepoAt, 1333 Title: title, 1334 Body: body, 1335 OwnerDid: user.Did, 1336 }) 1337 if err != nil { 1338 log.Println("failed to create issue", err) 1339 s.pages.Notice(w, "issues", "Failed to create issue.") 1340 return 1341 } 1342 1343 issueId, err := db.GetIssueId(s.db, f.RepoAt) 1344 if err != nil { 1345 log.Println("failed to get issue id", err) 1346 s.pages.Notice(w, "issues", "Failed to create issue.") 1347 return 1348 } 1349 1350 client, _ := s.auth.AuthorizedClient(r) 1351 atUri := f.RepoAt.String() 1352 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1353 Collection: tangled.RepoIssueNSID, 1354 Repo: user.Did, 1355 Rkey: s.TID(), 1356 Record: &lexutil.LexiconTypeDecoder{ 1357 Val: &tangled.RepoIssue{ 1358 Repo: atUri, 1359 Title: title, 1360 Body: &body, 1361 Owner: user.Did, 1362 IssueId: int64(issueId), 1363 }, 1364 }, 1365 }) 1366 if err != nil { 1367 log.Println("failed to create issue", err) 1368 s.pages.Notice(w, "issues", "Failed to create issue.") 1369 return 1370 } 1371 1372 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) 1373 if err != nil { 1374 log.Println("failed to set issue at", err) 1375 s.pages.Notice(w, "issues", "Failed to create issue.") 1376 return 1377 } 1378 1379 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1380 return 1381 } 1382} 1383 1384func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 1385 user := s.auth.GetUser(r) 1386 f, err := fullyResolvedRepo(r) 1387 if err != nil { 1388 log.Println("failed to get repo and knot", err) 1389 return 1390 } 1391 1392 pulls, err := db.GetPulls(s.db, f.RepoAt) 1393 if err != nil { 1394 log.Println("failed to get pulls", err) 1395 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 1396 return 1397 } 1398 1399 identsToResolve := make([]string, len(pulls)) 1400 for i, pull := range pulls { 1401 identsToResolve[i] = pull.OwnerDid 1402 } 1403 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1404 didHandleMap := make(map[string]string) 1405 for _, identity := range resolvedIds { 1406 if !identity.Handle.IsInvalidHandle() { 1407 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1408 } else { 1409 didHandleMap[identity.DID.String()] = identity.DID.String() 1410 } 1411 } 1412 1413 s.pages.RepoPulls(w, pages.RepoPullsParams{ 1414 LoggedInUser: s.auth.GetUser(r), 1415 RepoInfo: f.RepoInfo(s, user), 1416 Pulls: pulls, 1417 DidHandleMap: didHandleMap, 1418 }) 1419 return 1420} 1421 1422func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 1423 repoName := chi.URLParam(r, "repo") 1424 knot, ok := r.Context().Value("knot").(string) 1425 if !ok { 1426 log.Println("malformed middleware") 1427 return nil, fmt.Errorf("malformed middleware") 1428 } 1429 id, ok := r.Context().Value("resolvedId").(identity.Identity) 1430 if !ok { 1431 log.Println("malformed middleware") 1432 return nil, fmt.Errorf("malformed middleware") 1433 } 1434 1435 repoAt, ok := r.Context().Value("repoAt").(string) 1436 if !ok { 1437 log.Println("malformed middleware") 1438 return nil, fmt.Errorf("malformed middleware") 1439 } 1440 1441 parsedRepoAt, err := syntax.ParseATURI(repoAt) 1442 if err != nil { 1443 log.Println("malformed repo at-uri") 1444 return nil, fmt.Errorf("malformed middleware") 1445 } 1446 1447 // pass through values from the middleware 1448 description, ok := r.Context().Value("repoDescription").(string) 1449 addedAt, ok := r.Context().Value("repoAddedAt").(string) 1450 1451 return &FullyResolvedRepo{ 1452 Knot: knot, 1453 OwnerId: id, 1454 RepoName: repoName, 1455 RepoAt: parsedRepoAt, 1456 Description: description, 1457 AddedAt: addedAt, 1458 }, nil 1459} 1460 1461func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 1462 if u != nil { 1463 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) 1464 return pages.RolesInRepo{r} 1465 } else { 1466 return pages.RolesInRepo{} 1467 } 1468}