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