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}