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