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