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