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