this repo has no description
1package state 2 3import ( 4 "context" 5 "crypto/hmac" 6 "crypto/sha256" 7 "encoding/hex" 8 "encoding/json" 9 "fmt" 10 "log" 11 "log/slog" 12 "net/http" 13 "strings" 14 "time" 15 16 comatproto "github.com/bluesky-social/indigo/api/atproto" 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 lexutil "github.com/bluesky-social/indigo/lex/util" 19 "github.com/bluesky-social/jetstream/pkg/models" 20 securejoin "github.com/cyphar/filepath-securejoin" 21 "github.com/go-chi/chi/v5" 22 tangled "github.com/sotangled/tangled/api/tangled" 23 "github.com/sotangled/tangled/appview" 24 "github.com/sotangled/tangled/appview/auth" 25 "github.com/sotangled/tangled/appview/db" 26 "github.com/sotangled/tangled/appview/pages" 27 "github.com/sotangled/tangled/jetstream" 28 "github.com/sotangled/tangled/rbac" 29) 30 31type State struct { 32 db *db.DB 33 auth *auth.Auth 34 enforcer *rbac.Enforcer 35 tidClock *syntax.TIDClock 36 pages *pages.Pages 37 resolver *appview.Resolver 38 jc *jetstream.JetstreamClient 39} 40 41func Make() (*State, error) { 42 db, err := db.Make(appview.SqliteDbPath) 43 if err != nil { 44 return nil, err 45 } 46 47 auth, err := auth.Make() 48 if err != nil { 49 return nil, err 50 } 51 52 enforcer, err := rbac.NewEnforcer(appview.SqliteDbPath) 53 if err != nil { 54 return nil, err 55 } 56 57 clock := syntax.NewTIDClock(0) 58 59 pgs := pages.NewPages() 60 61 resolver := appview.NewResolver() 62 63 jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, slog.Default(), db, false) 64 if err != nil { 65 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 66 } 67 err = jc.StartJetstream(context.Background(), func(ctx context.Context, e *models.Event) error { 68 if e.Kind != models.EventKindCommit { 69 return nil 70 } 71 72 did := e.Did 73 raw := json.RawMessage(e.Commit.Record) 74 75 switch e.Commit.Collection { 76 case tangled.GraphFollowNSID: 77 record := tangled.GraphFollow{} 78 err := json.Unmarshal(raw, &record) 79 if err != nil { 80 log.Println("invalid record") 81 return err 82 } 83 err = db.AddFollow(did, record.Subject, e.Commit.RKey) 84 if err != nil { 85 return fmt.Errorf("failed to add follow to db: %w", err) 86 } 87 return db.UpdateLastTimeUs(e.TimeUS) 88 } 89 90 return nil 91 }) 92 if err != nil { 93 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 94 } 95 96 state := &State{ 97 db, 98 auth, 99 enforcer, 100 clock, 101 pgs, 102 resolver, 103 jc, 104 } 105 106 return state, nil 107} 108 109func (s *State) TID() string { 110 return s.tidClock.Next().String() 111} 112 113func (s *State) Login(w http.ResponseWriter, r *http.Request) { 114 ctx := r.Context() 115 116 switch r.Method { 117 case http.MethodGet: 118 err := s.pages.Login(w, pages.LoginParams{}) 119 if err != nil { 120 log.Printf("rendering login page: %s", err) 121 } 122 return 123 case http.MethodPost: 124 handle := strings.TrimPrefix(r.FormValue("handle"), "@") 125 appPassword := r.FormValue("app_password") 126 127 resolved, err := s.resolver.ResolveIdent(ctx, handle) 128 if err != nil { 129 log.Println("failed to resolve handle:", err) 130 s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 131 return 132 } 133 134 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword) 135 if err != nil { 136 s.pages.Notice(w, "login-msg", "Invalid handle or password.") 137 return 138 } 139 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} 140 141 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) 142 if err != nil { 143 s.pages.Notice(w, "login-msg", "Failed to login, try again later.") 144 return 145 } 146 147 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 148 s.pages.HxRedirect(w, "/") 149 return 150 } 151} 152 153func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 154 s.auth.ClearSession(r, w) 155 s.pages.HxRedirect(w, "/") 156} 157 158func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 159 user := s.auth.GetUser(r) 160 161 timeline, err := s.db.MakeTimeline() 162 if err != nil { 163 log.Println(err) 164 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 165 } 166 167 var didsToResolve []string 168 for _, ev := range timeline { 169 if ev.Repo != nil { 170 didsToResolve = append(didsToResolve, ev.Repo.Did) 171 } 172 if ev.Follow != nil { 173 didsToResolve = append(didsToResolve, ev.Follow.UserDid) 174 didsToResolve = append(didsToResolve, ev.Follow.SubjectDid) 175 } 176 } 177 178 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 179 didHandleMap := make(map[string]string) 180 for _, identity := range resolvedIds { 181 if !identity.Handle.IsInvalidHandle() { 182 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 183 } else { 184 didHandleMap[identity.DID.String()] = identity.DID.String() 185 } 186 } 187 188 s.pages.Timeline(w, pages.TimelineParams{ 189 LoggedInUser: user, 190 Timeline: timeline, 191 DidHandleMap: didHandleMap, 192 }) 193 194 return 195} 196 197// requires auth 198func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { 199 switch r.Method { 200 case http.MethodGet: 201 // list open registrations under this did 202 203 return 204 case http.MethodPost: 205 session, err := s.auth.Store.Get(r, appview.SessionName) 206 if err != nil || session.IsNew { 207 log.Println("unauthorized attempt to generate registration key") 208 http.Error(w, "Forbidden", http.StatusUnauthorized) 209 return 210 } 211 212 did := session.Values[appview.SessionDid].(string) 213 214 // check if domain is valid url, and strip extra bits down to just host 215 domain := r.FormValue("domain") 216 if domain == "" { 217 http.Error(w, "Invalid form", http.StatusBadRequest) 218 return 219 } 220 221 key, err := s.db.GenerateRegistrationKey(domain, did) 222 223 if err != nil { 224 log.Println(err) 225 http.Error(w, "unable to register this domain", http.StatusNotAcceptable) 226 return 227 } 228 229 w.Write([]byte(key)) 230 } 231} 232 233func (s *State) Keys(w http.ResponseWriter, r *http.Request) { 234 user := chi.URLParam(r, "user") 235 user = strings.TrimPrefix(user, "@") 236 237 if user == "" { 238 w.WriteHeader(http.StatusBadRequest) 239 return 240 } 241 242 id, err := s.resolver.ResolveIdent(r.Context(), user) 243 if err != nil { 244 w.WriteHeader(http.StatusInternalServerError) 245 return 246 } 247 248 pubKeys, err := s.db.GetPublicKeys(id.DID.String()) 249 if err != nil { 250 w.WriteHeader(http.StatusNotFound) 251 return 252 } 253 254 if len(pubKeys) == 0 { 255 w.WriteHeader(http.StatusNotFound) 256 return 257 } 258 259 for _, k := range pubKeys { 260 key := strings.TrimRight(k.Key, "\n") 261 w.Write([]byte(fmt.Sprintln(key))) 262 } 263} 264 265// create a signed request and check if a node responds to that 266func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 267 user := s.auth.GetUser(r) 268 269 domain := chi.URLParam(r, "domain") 270 if domain == "" { 271 http.Error(w, "malformed url", http.StatusBadRequest) 272 return 273 } 274 log.Println("checking ", domain) 275 276 secret, err := s.db.GetRegistrationKey(domain) 277 if err != nil { 278 log.Printf("no key found for domain %s: %s\n", domain, err) 279 return 280 } 281 282 client, err := NewSignedClient(domain, secret) 283 if err != nil { 284 log.Println("failed to create client to ", domain) 285 } 286 287 resp, err := client.Init(user.Did) 288 if err != nil { 289 w.Write([]byte("no dice")) 290 log.Println("domain was unreachable after 5 seconds") 291 return 292 } 293 294 if resp.StatusCode == http.StatusConflict { 295 log.Println("status conflict", resp.StatusCode) 296 w.Write([]byte("already registered, sorry!")) 297 return 298 } 299 300 if resp.StatusCode != http.StatusNoContent { 301 log.Println("status nok", resp.StatusCode) 302 w.Write([]byte("no dice")) 303 return 304 } 305 306 // verify response mac 307 signature := resp.Header.Get("X-Signature") 308 signatureBytes, err := hex.DecodeString(signature) 309 if err != nil { 310 return 311 } 312 313 expectedMac := hmac.New(sha256.New, []byte(secret)) 314 expectedMac.Write([]byte("ok")) 315 316 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 317 log.Printf("response body signature mismatch: %x\n", signatureBytes) 318 return 319 } 320 321 // mark as registered 322 err = s.db.Register(domain) 323 if err != nil { 324 log.Println("failed to register domain", err) 325 http.Error(w, err.Error(), http.StatusInternalServerError) 326 return 327 } 328 329 // set permissions for this did as owner 330 reg, err := s.db.RegistrationByDomain(domain) 331 if err != nil { 332 log.Println("failed to register domain", err) 333 http.Error(w, err.Error(), http.StatusInternalServerError) 334 return 335 } 336 337 // add basic acls for this domain 338 err = s.enforcer.AddDomain(domain) 339 if err != nil { 340 log.Println("failed to setup owner of domain", err) 341 http.Error(w, err.Error(), http.StatusInternalServerError) 342 return 343 } 344 345 // add this did as owner of this domain 346 err = s.enforcer.AddOwner(domain, reg.ByDid) 347 if err != nil { 348 log.Println("failed to setup owner of domain", err) 349 http.Error(w, err.Error(), http.StatusInternalServerError) 350 return 351 } 352 353 w.Write([]byte("check success")) 354} 355 356func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 357 domain := chi.URLParam(r, "domain") 358 if domain == "" { 359 http.Error(w, "malformed url", http.StatusBadRequest) 360 return 361 } 362 363 user := s.auth.GetUser(r) 364 reg, err := s.db.RegistrationByDomain(domain) 365 if err != nil { 366 w.Write([]byte("failed to pull up registration info")) 367 return 368 } 369 370 var members []string 371 if reg.Registered != nil { 372 members, err = s.enforcer.GetUserByRole("server:member", domain) 373 if err != nil { 374 w.Write([]byte("failed to fetch member list")) 375 return 376 } 377 } 378 379 ok, err := s.enforcer.IsServerOwner(user.Did, domain) 380 isOwner := err == nil && ok 381 382 p := pages.KnotParams{ 383 LoggedInUser: user, 384 Registration: reg, 385 Members: members, 386 IsOwner: isOwner, 387 } 388 389 s.pages.Knot(w, p) 390} 391 392// get knots registered by this user 393func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 394 // for now, this is just pubkeys 395 user := s.auth.GetUser(r) 396 registrations, err := s.db.RegistrationsByDid(user.Did) 397 if err != nil { 398 log.Println(err) 399 } 400 401 s.pages.Knots(w, pages.KnotsParams{ 402 LoggedInUser: user, 403 Registrations: registrations, 404 }) 405} 406 407// list members of domain, requires auth and requires owner status 408func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 409 domain := chi.URLParam(r, "domain") 410 if domain == "" { 411 http.Error(w, "malformed url", http.StatusBadRequest) 412 return 413 } 414 415 // list all members for this domain 416 memberDids, err := s.enforcer.GetUserByRole("server:member", domain) 417 if err != nil { 418 w.Write([]byte("failed to fetch member list")) 419 return 420 } 421 422 w.Write([]byte(strings.Join(memberDids, "\n"))) 423 return 424} 425 426// add member to domain, requires auth and requires invite access 427func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 428 domain := chi.URLParam(r, "domain") 429 if domain == "" { 430 http.Error(w, "malformed url", http.StatusBadRequest) 431 return 432 } 433 434 memberDid := r.FormValue("member") 435 if memberDid == "" { 436 http.Error(w, "malformed form", http.StatusBadRequest) 437 return 438 } 439 440 memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid) 441 if err != nil { 442 w.Write([]byte("failed to resolve member did to a handle")) 443 return 444 } 445 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 446 447 // announce this relation into the firehose, store into owners' pds 448 client, _ := s.auth.AuthorizedClient(r) 449 currentUser := s.auth.GetUser(r) 450 addedAt := time.Now().Format(time.RFC3339) 451 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 452 Collection: tangled.KnotMemberNSID, 453 Repo: currentUser.Did, 454 Rkey: s.TID(), 455 Record: &lexutil.LexiconTypeDecoder{ 456 Val: &tangled.KnotMember{ 457 Member: memberIdent.DID.String(), 458 Domain: domain, 459 AddedAt: &addedAt, 460 }}, 461 }) 462 463 // invalid record 464 if err != nil { 465 log.Printf("failed to create record: %s", err) 466 return 467 } 468 log.Println("created atproto record: ", resp.Uri) 469 470 secret, err := s.db.GetRegistrationKey(domain) 471 if err != nil { 472 log.Printf("no key found for domain %s: %s\n", domain, err) 473 return 474 } 475 476 ksClient, err := NewSignedClient(domain, secret) 477 if err != nil { 478 log.Println("failed to create client to ", domain) 479 return 480 } 481 482 ksResp, err := ksClient.AddMember(memberIdent.DID.String()) 483 if err != nil { 484 log.Printf("failed to make request to %s: %s", domain, err) 485 return 486 } 487 488 if ksResp.StatusCode != http.StatusNoContent { 489 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) 490 return 491 } 492 493 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 494 if err != nil { 495 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 496 return 497 } 498 499 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 500} 501 502func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 503} 504 505func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) { 506 switch r.Method { 507 case http.MethodGet: 508 user := s.auth.GetUser(r) 509 knots, err := s.enforcer.GetDomainsForUser(user.Did) 510 511 if err != nil { 512 s.pages.Notice(w, "repo", "Invalid user account.") 513 return 514 } 515 516 s.pages.NewRepo(w, pages.NewRepoParams{ 517 LoggedInUser: user, 518 Knots: knots, 519 }) 520 case http.MethodPost: 521 user := s.auth.GetUser(r) 522 523 domain := r.FormValue("domain") 524 if domain == "" { 525 s.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.") 526 return 527 } 528 529 repoName := r.FormValue("name") 530 if repoName == "" { 531 s.pages.Notice(w, "repo", "Invalid repo name.") 532 return 533 } 534 535 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 536 if err != nil || !ok { 537 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 538 return 539 } 540 541 secret, err := s.db.GetRegistrationKey(domain) 542 if err != nil { 543 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 544 return 545 } 546 547 client, err := NewSignedClient(domain, secret) 548 if err != nil { 549 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 550 return 551 } 552 553 resp, err := client.NewRepo(user.Did, repoName) 554 if err != nil { 555 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 556 return 557 } 558 559 switch resp.StatusCode { 560 case http.StatusConflict: 561 s.pages.Notice(w, "repo", "A repository with that name already exists.") 562 return 563 case http.StatusInternalServerError: 564 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 565 case http.StatusNoContent: 566 // continue 567 } 568 569 rkey := s.TID() 570 repo := &db.Repo{ 571 Did: user.Did, 572 Name: repoName, 573 Knot: domain, 574 Rkey: rkey, 575 } 576 577 xrpcClient, _ := s.auth.AuthorizedClient(r) 578 579 addedAt := time.Now().Format(time.RFC3339) 580 atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 581 Collection: tangled.RepoNSID, 582 Repo: user.Did, 583 Rkey: rkey, 584 Record: &lexutil.LexiconTypeDecoder{ 585 Val: &tangled.Repo{ 586 Knot: repo.Knot, 587 Name: repoName, 588 AddedAt: &addedAt, 589 Owner: user.Did, 590 }}, 591 }) 592 if err != nil { 593 log.Printf("failed to create record: %s", err) 594 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 595 return 596 } 597 log.Println("created repo record: ", atresp.Uri) 598 599 repo.AtUri = atresp.Uri 600 601 err = s.db.AddRepo(repo) 602 if err != nil { 603 log.Println(err) 604 s.pages.Notice(w, "repo", "Failed to save repository information.") 605 return 606 } 607 608 // acls 609 p, _ := securejoin.SecureJoin(user.Did, repoName) 610 err = s.enforcer.AddRepo(user.Did, domain, p) 611 if err != nil { 612 log.Println(err) 613 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 614 return 615 } 616 617 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 618 return 619 } 620} 621 622func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 623 didOrHandle := chi.URLParam(r, "user") 624 if didOrHandle == "" { 625 http.Error(w, "Bad request", http.StatusBadRequest) 626 return 627 } 628 629 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle) 630 if err != nil { 631 log.Printf("resolving identity: %s", err) 632 w.WriteHeader(http.StatusNotFound) 633 return 634 } 635 636 repos, err := s.db.GetAllReposByDid(ident.DID.String()) 637 if err != nil { 638 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 639 } 640 641 collaboratingRepos, err := s.db.CollaboratingIn(ident.DID.String()) 642 if err != nil { 643 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 644 } 645 var didsToResolve []string 646 for _, r := range collaboratingRepos { 647 didsToResolve = append(didsToResolve, r.Did) 648 } 649 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 650 didHandleMap := make(map[string]string) 651 for _, identity := range resolvedIds { 652 if !identity.Handle.IsInvalidHandle() { 653 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 654 } else { 655 didHandleMap[identity.DID.String()] = identity.DID.String() 656 } 657 } 658 659 followers, following, err := s.db.GetFollowerFollowing(ident.DID.String()) 660 if err != nil { 661 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 662 } 663 664 loggedInUser := s.auth.GetUser(r) 665 followStatus := db.IsNotFollowing 666 if loggedInUser != nil { 667 followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String()) 668 } 669 670 s.pages.ProfilePage(w, pages.ProfilePageParams{ 671 LoggedInUser: loggedInUser, 672 UserDid: ident.DID.String(), 673 UserHandle: ident.Handle.String(), 674 Repos: repos, 675 CollaboratingRepos: collaboratingRepos, 676 ProfileStats: pages.ProfileStats{ 677 Followers: followers, 678 Following: following, 679 }, 680 FollowStatus: db.FollowStatus(followStatus), 681 DidHandleMap: didHandleMap, 682 }) 683} 684 685func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 686 currentUser := s.auth.GetUser(r) 687 688 subject := r.URL.Query().Get("subject") 689 if subject == "" { 690 log.Println("invalid form") 691 return 692 } 693 694 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject) 695 if err != nil { 696 log.Println("failed to follow, invalid did") 697 } 698 699 if currentUser.Did == subjectIdent.DID.String() { 700 log.Println("cant follow or unfollow yourself") 701 return 702 } 703 704 client, _ := s.auth.AuthorizedClient(r) 705 706 switch r.Method { 707 case http.MethodPost: 708 createdAt := time.Now().Format(time.RFC3339) 709 rkey := s.TID() 710 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 711 Collection: tangled.GraphFollowNSID, 712 Repo: currentUser.Did, 713 Rkey: rkey, 714 Record: &lexutil.LexiconTypeDecoder{ 715 Val: &tangled.GraphFollow{ 716 Subject: subjectIdent.DID.String(), 717 CreatedAt: createdAt, 718 }}, 719 }) 720 if err != nil { 721 log.Println("failed to create atproto record", err) 722 return 723 } 724 725 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), rkey) 726 if err != nil { 727 log.Println("failed to follow", err) 728 return 729 } 730 731 log.Println("created atproto record: ", resp.Uri) 732 733 w.Write([]byte(fmt.Sprintf(` 734 <button id="followBtn" 735 class="btn mt-2" 736 hx-delete="/follow?subject=%s" 737 hx-trigger="click" 738 hx-target="#followBtn" 739 hx-swap="outerHTML"> 740 Unfollow 741 </button> 742 `, subjectIdent.DID.String()))) 743 744 return 745 case http.MethodDelete: 746 // find the record in the db 747 follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String()) 748 if err != nil { 749 log.Println("failed to get follow relationship") 750 return 751 } 752 753 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 754 Collection: tangled.GraphFollowNSID, 755 Repo: currentUser.Did, 756 Rkey: follow.RKey, 757 }) 758 759 if err != nil { 760 log.Println("failed to unfollow") 761 return 762 } 763 764 err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String()) 765 if err != nil { 766 log.Println("failed to delete follow from DB") 767 // this is not an issue, the firehose event might have already done this 768 } 769 770 w.Write([]byte(fmt.Sprintf(` 771 <button id="followBtn" 772 class="btn mt-2" 773 hx-post="/follow?subject=%s" 774 hx-trigger="click" 775 hx-target="#followBtn" 776 hx-swap="outerHTML"> 777 Follow 778 </button> 779 `, subjectIdent.DID.String()))) 780 return 781 } 782 783} 784 785func (s *State) Router() http.Handler { 786 router := chi.NewRouter() 787 788 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 789 pat := chi.URLParam(r, "*") 790 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 791 s.UserRouter().ServeHTTP(w, r) 792 } else { 793 s.StandardRouter().ServeHTTP(w, r) 794 } 795 }) 796 797 return router 798} 799 800func (s *State) UserRouter() http.Handler { 801 r := chi.NewRouter() 802 803 // strip @ from user 804 r.Use(StripLeadingAt) 805 806 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 807 r.Get("/", s.ProfilePage) 808 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) { 809 r.Get("/", s.RepoIndex) 810 r.Get("/commits/{ref}", s.RepoLog) 811 r.Route("/tree/{ref}", func(r chi.Router) { 812 r.Get("/", s.RepoIndex) 813 r.Get("/*", s.RepoTree) 814 }) 815 r.Get("/commit/{ref}", s.RepoCommit) 816 r.Get("/branches", s.RepoBranches) 817 r.Get("/tags", s.RepoTags) 818 r.Get("/blob/{ref}/*", s.RepoBlob) 819 820 r.Route("/issues", func(r chi.Router) { 821 r.Get("/", s.RepoIssues) 822 r.Get("/{issue}", s.RepoSingleIssue) 823 r.Get("/new", s.NewIssue) 824 r.Post("/new", s.NewIssue) 825 r.Post("/{issue}/comment", s.IssueComment) 826 r.Post("/{issue}/close", s.CloseIssue) 827 r.Post("/{issue}/reopen", s.ReopenIssue) 828 }) 829 830 r.Route("/pulls", func(r chi.Router) { 831 r.Get("/", s.RepoPulls) 832 }) 833 834 // These routes get proxied to the knot 835 r.Get("/info/refs", s.InfoRefs) 836 r.Post("/git-upload-pack", s.UploadPack) 837 838 // settings routes, needs auth 839 r.Group(func(r chi.Router) { 840 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 841 r.Get("/", s.RepoSettings) 842 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) 843 }) 844 }) 845 }) 846 }) 847 848 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 849 s.pages.Error404(w) 850 }) 851 852 return r 853} 854 855func (s *State) StandardRouter() http.Handler { 856 r := chi.NewRouter() 857 858 r.Handle("/static/*", s.pages.Static()) 859 860 r.Get("/", s.Timeline) 861 862 r.Get("/logout", s.Logout) 863 864 r.Get("/login", s.Login) 865 r.Post("/login", s.Login) 866 867 r.Route("/knots", func(r chi.Router) { 868 r.Use(AuthMiddleware(s)) 869 r.Get("/", s.Knots) 870 r.Post("/key", s.RegistrationKey) 871 872 r.Route("/{domain}", func(r chi.Router) { 873 r.Post("/init", s.InitKnotServer) 874 r.Get("/", s.KnotServerInfo) 875 r.Route("/member", func(r chi.Router) { 876 r.Use(RoleMiddleware(s, "server:owner")) 877 r.Get("/", s.ListMembers) 878 r.Put("/", s.AddMember) 879 r.Delete("/", s.RemoveMember) 880 }) 881 }) 882 }) 883 884 r.Route("/repo", func(r chi.Router) { 885 r.Route("/new", func(r chi.Router) { 886 r.Get("/", s.AddRepo) 887 r.Post("/", s.AddRepo) 888 }) 889 // r.Post("/import", s.ImportRepo) 890 }) 891 892 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { 893 r.Post("/", s.Follow) 894 r.Delete("/", s.Follow) 895 }) 896 897 r.Route("/settings", func(r chi.Router) { 898 r.Use(AuthMiddleware(s)) 899 r.Get("/", s.Settings) 900 r.Put("/keys", s.SettingsKeys) 901 }) 902 903 r.Get("/keys/{user}", s.Keys) 904 905 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 906 s.pages.Error404(w) 907 }) 908 return r 909}