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