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