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}