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