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