Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

1package repo 2 3import ( 4 "context" 5 "database/sql" 6 "errors" 7 "fmt" 8 "log/slog" 9 "net/http" 10 "net/url" 11 "slices" 12 "strings" 13 "time" 14 15 "tangled.org/core/appview/cloudflare" 16 17 "tangled.org/core/api/tangled" 18 "tangled.org/core/appview/config" 19 "tangled.org/core/appview/db" 20 "tangled.org/core/appview/models" 21 "tangled.org/core/appview/notify" 22 "tangled.org/core/appview/oauth" 23 "tangled.org/core/appview/pages" 24 "tangled.org/core/appview/pagination" 25 "tangled.org/core/appview/reporesolver" 26 "tangled.org/core/appview/sites" 27 "tangled.org/core/appview/validator" 28 xrpcclient "tangled.org/core/appview/xrpcclient" 29 "tangled.org/core/eventconsumer" 30 "tangled.org/core/idresolver" 31 "tangled.org/core/ogre" 32 "tangled.org/core/orm" 33 "tangled.org/core/rbac" 34 "tangled.org/core/tid" 35 "tangled.org/core/xrpc/serviceauth" 36 37 comatproto "github.com/bluesky-social/indigo/api/atproto" 38 "github.com/bluesky-social/indigo/atproto/atclient" 39 "github.com/bluesky-social/indigo/atproto/syntax" 40 lexutil "github.com/bluesky-social/indigo/lex/util" 41 42 "github.com/go-chi/chi/v5" 43) 44 45type Repo struct { 46 repoResolver *reporesolver.RepoResolver 47 idResolver *idresolver.Resolver 48 config *config.Config 49 oauth *oauth.OAuth 50 pages *pages.Pages 51 spindlestream *eventconsumer.Consumer 52 db *db.DB 53 enforcer *rbac.Enforcer 54 notifier notify.Notifier 55 logger *slog.Logger 56 serviceAuth *serviceauth.ServiceAuth 57 validator *validator.Validator 58 cfClient *cloudflare.Client 59 ogreClient *ogre.Client 60} 61 62func New( 63 oauth *oauth.OAuth, 64 repoResolver *reporesolver.RepoResolver, 65 pages *pages.Pages, 66 spindlestream *eventconsumer.Consumer, 67 idResolver *idresolver.Resolver, 68 db *db.DB, 69 config *config.Config, 70 notifier notify.Notifier, 71 enforcer *rbac.Enforcer, 72 logger *slog.Logger, 73 validator *validator.Validator, 74 cfClient *cloudflare.Client, 75) *Repo { 76 return &Repo{ 77 oauth: oauth, 78 repoResolver: repoResolver, 79 pages: pages, 80 idResolver: idResolver, 81 config: config, 82 spindlestream: spindlestream, 83 db: db, 84 notifier: notifier, 85 enforcer: enforcer, 86 logger: logger, 87 validator: validator, 88 cfClient: cfClient, 89 ogreClient: ogre.NewClient(config.Ogre.Host), 90 } 91} 92 93// modify the spindle configured for this repo 94func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { 95 user := rp.oauth.GetMultiAccountUser(r) 96 l := rp.logger.With("handler", "EditSpindle") 97 l = l.With("did", user.Did) 98 99 errorId := "operation-error" 100 fail := func(msg string, err error) { 101 l.Error(msg, "err", err) 102 rp.pages.Notice(w, errorId, msg) 103 } 104 105 f, err := rp.repoResolver.Resolve(r) 106 if err != nil { 107 fail("Failed to resolve repo. Try again later", err) 108 return 109 } 110 111 newSpindle := r.FormValue("spindle") 112 removingSpindle := newSpindle == "[[none]]" // see pages/templates/repo/settings/pipelines.html for more info on why we use this value 113 client, err := rp.oauth.AuthorizedClient(r) 114 if err != nil { 115 fail("Failed to authorize. Try again later.", err) 116 return 117 } 118 119 if !removingSpindle { 120 // ensure that this is a valid spindle for this user 121 validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 122 if err != nil { 123 fail("Failed to find spindles. Try again later.", err) 124 return 125 } 126 127 if !slices.Contains(validSpindles, newSpindle) { 128 fail("Failed to configure spindle.", fmt.Errorf("%s is not a valid spindle: %q", newSpindle, validSpindles)) 129 return 130 } 131 } 132 133 newRepo := *f 134 newRepo.Spindle = newSpindle 135 record := newRepo.AsRecord() 136 137 spindlePtr := &newSpindle 138 if removingSpindle { 139 spindlePtr = nil 140 newRepo.Spindle = "" 141 } 142 143 // optimistic update 144 err = db.UpdateSpindle(rp.db, newRepo.RepoDid, spindlePtr) 145 if err != nil { 146 fail("Failed to update spindle. Try again later.", err) 147 return 148 } 149 150 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 151 if err != nil { 152 fail("Failed to update spindle, no record found on PDS.", err) 153 return 154 } 155 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 156 Collection: tangled.RepoNSID, 157 Repo: newRepo.Did, 158 Rkey: newRepo.Rkey, 159 SwapRecord: ex.Cid, 160 Record: &lexutil.LexiconTypeDecoder{ 161 Val: &record, 162 }, 163 }) 164 165 if err != nil { 166 fail("Failed to update spindle, unable to save to PDS.", err) 167 return 168 } 169 170 if !removingSpindle { 171 // add this spindle to spindle stream 172 rp.spindlestream.AddSource( 173 context.Background(), 174 eventconsumer.NewSpindleSource(newSpindle), 175 ) 176 } 177 178 rp.pages.HxRefresh(w) 179} 180 181func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) { 182 user := rp.oauth.GetMultiAccountUser(r) 183 l := rp.logger.With("handler", "AddLabel") 184 l = l.With("did", user.Did) 185 186 f, err := rp.repoResolver.Resolve(r) 187 if err != nil { 188 l.Error("failed to get repo and knot", "err", err) 189 return 190 } 191 192 errorId := "add-label-error" 193 fail := func(msg string, err error) { 194 l.Error(msg, "err", err) 195 rp.pages.Notice(w, errorId, msg) 196 } 197 198 // get form values for label definition 199 name := r.FormValue("name") 200 concreteType := r.FormValue("valueType") 201 valueFormat := r.FormValue("valueFormat") 202 enumValues := r.FormValue("enumValues") 203 scope := r.Form["scope"] 204 color := r.FormValue("color") 205 multiple := r.FormValue("multiple") == "true" 206 207 var variants []string 208 for part := range strings.SplitSeq(enumValues, ",") { 209 if part = strings.TrimSpace(part); part != "" { 210 variants = append(variants, part) 211 } 212 } 213 214 if concreteType == "" { 215 concreteType = "null" 216 } 217 218 format := models.ValueTypeFormatAny 219 if valueFormat == "did" { 220 format = models.ValueTypeFormatDid 221 } 222 223 valueType := models.ValueType{ 224 Type: models.ConcreteType(concreteType), 225 Format: format, 226 Enum: variants, 227 } 228 229 label := models.LabelDefinition{ 230 Did: user.Did, 231 Rkey: tid.TID(), 232 Name: name, 233 ValueType: valueType, 234 Scope: scope, 235 Color: &color, 236 Multiple: multiple, 237 Created: time.Now(), 238 } 239 if err := rp.validator.ValidateLabelDefinition(&label); err != nil { 240 fail(err.Error(), err) 241 return 242 } 243 244 // announce this relation into the firehose, store into owners' pds 245 client, err := rp.oauth.AuthorizedClient(r) 246 if err != nil { 247 fail(err.Error(), err) 248 return 249 } 250 251 // emit a labelRecord 252 labelRecord := label.AsRecord() 253 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 254 Collection: tangled.LabelDefinitionNSID, 255 Repo: label.Did, 256 Rkey: label.Rkey, 257 Record: &lexutil.LexiconTypeDecoder{ 258 Val: &labelRecord, 259 }, 260 }) 261 // invalid record 262 if err != nil { 263 fail("Failed to write record to PDS.", err) 264 return 265 } 266 267 aturi := resp.Uri 268 l = l.With("at-uri", aturi) 269 l.Info("wrote label record to PDS") 270 271 // update the repo to subscribe to this label 272 newRepo := *f 273 newRepo.Labels = append(newRepo.Labels, aturi) 274 repoRecord := newRepo.AsRecord() 275 276 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 277 if err != nil { 278 fail("Failed to update labels, no record found on PDS.", err) 279 return 280 } 281 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 282 Collection: tangled.RepoNSID, 283 Repo: newRepo.Did, 284 Rkey: newRepo.Rkey, 285 SwapRecord: ex.Cid, 286 Record: &lexutil.LexiconTypeDecoder{ 287 Val: &repoRecord, 288 }, 289 }) 290 if err != nil { 291 fail("Failed to update labels for repo.", err) 292 return 293 } 294 295 tx, err := rp.db.BeginTx(r.Context(), nil) 296 if err != nil { 297 fail("Failed to add label.", err) 298 return 299 } 300 301 rollback := func() { 302 err1 := tx.Rollback() 303 err2 := rollbackRecord(context.Background(), aturi, client) 304 305 // ignore txn complete errors, this is okay 306 if errors.Is(err1, sql.ErrTxDone) { 307 err1 = nil 308 } 309 310 if errs := errors.Join(err1, err2); errs != nil { 311 l.Error("failed to rollback changes", "errs", errs) 312 return 313 } 314 } 315 defer rollback() 316 317 _, err = db.AddLabelDefinition(tx, &label) 318 if err != nil { 319 fail("Failed to add label.", err) 320 return 321 } 322 323 if err = db.SubscribeLabel(tx, &models.RepoLabel{ 324 RepoDid: syntax.DID(f.RepoDid), 325 LabelAt: label.AtUri(), 326 }); err != nil { 327 fail("Failed to subscribe to label.", err) 328 return 329 } 330 331 err = tx.Commit() 332 if err != nil { 333 fail("Failed to add label.", err) 334 return 335 } 336 337 // clear aturi when everything is successful 338 aturi = "" 339 340 rp.pages.HxRefresh(w) 341} 342 343func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) { 344 user := rp.oauth.GetMultiAccountUser(r) 345 l := rp.logger.With("handler", "DeleteLabel") 346 l = l.With("did", user.Did) 347 348 f, err := rp.repoResolver.Resolve(r) 349 if err != nil { 350 l.Error("failed to get repo and knot", "err", err) 351 return 352 } 353 354 errorId := "label-operation" 355 fail := func(msg string, err error) { 356 l.Error(msg, "err", err) 357 rp.pages.Notice(w, errorId, msg) 358 } 359 360 // get form values 361 labelId := r.FormValue("label-id") 362 363 label, err := db.GetLabelDefinition(rp.db, orm.FilterEq("id", labelId)) 364 if err != nil { 365 fail("Failed to find label definition.", err) 366 return 367 } 368 369 client, err := rp.oauth.AuthorizedClient(r) 370 if err != nil { 371 fail(err.Error(), err) 372 return 373 } 374 375 // delete label record from PDS 376 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 377 Collection: tangled.LabelDefinitionNSID, 378 Repo: label.Did, 379 Rkey: label.Rkey, 380 }) 381 if err != nil { 382 fail("Failed to delete label record from PDS.", err) 383 return 384 } 385 386 // update repo record to remove the label reference 387 newRepo := *f 388 var updated []string 389 removedAt := label.AtUri().String() 390 for _, l := range newRepo.Labels { 391 if l != removedAt { 392 updated = append(updated, l) 393 } 394 } 395 newRepo.Labels = updated 396 repoRecord := newRepo.AsRecord() 397 398 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 399 if err != nil { 400 fail("Failed to update labels, no record found on PDS.", err) 401 return 402 } 403 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 404 Collection: tangled.RepoNSID, 405 Repo: newRepo.Did, 406 Rkey: newRepo.Rkey, 407 SwapRecord: ex.Cid, 408 Record: &lexutil.LexiconTypeDecoder{ 409 Val: &repoRecord, 410 }, 411 }) 412 if err != nil { 413 fail("Failed to update repo record.", err) 414 return 415 } 416 417 // transaction for DB changes 418 tx, err := rp.db.BeginTx(r.Context(), nil) 419 if err != nil { 420 fail("Failed to delete label.", err) 421 return 422 } 423 defer tx.Rollback() 424 425 err = db.UnsubscribeLabel( 426 tx, 427 orm.FilterEq("repo_did", f.RepoDid), 428 orm.FilterEq("label_at", removedAt), 429 ) 430 if err != nil { 431 fail("Failed to unsubscribe label.", err) 432 return 433 } 434 435 err = db.DeleteLabelDefinition(tx, orm.FilterEq("id", label.Id)) 436 if err != nil { 437 fail("Failed to delete label definition.", err) 438 return 439 } 440 441 err = tx.Commit() 442 if err != nil { 443 fail("Failed to delete label.", err) 444 return 445 } 446 447 // everything succeeded 448 rp.pages.HxRefresh(w) 449} 450 451func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) { 452 user := rp.oauth.GetMultiAccountUser(r) 453 l := rp.logger.With("handler", "SubscribeLabel") 454 l = l.With("did", user.Did) 455 456 f, err := rp.repoResolver.Resolve(r) 457 if err != nil { 458 l.Error("failed to get repo and knot", "err", err) 459 return 460 } 461 462 if err := r.ParseForm(); err != nil { 463 l.Error("invalid form", "err", err) 464 return 465 } 466 467 errorId := "default-label-operation" 468 fail := func(msg string, err error) { 469 l.Error(msg, "err", err) 470 rp.pages.Notice(w, errorId, msg) 471 } 472 473 labelAts := r.Form["label"] 474 _, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts)) 475 if err != nil { 476 fail("Failed to subscribe to label.", err) 477 return 478 } 479 480 newRepo := *f 481 newRepo.Labels = append(newRepo.Labels, labelAts...) 482 483 // dedup 484 slices.Sort(newRepo.Labels) 485 newRepo.Labels = slices.Compact(newRepo.Labels) 486 487 repoRecord := newRepo.AsRecord() 488 489 client, err := rp.oauth.AuthorizedClient(r) 490 if err != nil { 491 fail(err.Error(), err) 492 return 493 } 494 495 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Did, f.Rkey) 496 if err != nil { 497 fail("Failed to update labels, no record found on PDS.", err) 498 return 499 } 500 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 501 Collection: tangled.RepoNSID, 502 Repo: newRepo.Did, 503 Rkey: newRepo.Rkey, 504 SwapRecord: ex.Cid, 505 Record: &lexutil.LexiconTypeDecoder{ 506 Val: &repoRecord, 507 }, 508 }) 509 510 tx, err := rp.db.Begin() 511 if err != nil { 512 fail("Failed to subscribe to label.", err) 513 return 514 } 515 defer tx.Rollback() 516 517 for _, l := range labelAts { 518 err = db.SubscribeLabel(tx, &models.RepoLabel{ 519 RepoDid: syntax.DID(f.RepoDid), 520 LabelAt: syntax.ATURI(l), 521 }) 522 if err != nil { 523 fail("Failed to subscribe to label.", err) 524 return 525 } 526 } 527 528 if err := tx.Commit(); err != nil { 529 fail("Failed to subscribe to label.", err) 530 return 531 } 532 533 // everything succeeded 534 rp.pages.HxRefresh(w) 535} 536 537func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) { 538 user := rp.oauth.GetMultiAccountUser(r) 539 l := rp.logger.With("handler", "UnsubscribeLabel") 540 l = l.With("did", user.Did) 541 542 f, err := rp.repoResolver.Resolve(r) 543 if err != nil { 544 l.Error("failed to get repo and knot", "err", err) 545 return 546 } 547 548 if err := r.ParseForm(); err != nil { 549 l.Error("invalid form", "err", err) 550 return 551 } 552 553 errorId := "default-label-operation" 554 fail := func(msg string, err error) { 555 l.Error(msg, "err", err) 556 rp.pages.Notice(w, errorId, msg) 557 } 558 559 labelAts := r.Form["label"] 560 _, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts)) 561 if err != nil { 562 fail("Failed to unsubscribe to label.", err) 563 return 564 } 565 566 // update repo record to remove the label reference 567 newRepo := *f 568 var updated []string 569 for _, l := range newRepo.Labels { 570 if !slices.Contains(labelAts, l) { 571 updated = append(updated, l) 572 } 573 } 574 newRepo.Labels = updated 575 repoRecord := newRepo.AsRecord() 576 577 client, err := rp.oauth.AuthorizedClient(r) 578 if err != nil { 579 fail(err.Error(), err) 580 return 581 } 582 583 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Did, f.Rkey) 584 if err != nil { 585 fail("Failed to update labels, no record found on PDS.", err) 586 return 587 } 588 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 589 Collection: tangled.RepoNSID, 590 Repo: newRepo.Did, 591 Rkey: newRepo.Rkey, 592 SwapRecord: ex.Cid, 593 Record: &lexutil.LexiconTypeDecoder{ 594 Val: &repoRecord, 595 }, 596 }) 597 598 err = db.UnsubscribeLabel( 599 rp.db, 600 orm.FilterEq("repo_did", f.RepoDid), 601 orm.FilterIn("label_at", labelAts), 602 ) 603 if err != nil { 604 fail("Failed to unsubscribe label.", err) 605 return 606 } 607 608 // everything succeeded 609 rp.pages.HxRefresh(w) 610} 611 612func (rp *Repo) LabelPanel(w http.ResponseWriter, r *http.Request) { 613 l := rp.logger.With("handler", "LabelPanel") 614 615 f, err := rp.repoResolver.Resolve(r) 616 if err != nil { 617 l.Error("failed to get repo and knot", "err", err) 618 return 619 } 620 621 subjectStr := r.FormValue("subject") 622 subject, err := syntax.ParseATURI(subjectStr) 623 if err != nil { 624 l.Error("failed to get repo and knot", "err", err) 625 return 626 } 627 628 labelDefs, err := db.GetLabelDefinitions( 629 rp.db, 630 orm.FilterIn("at_uri", f.Labels), 631 orm.FilterContains("scope", subject.Collection().String()), 632 ) 633 if err != nil { 634 l.Error("failed to fetch label defs", "err", err) 635 return 636 } 637 638 defs := make(map[string]*models.LabelDefinition) 639 for _, l := range labelDefs { 640 defs[l.AtUri().String()] = &l 641 } 642 643 states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject)) 644 if err != nil { 645 l.Error("failed to build label state", "err", err) 646 return 647 } 648 state := states[subject] 649 650 user := rp.oauth.GetMultiAccountUser(r) 651 rp.pages.LabelPanel(w, pages.LabelPanelParams{ 652 LoggedInUser: user, 653 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 654 Defs: defs, 655 Subject: subject.String(), 656 State: state, 657 }) 658} 659 660func (rp *Repo) EditLabelPanel(w http.ResponseWriter, r *http.Request) { 661 l := rp.logger.With("handler", "EditLabelPanel") 662 663 f, err := rp.repoResolver.Resolve(r) 664 if err != nil { 665 l.Error("failed to get repo and knot", "err", err) 666 return 667 } 668 669 subjectStr := r.FormValue("subject") 670 subject, err := syntax.ParseATURI(subjectStr) 671 if err != nil { 672 l.Error("failed to get repo and knot", "err", err) 673 return 674 } 675 676 labelDefs, err := db.GetLabelDefinitions( 677 rp.db, 678 orm.FilterIn("at_uri", f.Labels), 679 orm.FilterContains("scope", subject.Collection().String()), 680 ) 681 if err != nil { 682 l.Error("failed to fetch labels", "err", err) 683 return 684 } 685 686 defs := make(map[string]*models.LabelDefinition) 687 for _, l := range labelDefs { 688 defs[l.AtUri().String()] = &l 689 } 690 691 states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject)) 692 if err != nil { 693 l.Error("failed to build label state", "err", err) 694 return 695 } 696 state := states[subject] 697 698 user := rp.oauth.GetMultiAccountUser(r) 699 rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{ 700 LoggedInUser: user, 701 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 702 Defs: defs, 703 Subject: subject.String(), 704 State: state, 705 }) 706} 707 708func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 709 user := rp.oauth.GetMultiAccountUser(r) 710 l := rp.logger.With("handler", "AddCollaborator") 711 l = l.With("did", user.Did) 712 713 f, err := rp.repoResolver.Resolve(r) 714 if err != nil { 715 l.Error("failed to get repo and knot", "err", err) 716 return 717 } 718 719 errorId := "add-collaborator-error" 720 fail := func(msg string, err error) { 721 l.Error(msg, "err", err) 722 rp.pages.Notice(w, errorId, msg) 723 } 724 725 collaborator := r.FormValue("collaborator") 726 if collaborator == "" { 727 fail("Invalid form.", nil) 728 return 729 } 730 731 // remove a single leading `@`, to make @handle work with ResolveIdent 732 collaborator = strings.TrimPrefix(collaborator, "@") 733 734 collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator) 735 if err != nil { 736 fail(fmt.Sprintf("'%s' is not a valid DID/handle.", collaborator), err) 737 return 738 } 739 740 if collaboratorIdent.DID.String() == user.Did { 741 fail("You seem to be adding yourself as a collaborator.", nil) 742 return 743 } 744 l = l.With("collaborator", collaboratorIdent.Handle) 745 l = l.With("knot", f.Knot) 746 747 // announce this relation into the firehose, store into owners' pds 748 client, err := rp.oauth.AuthorizedClient(r) 749 if err != nil { 750 fail("Failed to write to PDS.", err) 751 return 752 } 753 754 // emit a record 755 currentUser := rp.oauth.GetMultiAccountUser(r) 756 rkey := tid.TID() 757 createdAt := time.Now() 758 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 759 Collection: tangled.RepoCollaboratorNSID, 760 Repo: currentUser.Did, 761 Rkey: rkey, 762 Record: &lexutil.LexiconTypeDecoder{ 763 Val: repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt), 764 }, 765 }) 766 // invalid record 767 if err != nil { 768 fail("Failed to write record to PDS.", err) 769 return 770 } 771 772 aturi := resp.Uri 773 l = l.With("at-uri", aturi) 774 l.Info("wrote record to PDS") 775 776 tx, err := rp.db.BeginTx(r.Context(), nil) 777 if err != nil { 778 fail("Failed to add collaborator.", err) 779 return 780 } 781 782 rollback := func() { 783 err1 := tx.Rollback() 784 err2 := rp.enforcer.E.LoadPolicy() 785 err3 := rollbackRecord(context.Background(), aturi, client) 786 787 // ignore txn complete errors, this is okay 788 if errors.Is(err1, sql.ErrTxDone) { 789 err1 = nil 790 } 791 792 if errs := errors.Join(err1, err2, err3); errs != nil { 793 l.Error("failed to rollback changes", "errs", errs) 794 return 795 } 796 } 797 defer rollback() 798 799 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.RepoIdentifier()) 800 if err != nil { 801 fail("Failed to add collaborator permissions.", err) 802 return 803 } 804 805 err = db.AddCollaborator(tx, models.Collaborator{ 806 Did: syntax.DID(currentUser.Did), 807 Rkey: rkey, 808 SubjectDid: collaboratorIdent.DID, 809 RepoDid: syntax.DID(f.RepoDid), 810 Created: createdAt, 811 }) 812 if err != nil { 813 fail("Failed to add collaborator.", err) 814 return 815 } 816 817 err = tx.Commit() 818 if err != nil { 819 fail("Failed to add collaborator.", err) 820 return 821 } 822 823 err = rp.enforcer.E.SavePolicy() 824 if err != nil { 825 fail("Failed to update collaborator permissions.", err) 826 return 827 } 828 829 // clear aturi to when everything is successful 830 aturi = "" 831 832 rp.pages.HxRefresh(w) 833} 834 835func (rp *Repo) RenameRepo(w http.ResponseWriter, r *http.Request) { 836 l := rp.logger.With("handler", "RenameRepo") 837 noticeId := "rename-repo-error" 838 839 user := rp.oauth.GetMultiAccountUser(r) 840 f, err := rp.repoResolver.Resolve(r) 841 if err != nil { 842 l.Error("failed to get repo and knot", "err", err) 843 rp.pages.Notice(w, noticeId, "Failed to load repository.") 844 return 845 } 846 l = l.With("did", user.Did, "rkey", f.Rkey, "oldName", f.Name) 847 848 if f.RepoDid == "" { 849 rp.pages.Notice(w, noticeId, "This repository's knot has not completed the DID migration; rename is unavailable.") 850 return 851 } 852 853 newName, err := validateRenameInput(f.Name, f.Rkey, r.FormValue("name")) 854 if err != nil { 855 rp.pages.Notice(w, noticeId, err.Error()) 856 return 857 } 858 newRkey := strings.ToLower(newName) 859 l = l.With("newName", newName, "newRkey", newRkey) 860 861 atpClient, err := rp.oauth.AuthorizedClient(r) 862 if err != nil { 863 l.Error("failed to get authorized client", "err", err) 864 rp.pages.Notice(w, noticeId, "Failed to authorize. Try again later.") 865 return 866 } 867 868 newRepo := *f 869 newRepo.Name = newName 870 newRepo.Rkey = newRkey 871 newRepo.Created = time.Now() 872 record := newRepo.AsRecord() 873 874 if newRkey == f.Rkey { 875 ex, err := comatproto.RepoGetRecord(r.Context(), atpClient, "", tangled.RepoNSID, f.Did, f.Rkey) 876 if err != nil { 877 l.Error("failed to fetch existing record", "err", err) 878 rp.pages.Notice(w, noticeId, "Failed to read repository record from PDS.") 879 return 880 } 881 882 _, err = comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 883 Collection: tangled.RepoNSID, 884 Repo: f.Did, 885 Rkey: f.Rkey, 886 SwapRecord: ex.Cid, 887 Record: &lexutil.LexiconTypeDecoder{ 888 Val: &record, 889 }, 890 }) 891 if err != nil { 892 l.Error("failed to update display name on PDS", "err", err) 893 rp.pages.Notice(w, noticeId, "Failed to save display name to PDS.") 894 return 895 } 896 l.Info("updated display name on PDS") 897 898 if err := db.UpdateRepoDisplayName(rp.db, f.Did, f.Rkey, newName); err != nil { 899 l.Error("optimistic display name update failed", "err", err) 900 } 901 } else { 902 ex, getErr := comatproto.RepoGetRecord(r.Context(), atpClient, "", tangled.RepoNSID, f.Did, newRkey) 903 switch { 904 case getErr != nil: 905 _, err = comatproto.RepoCreateRecord(r.Context(), atpClient, &comatproto.RepoCreateRecord_Input{ 906 Collection: tangled.RepoNSID, 907 Repo: f.Did, 908 Rkey: &newRkey, 909 Record: &lexutil.LexiconTypeDecoder{Val: &record}, 910 }) 911 if err != nil { 912 l.Error("failed to write rename to PDS", "err", err) 913 rp.pages.Notice(w, noticeId, "Failed to save renamed repository to PDS.") 914 return 915 } 916 l.Info("wrote rename-create to PDS; old record retained as alias") 917 918 default: 919 existing, ok := ex.Value.Val.(*tangled.Repo) 920 if !ok || existing.RepoDid == nil || *existing.RepoDid != f.RepoDid { 921 rp.pages.Notice(w, noticeId, fmt.Sprintf("You already have a repository named %q.", newRkey)) 922 return 923 } 924 _, err = comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 925 Collection: tangled.RepoNSID, 926 Repo: f.Did, 927 Rkey: newRkey, 928 SwapRecord: ex.Cid, 929 Record: &lexutil.LexiconTypeDecoder{Val: &record}, 930 }) 931 if err != nil { 932 l.Error("failed to rewrite rename-back record on PDS", "err", err) 933 rp.pages.Notice(w, noticeId, "Failed to save renamed repository to PDS.") 934 return 935 } 936 l.Info("rewrote rename-back record on PDS over prior alias") 937 } 938 939 tx, err := rp.db.Begin() 940 if err != nil { 941 l.Error("failed to begin rename tx", "err", err) 942 rp.pages.HxLocation(w, fmt.Sprintf("/%s", f.RepoDid)) 943 return 944 } 945 defer tx.Rollback() 946 947 if err := db.RenameRepo(tx, f.Did, f.Rkey, newRkey, newName); err != nil { 948 l.Error("optimistic rename failed", "err", err) 949 rp.pages.HxLocation(w, fmt.Sprintf("/%s", f.RepoDid)) 950 return 951 } 952 if err := db.RecordRepoRename(tx, f.Did, f.Rkey, f.RepoDid); err != nil { 953 l.Error("failed to record rename history", "err", err) 954 } 955 if err := db.DeleteRepoRename(tx, f.Did, newRkey); err != nil { 956 l.Error("failed to clear stale rename hint", "err", err) 957 } 958 if err := tx.Commit(); err != nil { 959 l.Error("failed to commit rename tx", "err", err) 960 rp.pages.HxLocation(w, fmt.Sprintf("/%s", f.RepoDid)) 961 return 962 } 963 } 964 965 oldRepo := *f 966 rp.notifier.RenameRepo(r.Context(), syntax.DID(user.Did), &oldRepo, &newRepo) 967 968 if newRkey != f.Rkey { 969 rp.migrateSiteOnRename(r.Context(), f, newRkey) 970 } 971 972 rp.pages.HxLocation(w, fmt.Sprintf("/%s", f.RepoDid)) 973} 974 975func validateRenameInput(currentName, currentRkey, raw string) (string, error) { 976 newName := strings.TrimSpace(raw) 977 if newName == "" { 978 return "", errors.New("Repository name cannot be empty.") 979 } 980 if err := models.ValidateRepoName(newName); err != nil { 981 return "", err 982 } 983 newName = models.StripGitExt(newName) 984 if newName == currentName { 985 if _, tidErr := syntax.ParseTID(currentRkey); tidErr == nil { 986 return newName, nil 987 } 988 return "", errors.New("New name matches the current name.") 989 } 990 return newName, nil 991} 992 993func (rp *Repo) migrateSiteOnRename(ctx context.Context, oldRepo *models.Repo, newRkey string) { 994 l := rp.logger.With("handler", "migrateSiteOnRename", "repo_did", oldRepo.RepoDid) 995 996 siteConfig, err := db.GetRepoSiteConfig(rp.db, oldRepo.RepoDid) 997 if err != nil || siteConfig == nil { 998 return 999 } 1000 1001 if !rp.cfClient.Enabled() { 1002 return 1003 } 1004 1005 ownerClaim, _ := db.GetActiveDomainClaimForDid(rp.db, oldRepo.Did) 1006 1007 go func() { 1008 bgCtx := context.Background() 1009 oldRkey := oldRepo.Rkey 1010 1011 if err := sites.Delete(bgCtx, rp.cfClient, oldRepo.Did, oldRkey); err != nil { 1012 l.Error("sites: failed to delete old R2 prefix", "oldRkey", oldRkey, "err", err) 1013 } 1014 1015 newRepo := *oldRepo 1016 newRepo.Rkey = newRkey 1017 if deployErr := sites.Deploy(bgCtx, rp.cfClient, rp.config, &newRepo, siteConfig.Branch, siteConfig.Dir); deployErr != nil { 1018 l.Error("sites: redeploy after rename failed", "err", deployErr) 1019 } 1020 1021 if ownerClaim != nil { 1022 if err := sites.DeleteDomainMapping(bgCtx, rp.cfClient, ownerClaim.Domain, oldRkey); err != nil { 1023 l.Error("sites: failed to remove old KV mapping", "oldRkey", oldRkey, "err", err) 1024 } 1025 if err := sites.PutDomainMapping(bgCtx, rp.cfClient, ownerClaim.Domain, oldRepo.Did, newRkey, siteConfig.IsIndex); err != nil { 1026 l.Error("sites: failed to write new KV mapping", "newRkey", newRkey, "err", err) 1027 } 1028 } 1029 1030 l.Info("sites: migrated on rename", "oldRkey", oldRkey, "newRkey", newRkey) 1031 }() 1032} 1033 1034func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 1035 user := rp.oauth.GetMultiAccountUser(r) 1036 l := rp.logger.With("handler", "DeleteRepo") 1037 1038 noticeId := "operation-error" 1039 f, err := rp.repoResolver.Resolve(r) 1040 if err != nil { 1041 l.Error("failed to get repo and knot", "err", err) 1042 return 1043 } 1044 1045 // remove record from pds 1046 atpClient, err := rp.oauth.AuthorizedClient(r) 1047 if err != nil { 1048 l.Error("failed to get authorized client", "err", err) 1049 return 1050 } 1051 _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ 1052 Collection: tangled.RepoNSID, 1053 Repo: user.Did, 1054 Rkey: f.Rkey, 1055 }) 1056 if err != nil { 1057 l.Error("failed to delete record", "err", err) 1058 rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.") 1059 return 1060 } 1061 l.Info("removed repo record", "aturi", f.RepoAt().String()) 1062 1063 client, err := rp.oauth.ServiceClient( 1064 r, 1065 oauth.WithService(f.Knot), 1066 oauth.WithLxm(tangled.RepoDeleteNSID), 1067 oauth.WithDev(rp.config.Core.Dev), 1068 ) 1069 if err != nil { 1070 l.Error("failed to connect to knot server", "err", err) 1071 return 1072 } 1073 1074 err = tangled.RepoDelete( 1075 r.Context(), 1076 client, 1077 &tangled.RepoDelete_Input{ 1078 Did: f.Did, 1079 Name: f.Name, 1080 Rkey: f.Rkey, 1081 }, 1082 ) 1083 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1084 l.Error("failed to call XRPC repo.delete", "xrpcerr", xrpcerr, "err", err) 1085 rp.pages.Notice(w, noticeId, xrpcerr.Error()) 1086 return 1087 } 1088 l.Info("deleted repo from knot") 1089 1090 tx, err := rp.db.BeginTx(r.Context(), nil) 1091 if err != nil { 1092 l.Error("failed to start tx") 1093 w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 1094 return 1095 } 1096 defer func() { 1097 tx.Rollback() 1098 err = rp.enforcer.E.LoadPolicy() 1099 if err != nil { 1100 l.Error("failed to rollback policies") 1101 } 1102 }() 1103 1104 // remove collaborator RBAC 1105 repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.RepoIdentifier(), f.Knot) 1106 if err != nil { 1107 rp.pages.Notice(w, noticeId, "Failed to remove collaborators") 1108 return 1109 } 1110 for _, c := range repoCollaborators { 1111 did := c[0] 1112 rp.enforcer.RemoveCollaborator(did, f.Knot, f.RepoIdentifier()) 1113 } 1114 l.Info("removed collaborators") 1115 1116 // remove repo RBAC 1117 err = rp.enforcer.RemoveRepo(f.Did, f.Knot, f.RepoIdentifier()) 1118 if err != nil { 1119 rp.pages.Notice(w, noticeId, "Failed to update RBAC rules") 1120 return 1121 } 1122 1123 // remove repo from db 1124 err = db.RemoveRepo(tx, f.Did, f.Rkey) 1125 if err != nil { 1126 rp.pages.Notice(w, noticeId, "Failed to update appview") 1127 return 1128 } 1129 l.Info("removed repo from db") 1130 1131 err = tx.Commit() 1132 if err != nil { 1133 l.Error("failed to commit changes", "err", err) 1134 http.Error(w, err.Error(), http.StatusInternalServerError) 1135 return 1136 } 1137 1138 err = rp.enforcer.E.SavePolicy() 1139 if err != nil { 1140 l.Error("failed to update ACLs", "err", err) 1141 http.Error(w, err.Error(), http.StatusInternalServerError) 1142 return 1143 } 1144 1145 rp.notifier.DeleteRepo(r.Context(), f) 1146 rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.Did)) 1147} 1148 1149func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1150 l := rp.logger.With("handler", "SyncRepoFork") 1151 1152 ref := chi.URLParam(r, "ref") 1153 ref, _ = url.PathUnescape(ref) 1154 1155 user := rp.oauth.GetMultiAccountUser(r) 1156 f, err := rp.repoResolver.Resolve(r) 1157 if err != nil { 1158 l.Error("failed to resolve source repo", "err", err) 1159 return 1160 } 1161 1162 switch r.Method { 1163 case http.MethodPost: 1164 client, err := rp.oauth.ServiceClient( 1165 r, 1166 oauth.WithService(f.Knot), 1167 oauth.WithLxm(tangled.RepoForkSyncNSID), 1168 oauth.WithDev(rp.config.Core.Dev), 1169 ) 1170 if err != nil { 1171 rp.pages.Notice(w, "repo", "Failed to connect to knot server.") 1172 return 1173 } 1174 1175 if f.Source == "" { 1176 rp.pages.Notice(w, "repo", "This repository is not a fork.") 1177 return 1178 } 1179 1180 err = tangled.RepoForkSync( 1181 r.Context(), 1182 client, 1183 &tangled.RepoForkSync_Input{ 1184 Did: user.Did, 1185 Name: f.Name, 1186 Source: f.Source, 1187 Branch: ref, 1188 }, 1189 ) 1190 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1191 l.Error("failed to call XRPC repo.forkSync", "xrpcerr", xrpcerr, "err", err) 1192 rp.pages.Notice(w, "repo", err.Error()) 1193 return 1194 } 1195 1196 rp.pages.HxRefresh(w) 1197 return 1198 } 1199} 1200 1201func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { 1202 l := rp.logger.With("handler", "ForkRepo") 1203 1204 user := rp.oauth.GetMultiAccountUser(r) 1205 f, err := rp.repoResolver.Resolve(r) 1206 if err != nil { 1207 l.Error("failed to resolve source repo", "err", err) 1208 return 1209 } 1210 1211 switch r.Method { 1212 case http.MethodGet: 1213 user := rp.oauth.GetMultiAccountUser(r) 1214 knots, err := rp.enforcer.GetKnotsForUser(user.Did) 1215 if err != nil { 1216 rp.pages.Notice(w, "repo", "Invalid user account.") 1217 return 1218 } 1219 1220 rp.pages.ForkRepo(w, pages.ForkRepoParams{ 1221 LoggedInUser: user, 1222 Knots: knots, 1223 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 1224 }) 1225 1226 case http.MethodPost: 1227 l := rp.logger.With("handler", "ForkRepo") 1228 1229 targetKnot := r.FormValue("knot") 1230 if targetKnot == "" { 1231 rp.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.") 1232 return 1233 } 1234 l = l.With("targetKnot", targetKnot) 1235 1236 ok, err := rp.enforcer.E.Enforce(user.Did, targetKnot, targetKnot, "repo:create") 1237 if err != nil || !ok { 1238 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1239 return 1240 } 1241 1242 // choose a name for a fork 1243 forkName := strings.ToLower(r.FormValue("repo_name")) 1244 if forkName == "" { 1245 rp.pages.Notice(w, "repo", "Repository name cannot be empty.") 1246 return 1247 } 1248 1249 // this check is *only* to see if the forked repo name already exists 1250 // in the user's account. 1251 existingRepo, err := db.GetRepo( 1252 rp.db, 1253 orm.FilterEq("did", user.Did), 1254 orm.FilterEq("name", forkName), 1255 ) 1256 if err != nil { 1257 if !errors.Is(err, sql.ErrNoRows) { 1258 l.Error("error fetching existing repo from db", "err", err) 1259 rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 1260 return 1261 } 1262 } else if existingRepo != nil { 1263 // repo with this name already exists 1264 rp.pages.Notice(w, "repo", "A repository with this name already exists.") 1265 return 1266 } 1267 l = l.With("forkName", forkName) 1268 1269 uri := "https" 1270 if rp.config.Core.Dev { 1271 uri = "http" 1272 } 1273 1274 forkSourceUrl := fmt.Sprintf("%s://%s/%s", uri, f.Knot, f.RepoIdentifier()) 1275 l = l.With("cloneUrl", forkSourceUrl) 1276 1277 rkey := strings.ToLower(forkName) 1278 1279 // TODO: this could coordinate better with the knot to receive a clone status 1280 client, err := rp.oauth.ServiceClient( 1281 r, 1282 oauth.WithService(targetKnot), 1283 oauth.WithLxm(tangled.RepoCreateNSID), 1284 oauth.WithDev(rp.config.Core.Dev), 1285 oauth.WithTimeout(time.Second*20), 1286 ) 1287 if err != nil { 1288 l.Error("could not create service client", "err", err) 1289 rp.pages.Notice(w, "repo", "Failed to connect to knot server.") 1290 return 1291 } 1292 1293 forkInput := &tangled.RepoCreate_Input{ 1294 Rkey: rkey, 1295 Name: rkey, 1296 Source: &forkSourceUrl, 1297 } 1298 createResp, err := tangled.RepoCreate( 1299 r.Context(), 1300 client, 1301 forkInput, 1302 ) 1303 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1304 l.Error("failed to call XRPC repo.create", "xrpcerr", xrpcerr, "err", err) 1305 rp.pages.Notice(w, "repo", xrpcerr.Error()) 1306 return 1307 } 1308 1309 var repoDid string 1310 if createResp != nil && createResp.RepoDid != nil { 1311 repoDid = *createResp.RepoDid 1312 } 1313 if repoDid == "" { 1314 l.Error("knot returned empty repo DID for fork") 1315 rp.pages.Notice(w, "repo", "Knot failed to mint a repo DID. The knot may need to be upgraded.") 1316 return 1317 } 1318 1319 forkSource := f.RepoAt().String() 1320 if f.RepoDid != "" { 1321 forkSource = f.RepoDid 1322 } 1323 1324 repo := &models.Repo{ 1325 Did: user.Did, 1326 Name: rkey, 1327 Knot: targetKnot, 1328 Rkey: rkey, 1329 Source: forkSource, 1330 Description: f.Description, 1331 Created: time.Now(), 1332 Labels: rp.config.Label.DefaultLabelDefs, 1333 RepoDid: repoDid, 1334 } 1335 record := repo.AsRecord() 1336 1337 cleanupKnot := func() { 1338 go func() { 1339 delays := []time.Duration{0, 2 * time.Second, 5 * time.Second} 1340 for attempt, delay := range delays { 1341 time.Sleep(delay) 1342 deleteClient, dErr := rp.oauth.ServiceClient( 1343 r, 1344 oauth.WithService(targetKnot), 1345 oauth.WithLxm(tangled.RepoDeleteNSID), 1346 oauth.WithDev(rp.config.Core.Dev), 1347 ) 1348 if dErr != nil { 1349 l.Error("failed to create delete client for knot cleanup", "attempt", attempt+1, "err", dErr) 1350 continue 1351 } 1352 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 1353 if dErr := tangled.RepoDelete(ctx, deleteClient, &tangled.RepoDelete_Input{ 1354 Did: user.Did, 1355 Name: forkName, 1356 Rkey: rkey, 1357 }); dErr != nil { 1358 cancel() 1359 l.Error("failed to clean up fork on knot after rollback", "attempt", attempt+1, "err", dErr) 1360 continue 1361 } 1362 cancel() 1363 l.Info("successfully cleaned up fork on knot after rollback", "attempt", attempt+1) 1364 return 1365 } 1366 l.Error("exhausted retries for knot cleanup, fork may be orphaned", 1367 "did", user.Did, "fork", forkName, "knot", targetKnot) 1368 }() 1369 } 1370 1371 atpClient, err := rp.oauth.AuthorizedClient(r) 1372 if err != nil { 1373 l.Error("failed to create xrpcclient", "err", err) 1374 cleanupKnot() 1375 rp.pages.Notice(w, "repo", "Failed to fork repository.") 1376 return 1377 } 1378 1379 atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 1380 Collection: tangled.RepoNSID, 1381 Repo: user.Did, 1382 Rkey: rkey, 1383 Record: &lexutil.LexiconTypeDecoder{ 1384 Val: &record, 1385 }, 1386 }) 1387 if err != nil { 1388 l.Error("failed to write to PDS", "err", err) 1389 cleanupKnot() 1390 rp.pages.Notice(w, "repo", "Failed to announce repository creation.") 1391 return 1392 } 1393 1394 aturi := atresp.Uri 1395 l = l.With("aturi", aturi) 1396 l.Info("wrote to PDS") 1397 1398 tx, err := rp.db.BeginTx(r.Context(), nil) 1399 if err != nil { 1400 l.Info("txn failed", "err", err) 1401 rp.pages.Notice(w, "repo", "Failed to save repository information.") 1402 return 1403 } 1404 1405 rollback := func() { 1406 err1 := tx.Rollback() 1407 err2 := rp.enforcer.E.LoadPolicy() 1408 err3 := rollbackRecord(context.Background(), aturi, atpClient) 1409 1410 if errors.Is(err1, sql.ErrTxDone) { 1411 err1 = nil 1412 } 1413 1414 if errs := errors.Join(err1, err2, err3); errs != nil { 1415 l.Error("failed to rollback changes", "errs", errs) 1416 } 1417 1418 if aturi != "" { 1419 cleanupKnot() 1420 } 1421 } 1422 defer rollback() 1423 1424 err = db.AddRepo(tx, repo) 1425 if err != nil { 1426 l.Error("failed to AddRepo", "err", err) 1427 rp.pages.Notice(w, "repo", "Failed to save repository information.") 1428 return 1429 } 1430 1431 rbacPath := repo.RepoIdentifier() 1432 err = rp.enforcer.AddRepo(user.Did, targetKnot, rbacPath) 1433 if err != nil { 1434 l.Error("failed to add ACLs", "err", err) 1435 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") 1436 return 1437 } 1438 1439 err = tx.Commit() 1440 if err != nil { 1441 l.Error("failed to commit changes", "err", err) 1442 http.Error(w, err.Error(), http.StatusInternalServerError) 1443 return 1444 } 1445 1446 err = rp.enforcer.E.SavePolicy() 1447 if err != nil { 1448 l.Error("failed to update ACLs", "err", err) 1449 http.Error(w, err.Error(), http.StatusInternalServerError) 1450 return 1451 } 1452 1453 aturi = "" 1454 1455 rp.notifier.NewRepo(r.Context(), repo) 1456 if repoDid != "" { 1457 rp.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid)) 1458 } else { 1459 rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, forkName)) 1460 } 1461 } 1462} 1463 1464func (rp *Repo) Stars(w http.ResponseWriter, r *http.Request) { 1465 l := rp.logger.With("handler", "Stars") 1466 1467 user := rp.oauth.GetMultiAccountUser(r) 1468 f, err := rp.repoResolver.Resolve(r) 1469 if err != nil { 1470 l.Error("failed to resolve source repo", "err", err) 1471 return 1472 } 1473 1474 page := pagination.FromContext(r.Context()) 1475 if page.Limit > 30 || page.Limit <= 0 { 1476 page.Limit = 30 1477 } 1478 1479 starrers, err := db.GetStars(rp.db, string(f.RepoDid), page) 1480 if err != nil { 1481 l.Error("failed to fetch starrers", "err", err, "repoDid", f.RepoDid) 1482 return 1483 } 1484 1485 totalCount, err := db.GetStarCount(rp.db, models.StarSubjectRepo, string(f.RepoDid)) 1486 if err != nil { 1487 l.Error("failed to fetch star count", "err", err, "repoDid", f.RepoDid) 1488 return 1489 } 1490 1491 rp.pages.RepoStars(w, pages.RepoStarsParams{ 1492 LoggedInUser: user, 1493 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 1494 Starrers: starrers, 1495 Page: page, 1496 TotalCount: totalCount, 1497 }) 1498} 1499 1500// this is used to rollback changes made to the PDS 1501// 1502// it is a no-op if the provided ATURI is empty 1503func rollbackRecord(ctx context.Context, aturi string, client *atclient.APIClient) error { 1504 if aturi == "" { 1505 return nil 1506 } 1507 1508 parsed := syntax.ATURI(aturi) 1509 1510 collection := parsed.Collection().String() 1511 repo := parsed.Authority().String() 1512 rkey := parsed.RecordKey().String() 1513 1514 _, err := comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{ 1515 Collection: collection, 1516 Repo: repo, 1517 Rkey: rkey, 1518 }) 1519 return err 1520} 1521 1522func repoCollaboratorRecord(f *models.Repo, subject string, createdAt time.Time) *tangled.RepoCollaborator { 1523 return &tangled.RepoCollaborator{ 1524 Subject: subject, 1525 CreatedAt: createdAt.Format(time.RFC3339), 1526 Repo: f.RepoDid, 1527 } 1528}