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