forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at knot-xrpc 18 kB view raw
1package spindles 2 3import ( 4 "errors" 5 "fmt" 6 "log/slog" 7 "net/http" 8 "slices" 9 "time" 10 11 "github.com/go-chi/chi/v5" 12 "tangled.sh/tangled.sh/core/api/tangled" 13 "tangled.sh/tangled.sh/core/appview/config" 14 "tangled.sh/tangled.sh/core/appview/db" 15 "tangled.sh/tangled.sh/core/appview/middleware" 16 "tangled.sh/tangled.sh/core/appview/oauth" 17 "tangled.sh/tangled.sh/core/appview/pages" 18 "tangled.sh/tangled.sh/core/appview/serververify" 19 "tangled.sh/tangled.sh/core/idresolver" 20 "tangled.sh/tangled.sh/core/rbac" 21 "tangled.sh/tangled.sh/core/tid" 22 23 comatproto "github.com/bluesky-social/indigo/api/atproto" 24 "github.com/bluesky-social/indigo/atproto/syntax" 25 lexutil "github.com/bluesky-social/indigo/lex/util" 26) 27 28type Spindles struct { 29 Db *db.DB 30 OAuth *oauth.OAuth 31 Pages *pages.Pages 32 Config *config.Config 33 Enforcer *rbac.Enforcer 34 IdResolver *idresolver.Resolver 35 Logger *slog.Logger 36} 37 38func (s *Spindles) Router() http.Handler { 39 r := chi.NewRouter() 40 41 r.With(middleware.AuthMiddleware(s.OAuth)).Get("/", s.spindles) 42 r.With(middleware.AuthMiddleware(s.OAuth)).Post("/register", s.register) 43 44 r.With(middleware.AuthMiddleware(s.OAuth)).Get("/{instance}", s.dashboard) 45 r.With(middleware.AuthMiddleware(s.OAuth)).Delete("/{instance}", s.delete) 46 47 r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/retry", s.retry) 48 r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/add", s.addMember) 49 r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/remove", s.removeMember) 50 51 return r 52} 53 54func (s *Spindles) spindles(w http.ResponseWriter, r *http.Request) { 55 user := s.OAuth.GetUser(r) 56 all, err := db.GetSpindles( 57 s.Db, 58 db.FilterEq("owner", user.Did), 59 ) 60 if err != nil { 61 s.Logger.Error("failed to fetch spindles", "err", err) 62 w.WriteHeader(http.StatusInternalServerError) 63 return 64 } 65 66 s.Pages.Spindles(w, pages.SpindlesParams{ 67 LoggedInUser: user, 68 Spindles: all, 69 }) 70} 71 72func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) { 73 l := s.Logger.With("handler", "dashboard") 74 75 user := s.OAuth.GetUser(r) 76 l = l.With("user", user.Did) 77 78 instance := chi.URLParam(r, "instance") 79 if instance == "" { 80 return 81 } 82 l = l.With("instance", instance) 83 84 spindles, err := db.GetSpindles( 85 s.Db, 86 db.FilterEq("instance", instance), 87 db.FilterEq("owner", user.Did), 88 db.FilterIsNot("verified", "null"), 89 ) 90 if err != nil || len(spindles) != 1 { 91 l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles)) 92 http.Error(w, "Not found", http.StatusNotFound) 93 return 94 } 95 96 spindle := spindles[0] 97 members, err := s.Enforcer.GetSpindleUsersByRole("server:member", spindle.Instance) 98 if err != nil { 99 l.Error("failed to get spindle members", "err", err) 100 http.Error(w, "Not found", http.StatusInternalServerError) 101 return 102 } 103 slices.Sort(members) 104 105 repos, err := db.GetRepos( 106 s.Db, 107 0, 108 db.FilterEq("spindle", instance), 109 ) 110 if err != nil { 111 l.Error("failed to get spindle repos", "err", err) 112 http.Error(w, "Not found", http.StatusInternalServerError) 113 return 114 } 115 116 identsToResolve := make([]string, len(members)) 117 copy(identsToResolve, members) 118 resolvedIds := s.IdResolver.ResolveIdents(r.Context(), identsToResolve) 119 didHandleMap := make(map[string]string) 120 for _, identity := range resolvedIds { 121 if !identity.Handle.IsInvalidHandle() { 122 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 123 } else { 124 didHandleMap[identity.DID.String()] = identity.DID.String() 125 } 126 } 127 128 // organize repos by did 129 repoMap := make(map[string][]db.Repo) 130 for _, r := range repos { 131 repoMap[r.Did] = append(repoMap[r.Did], r) 132 } 133 134 s.Pages.SpindleDashboard(w, pages.SpindleDashboardParams{ 135 LoggedInUser: user, 136 Spindle: spindle, 137 Members: members, 138 Repos: repoMap, 139 DidHandleMap: didHandleMap, 140 }) 141} 142 143// this endpoint inserts a record on behalf of the user to register that domain 144// 145// when registered, it also makes a request to see if the spindle declares this users as its owner, 146// and if so, marks the spindle as verified. 147// 148// if the spindle is not up yet, the user is free to retry verification at a later point 149func (s *Spindles) register(w http.ResponseWriter, r *http.Request) { 150 user := s.OAuth.GetUser(r) 151 l := s.Logger.With("handler", "register") 152 153 noticeId := "register-error" 154 defaultErr := "Failed to register spindle. Try again later." 155 fail := func() { 156 s.Pages.Notice(w, noticeId, defaultErr) 157 } 158 159 instance := r.FormValue("instance") 160 if instance == "" { 161 s.Pages.Notice(w, noticeId, "Incomplete form.") 162 return 163 } 164 l = l.With("instance", instance) 165 l = l.With("user", user.Did) 166 167 tx, err := s.Db.Begin() 168 if err != nil { 169 l.Error("failed to start transaction", "err", err) 170 fail() 171 return 172 } 173 defer func() { 174 tx.Rollback() 175 s.Enforcer.E.LoadPolicy() 176 }() 177 178 err = db.AddSpindle(tx, db.Spindle{ 179 Owner: syntax.DID(user.Did), 180 Instance: instance, 181 }) 182 if err != nil { 183 l.Error("failed to insert", "err", err) 184 fail() 185 return 186 } 187 188 err = s.Enforcer.AddSpindle(instance) 189 if err != nil { 190 l.Error("failed to create spindle", "err", err) 191 fail() 192 return 193 } 194 195 // create record on pds 196 client, err := s.OAuth.AuthorizedClient(r) 197 if err != nil { 198 l.Error("failed to authorize client", "err", err) 199 fail() 200 return 201 } 202 203 ex, _ := client.RepoGetRecord(r.Context(), "", tangled.SpindleNSID, user.Did, instance) 204 var exCid *string 205 if ex != nil { 206 exCid = ex.Cid 207 } 208 209 // re-announce by registering under same rkey 210 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 211 Collection: tangled.SpindleNSID, 212 Repo: user.Did, 213 Rkey: instance, 214 Record: &lexutil.LexiconTypeDecoder{ 215 Val: &tangled.Spindle{ 216 CreatedAt: time.Now().Format(time.RFC3339), 217 }, 218 }, 219 SwapRecord: exCid, 220 }) 221 222 if err != nil { 223 l.Error("failed to put record", "err", err) 224 fail() 225 return 226 } 227 228 err = tx.Commit() 229 if err != nil { 230 l.Error("failed to commit transaction", "err", err) 231 fail() 232 return 233 } 234 235 err = s.Enforcer.E.SavePolicy() 236 if err != nil { 237 l.Error("failed to update ACL", "err", err) 238 s.Pages.HxRefresh(w) 239 return 240 } 241 242 // begin verification 243 err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 244 if err != nil { 245 l.Error("verification failed", "err", err) 246 s.Pages.HxRefresh(w) 247 return 248 } 249 250 _, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 251 if err != nil { 252 l.Error("failed to mark verified", "err", err) 253 s.Pages.HxRefresh(w) 254 return 255 } 256 257 // ok 258 s.Pages.HxRefresh(w) 259} 260 261func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) { 262 user := s.OAuth.GetUser(r) 263 l := s.Logger.With("handler", "delete") 264 265 noticeId := "operation-error" 266 defaultErr := "Failed to delete spindle. Try again later." 267 fail := func() { 268 s.Pages.Notice(w, noticeId, defaultErr) 269 } 270 271 instance := chi.URLParam(r, "instance") 272 if instance == "" { 273 l.Error("empty instance") 274 fail() 275 return 276 } 277 278 spindles, err := db.GetSpindles( 279 s.Db, 280 db.FilterEq("owner", user.Did), 281 db.FilterEq("instance", instance), 282 ) 283 if err != nil || len(spindles) != 1 { 284 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 285 fail() 286 return 287 } 288 289 if string(spindles[0].Owner) != user.Did { 290 l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 291 s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.") 292 return 293 } 294 295 tx, err := s.Db.Begin() 296 if err != nil { 297 l.Error("failed to start txn", "err", err) 298 fail() 299 return 300 } 301 defer func() { 302 tx.Rollback() 303 s.Enforcer.E.LoadPolicy() 304 }() 305 306 // remove spindle members first 307 err = db.RemoveSpindleMember( 308 tx, 309 db.FilterEq("did", user.Did), 310 db.FilterEq("instance", instance), 311 ) 312 if err != nil { 313 l.Error("failed to remove spindle members", "err", err) 314 fail() 315 return 316 } 317 318 err = db.DeleteSpindle( 319 tx, 320 db.FilterEq("owner", user.Did), 321 db.FilterEq("instance", instance), 322 ) 323 if err != nil { 324 l.Error("failed to delete spindle", "err", err) 325 fail() 326 return 327 } 328 329 // delete from enforcer 330 if spindles[0].Verified != nil { 331 err = s.Enforcer.RemoveSpindle(instance) 332 if err != nil { 333 l.Error("failed to update ACL", "err", err) 334 fail() 335 return 336 } 337 } 338 339 client, err := s.OAuth.AuthorizedClient(r) 340 if err != nil { 341 l.Error("failed to authorize client", "err", err) 342 fail() 343 return 344 } 345 346 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 347 Collection: tangled.SpindleNSID, 348 Repo: user.Did, 349 Rkey: instance, 350 }) 351 if err != nil { 352 // non-fatal 353 l.Error("failed to delete record", "err", err) 354 } 355 356 err = tx.Commit() 357 if err != nil { 358 l.Error("failed to delete spindle", "err", err) 359 fail() 360 return 361 } 362 363 err = s.Enforcer.E.SavePolicy() 364 if err != nil { 365 l.Error("failed to update ACL", "err", err) 366 s.Pages.HxRefresh(w) 367 return 368 } 369 370 shouldRedirect := r.Header.Get("shouldRedirect") 371 if shouldRedirect == "true" { 372 s.Pages.HxRedirect(w, "/spindles") 373 return 374 } 375 376 w.Write([]byte{}) 377} 378 379func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { 380 user := s.OAuth.GetUser(r) 381 l := s.Logger.With("handler", "retry") 382 383 noticeId := "operation-error" 384 defaultErr := "Failed to verify spindle. Try again later." 385 fail := func() { 386 s.Pages.Notice(w, noticeId, defaultErr) 387 } 388 389 instance := chi.URLParam(r, "instance") 390 if instance == "" { 391 l.Error("empty instance") 392 fail() 393 return 394 } 395 l = l.With("instance", instance) 396 l = l.With("user", user.Did) 397 398 spindles, err := db.GetSpindles( 399 s.Db, 400 db.FilterEq("owner", user.Did), 401 db.FilterEq("instance", instance), 402 ) 403 if err != nil || len(spindles) != 1 { 404 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 405 fail() 406 return 407 } 408 409 if string(spindles[0].Owner) != user.Did { 410 l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 411 s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.") 412 return 413 } 414 415 // begin verification 416 err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 417 if err != nil { 418 l.Error("verification failed", "err", err) 419 420 if errors.Is(err, serververify.FetchError) { 421 s.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.") 422 return 423 } 424 425 if e, ok := err.(*serververify.OwnerMismatch); ok { 426 s.Pages.Notice(w, noticeId, e.Error()) 427 return 428 } 429 430 fail() 431 return 432 } 433 434 rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 435 if err != nil { 436 l.Error("failed to mark verified", "err", err) 437 s.Pages.Notice(w, noticeId, err.Error()) 438 return 439 } 440 441 verifiedSpindle, err := db.GetSpindles( 442 s.Db, 443 db.FilterEq("id", rowId), 444 ) 445 if err != nil || len(verifiedSpindle) != 1 { 446 l.Error("failed get new spindle", "err", err) 447 s.Pages.HxRefresh(w) 448 return 449 } 450 451 shouldRefresh := r.Header.Get("shouldRefresh") 452 if shouldRefresh == "true" { 453 s.Pages.HxRefresh(w) 454 return 455 } 456 457 w.Header().Set("HX-Reswap", "outerHTML") 458 s.Pages.SpindleListing(w, pages.SpindleListingParams{verifiedSpindle[0]}) 459} 460 461func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) { 462 user := s.OAuth.GetUser(r) 463 l := s.Logger.With("handler", "addMember") 464 465 instance := chi.URLParam(r, "instance") 466 if instance == "" { 467 l.Error("empty instance") 468 http.Error(w, "Not found", http.StatusNotFound) 469 return 470 } 471 l = l.With("instance", instance) 472 l = l.With("user", user.Did) 473 474 spindles, err := db.GetSpindles( 475 s.Db, 476 db.FilterEq("owner", user.Did), 477 db.FilterEq("instance", instance), 478 ) 479 if err != nil || len(spindles) != 1 { 480 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 481 http.Error(w, "Not found", http.StatusNotFound) 482 return 483 } 484 485 noticeId := fmt.Sprintf("add-member-error-%d", spindles[0].Id) 486 defaultErr := "Failed to add member. Try again later." 487 fail := func() { 488 s.Pages.Notice(w, noticeId, defaultErr) 489 } 490 491 if string(spindles[0].Owner) != user.Did { 492 l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 493 s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 494 return 495 } 496 497 member := r.FormValue("member") 498 if member == "" { 499 l.Error("empty member") 500 s.Pages.Notice(w, noticeId, "Failed to add member, empty form.") 501 return 502 } 503 l = l.With("member", member) 504 505 memberId, err := s.IdResolver.ResolveIdent(r.Context(), member) 506 if err != nil { 507 l.Error("failed to resolve member identity to handle", "err", err) 508 s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 509 return 510 } 511 if memberId.Handle.IsInvalidHandle() { 512 l.Error("failed to resolve member identity to handle") 513 s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 514 return 515 } 516 517 // write to pds 518 client, err := s.OAuth.AuthorizedClient(r) 519 if err != nil { 520 l.Error("failed to authorize client", "err", err) 521 fail() 522 return 523 } 524 525 tx, err := s.Db.Begin() 526 if err != nil { 527 l.Error("failed to start txn", "err", err) 528 fail() 529 return 530 } 531 defer func() { 532 tx.Rollback() 533 s.Enforcer.E.LoadPolicy() 534 }() 535 536 rkey := tid.TID() 537 538 // add member to db 539 if err = db.AddSpindleMember(tx, db.SpindleMember{ 540 Did: syntax.DID(user.Did), 541 Rkey: rkey, 542 Instance: instance, 543 Subject: memberId.DID, 544 }); err != nil { 545 l.Error("failed to add spindle member", "err", err) 546 fail() 547 return 548 } 549 550 if err = s.Enforcer.AddSpindleMember(instance, memberId.DID.String()); err != nil { 551 l.Error("failed to add member to ACLs") 552 fail() 553 return 554 } 555 556 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 557 Collection: tangled.SpindleMemberNSID, 558 Repo: user.Did, 559 Rkey: rkey, 560 Record: &lexutil.LexiconTypeDecoder{ 561 Val: &tangled.SpindleMember{ 562 CreatedAt: time.Now().Format(time.RFC3339), 563 Instance: instance, 564 Subject: memberId.DID.String(), 565 }, 566 }, 567 }) 568 if err != nil { 569 l.Error("failed to add record to PDS", "err", err) 570 s.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.") 571 return 572 } 573 574 if err = tx.Commit(); err != nil { 575 l.Error("failed to commit txn", "err", err) 576 fail() 577 return 578 } 579 580 if err = s.Enforcer.E.SavePolicy(); err != nil { 581 l.Error("failed to add member to ACLs", "err", err) 582 fail() 583 return 584 } 585 586 // success 587 s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance)) 588} 589 590func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) { 591 user := s.OAuth.GetUser(r) 592 l := s.Logger.With("handler", "removeMember") 593 594 noticeId := "operation-error" 595 defaultErr := "Failed to remove member. Try again later." 596 fail := func() { 597 s.Pages.Notice(w, noticeId, defaultErr) 598 } 599 600 instance := chi.URLParam(r, "instance") 601 if instance == "" { 602 l.Error("empty instance") 603 fail() 604 return 605 } 606 l = l.With("instance", instance) 607 l = l.With("user", user.Did) 608 609 spindles, err := db.GetSpindles( 610 s.Db, 611 db.FilterEq("owner", user.Did), 612 db.FilterEq("instance", instance), 613 ) 614 if err != nil || len(spindles) != 1 { 615 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 616 fail() 617 return 618 } 619 620 if string(spindles[0].Owner) != user.Did { 621 l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 622 s.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.") 623 return 624 } 625 626 member := r.FormValue("member") 627 if member == "" { 628 l.Error("empty member") 629 s.Pages.Notice(w, noticeId, "Failed to remove member, empty form.") 630 return 631 } 632 l = l.With("member", member) 633 634 memberId, err := s.IdResolver.ResolveIdent(r.Context(), member) 635 if err != nil { 636 l.Error("failed to resolve member identity to handle", "err", err) 637 s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 638 return 639 } 640 if memberId.Handle.IsInvalidHandle() { 641 l.Error("failed to resolve member identity to handle") 642 s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 643 return 644 } 645 646 tx, err := s.Db.Begin() 647 if err != nil { 648 l.Error("failed to start txn", "err", err) 649 fail() 650 return 651 } 652 defer func() { 653 tx.Rollback() 654 s.Enforcer.E.LoadPolicy() 655 }() 656 657 // get the record from the DB first: 658 members, err := db.GetSpindleMembers( 659 s.Db, 660 db.FilterEq("did", user.Did), 661 db.FilterEq("instance", instance), 662 db.FilterEq("subject", memberId.DID), 663 ) 664 if err != nil || len(members) != 1 { 665 l.Error("failed to get member", "err", err) 666 fail() 667 return 668 } 669 670 // remove from db 671 if err = db.RemoveSpindleMember( 672 tx, 673 db.FilterEq("did", user.Did), 674 db.FilterEq("instance", instance), 675 db.FilterEq("subject", memberId.DID), 676 ); err != nil { 677 l.Error("failed to remove spindle member", "err", err) 678 fail() 679 return 680 } 681 682 // remove from enforcer 683 if err = s.Enforcer.RemoveSpindleMember(instance, memberId.DID.String()); err != nil { 684 l.Error("failed to update ACLs", "err", err) 685 fail() 686 return 687 } 688 689 client, err := s.OAuth.AuthorizedClient(r) 690 if err != nil { 691 l.Error("failed to authorize client", "err", err) 692 fail() 693 return 694 } 695 696 // remove from pds 697 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 698 Collection: tangled.SpindleMemberNSID, 699 Repo: user.Did, 700 Rkey: members[0].Rkey, 701 }) 702 if err != nil { 703 // non-fatal 704 l.Error("failed to delete record", "err", err) 705 } 706 707 // commit everything 708 if err = tx.Commit(); err != nil { 709 l.Error("failed to commit txn", "err", err) 710 fail() 711 return 712 } 713 714 // commit everything 715 if err = s.Enforcer.E.SavePolicy(); err != nil { 716 l.Error("failed to save ACLs", "err", err) 717 fail() 718 return 719 } 720 721 // ok 722 s.Pages.HxRefresh(w) 723}