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