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