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