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