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