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