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