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