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