1package state
2
3import (
4 "context"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "fmt"
9 "log"
10 "log/slog"
11 "net/http"
12 "strings"
13 "time"
14
15 comatproto "github.com/bluesky-social/indigo/api/atproto"
16 "github.com/bluesky-social/indigo/atproto/syntax"
17 lexutil "github.com/bluesky-social/indigo/lex/util"
18 securejoin "github.com/cyphar/filepath-securejoin"
19 "github.com/go-chi/chi/v5"
20 tangled "github.com/sotangled/tangled/api/tangled"
21 "github.com/sotangled/tangled/appview"
22 "github.com/sotangled/tangled/appview/auth"
23 "github.com/sotangled/tangled/appview/db"
24 "github.com/sotangled/tangled/appview/pages"
25 "github.com/sotangled/tangled/jetstream"
26 "github.com/sotangled/tangled/rbac"
27)
28
29type State struct {
30 db *db.DB
31 auth *auth.Auth
32 enforcer *rbac.Enforcer
33 tidClock *syntax.TIDClock
34 pages *pages.Pages
35 resolver *appview.Resolver
36 jc *jetstream.JetstreamClient
37 config *appview.Config
38}
39
40func Make(config *appview.Config) (*State, error) {
41 db, err := db.Make(config.DbPath)
42 if err != nil {
43 return nil, err
44 }
45
46 auth, err := auth.Make(config.CookieSecret)
47 if err != nil {
48 return nil, err
49 }
50
51 enforcer, err := rbac.NewEnforcer(config.DbPath)
52 if err != nil {
53 return nil, err
54 }
55
56 clock := syntax.NewTIDClock(0)
57
58 pgs := pages.NewPages()
59
60 resolver := appview.NewResolver()
61
62 jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, slog.Default(), db, false)
63 if err != nil {
64 return nil, fmt.Errorf("failed to create jetstream client: %w", err)
65 }
66 err = jc.StartJetstream(context.Background(), jetstreamIngester(db))
67 if err != nil {
68 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
69 }
70
71 state := &State{
72 db,
73 auth,
74 enforcer,
75 clock,
76 pgs,
77 resolver,
78 jc,
79 config,
80 }
81
82 return state, nil
83}
84
85func (s *State) TID() string {
86 return s.tidClock.Next().String()
87}
88
89func (s *State) Login(w http.ResponseWriter, r *http.Request) {
90 ctx := r.Context()
91
92 switch r.Method {
93 case http.MethodGet:
94 err := s.pages.Login(w, pages.LoginParams{})
95 if err != nil {
96 log.Printf("rendering login page: %s", err)
97 }
98 return
99 case http.MethodPost:
100 handle := strings.TrimPrefix(r.FormValue("handle"), "@")
101 appPassword := r.FormValue("app_password")
102
103 resolved, err := s.resolver.ResolveIdent(ctx, handle)
104 if err != nil {
105 log.Println("failed to resolve handle:", err)
106 s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
107 return
108 }
109
110 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword)
111 if err != nil {
112 s.pages.Notice(w, "login-msg", "Invalid handle or password.")
113 return
114 }
115 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
116
117 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
118 if err != nil {
119 s.pages.Notice(w, "login-msg", "Failed to login, try again later.")
120 return
121 }
122
123 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
124 s.pages.HxRedirect(w, "/")
125 return
126 }
127}
128
129func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
130 s.auth.ClearSession(r, w)
131 s.pages.HxRedirect(w, "/")
132}
133
134func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
135 user := s.auth.GetUser(r)
136
137 timeline, err := s.db.MakeTimeline()
138 if err != nil {
139 log.Println(err)
140 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
141 }
142
143 var didsToResolve []string
144 for _, ev := range timeline {
145 if ev.Repo != nil {
146 didsToResolve = append(didsToResolve, ev.Repo.Did)
147 }
148 if ev.Follow != nil {
149 didsToResolve = append(didsToResolve, ev.Follow.UserDid)
150 didsToResolve = append(didsToResolve, ev.Follow.SubjectDid)
151 }
152 }
153
154 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
155 didHandleMap := make(map[string]string)
156 for _, identity := range resolvedIds {
157 if !identity.Handle.IsInvalidHandle() {
158 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
159 } else {
160 didHandleMap[identity.DID.String()] = identity.DID.String()
161 }
162 }
163
164 s.pages.Timeline(w, pages.TimelineParams{
165 LoggedInUser: user,
166 Timeline: timeline,
167 DidHandleMap: didHandleMap,
168 })
169
170 return
171}
172
173// requires auth
174func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
175 switch r.Method {
176 case http.MethodGet:
177 // list open registrations under this did
178
179 return
180 case http.MethodPost:
181 session, err := s.auth.Store.Get(r, appview.SessionName)
182 if err != nil || session.IsNew {
183 log.Println("unauthorized attempt to generate registration key")
184 http.Error(w, "Forbidden", http.StatusUnauthorized)
185 return
186 }
187
188 did := session.Values[appview.SessionDid].(string)
189
190 // check if domain is valid url, and strip extra bits down to just host
191 domain := r.FormValue("domain")
192 if domain == "" {
193 http.Error(w, "Invalid form", http.StatusBadRequest)
194 return
195 }
196
197 key, err := s.db.GenerateRegistrationKey(domain, did)
198
199 if err != nil {
200 log.Println(err)
201 http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
202 return
203 }
204
205 w.Write([]byte(key))
206 }
207}
208
209func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
210 user := chi.URLParam(r, "user")
211 user = strings.TrimPrefix(user, "@")
212
213 if user == "" {
214 w.WriteHeader(http.StatusBadRequest)
215 return
216 }
217
218 id, err := s.resolver.ResolveIdent(r.Context(), user)
219 if err != nil {
220 w.WriteHeader(http.StatusInternalServerError)
221 return
222 }
223
224 pubKeys, err := s.db.GetPublicKeys(id.DID.String())
225 if err != nil {
226 w.WriteHeader(http.StatusNotFound)
227 return
228 }
229
230 if len(pubKeys) == 0 {
231 w.WriteHeader(http.StatusNotFound)
232 return
233 }
234
235 for _, k := range pubKeys {
236 key := strings.TrimRight(k.Key, "\n")
237 w.Write([]byte(fmt.Sprintln(key)))
238 }
239}
240
241// create a signed request and check if a node responds to that
242func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
243 user := s.auth.GetUser(r)
244
245 domain := chi.URLParam(r, "domain")
246 if domain == "" {
247 http.Error(w, "malformed url", http.StatusBadRequest)
248 return
249 }
250 log.Println("checking ", domain)
251
252 secret, err := s.db.GetRegistrationKey(domain)
253 if err != nil {
254 log.Printf("no key found for domain %s: %s\n", domain, err)
255 return
256 }
257
258 client, err := NewSignedClient(domain, secret)
259 if err != nil {
260 log.Println("failed to create client to ", domain)
261 }
262
263 resp, err := client.Init(user.Did)
264 if err != nil {
265 w.Write([]byte("no dice"))
266 log.Println("domain was unreachable after 5 seconds")
267 return
268 }
269
270 if resp.StatusCode == http.StatusConflict {
271 log.Println("status conflict", resp.StatusCode)
272 w.Write([]byte("already registered, sorry!"))
273 return
274 }
275
276 if resp.StatusCode != http.StatusNoContent {
277 log.Println("status nok", resp.StatusCode)
278 w.Write([]byte("no dice"))
279 return
280 }
281
282 // verify response mac
283 signature := resp.Header.Get("X-Signature")
284 signatureBytes, err := hex.DecodeString(signature)
285 if err != nil {
286 return
287 }
288
289 expectedMac := hmac.New(sha256.New, []byte(secret))
290 expectedMac.Write([]byte("ok"))
291
292 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
293 log.Printf("response body signature mismatch: %x\n", signatureBytes)
294 return
295 }
296
297 // mark as registered
298 err = s.db.Register(domain)
299 if err != nil {
300 log.Println("failed to register domain", err)
301 http.Error(w, err.Error(), http.StatusInternalServerError)
302 return
303 }
304
305 // set permissions for this did as owner
306 reg, err := s.db.RegistrationByDomain(domain)
307 if err != nil {
308 log.Println("failed to register domain", err)
309 http.Error(w, err.Error(), http.StatusInternalServerError)
310 return
311 }
312
313 // add basic acls for this domain
314 err = s.enforcer.AddDomain(domain)
315 if err != nil {
316 log.Println("failed to setup owner of domain", err)
317 http.Error(w, err.Error(), http.StatusInternalServerError)
318 return
319 }
320
321 // add this did as owner of this domain
322 err = s.enforcer.AddOwner(domain, reg.ByDid)
323 if err != nil {
324 log.Println("failed to setup owner of domain", err)
325 http.Error(w, err.Error(), http.StatusInternalServerError)
326 return
327 }
328
329 w.Write([]byte("check success"))
330}
331
332func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
333 domain := chi.URLParam(r, "domain")
334 if domain == "" {
335 http.Error(w, "malformed url", http.StatusBadRequest)
336 return
337 }
338
339 user := s.auth.GetUser(r)
340 reg, err := s.db.RegistrationByDomain(domain)
341 if err != nil {
342 w.Write([]byte("failed to pull up registration info"))
343 return
344 }
345
346 var members []string
347 if reg.Registered != nil {
348 members, err = s.enforcer.GetUserByRole("server:member", domain)
349 if err != nil {
350 w.Write([]byte("failed to fetch member list"))
351 return
352 }
353 }
354
355 ok, err := s.enforcer.IsServerOwner(user.Did, domain)
356 isOwner := err == nil && ok
357
358 p := pages.KnotParams{
359 LoggedInUser: user,
360 Registration: reg,
361 Members: members,
362 IsOwner: isOwner,
363 }
364
365 s.pages.Knot(w, p)
366}
367
368// get knots registered by this user
369func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
370 // for now, this is just pubkeys
371 user := s.auth.GetUser(r)
372 registrations, err := s.db.RegistrationsByDid(user.Did)
373 if err != nil {
374 log.Println(err)
375 }
376
377 s.pages.Knots(w, pages.KnotsParams{
378 LoggedInUser: user,
379 Registrations: registrations,
380 })
381}
382
383// list members of domain, requires auth and requires owner status
384func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
385 domain := chi.URLParam(r, "domain")
386 if domain == "" {
387 http.Error(w, "malformed url", http.StatusBadRequest)
388 return
389 }
390
391 // list all members for this domain
392 memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
393 if err != nil {
394 w.Write([]byte("failed to fetch member list"))
395 return
396 }
397
398 w.Write([]byte(strings.Join(memberDids, "\n")))
399 return
400}
401
402// add member to domain, requires auth and requires invite access
403func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
404 domain := chi.URLParam(r, "domain")
405 if domain == "" {
406 http.Error(w, "malformed url", http.StatusBadRequest)
407 return
408 }
409
410 memberDid := r.FormValue("member")
411 if memberDid == "" {
412 http.Error(w, "malformed form", http.StatusBadRequest)
413 return
414 }
415
416 memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid)
417 if err != nil {
418 w.Write([]byte("failed to resolve member did to a handle"))
419 return
420 }
421 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain)
422
423 // announce this relation into the firehose, store into owners' pds
424 client, _ := s.auth.AuthorizedClient(r)
425 currentUser := s.auth.GetUser(r)
426 addedAt := time.Now().Format(time.RFC3339)
427 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
428 Collection: tangled.KnotMemberNSID,
429 Repo: currentUser.Did,
430 Rkey: s.TID(),
431 Record: &lexutil.LexiconTypeDecoder{
432 Val: &tangled.KnotMember{
433 Member: memberIdent.DID.String(),
434 Domain: domain,
435 AddedAt: &addedAt,
436 }},
437 })
438
439 // invalid record
440 if err != nil {
441 log.Printf("failed to create record: %s", err)
442 return
443 }
444 log.Println("created atproto record: ", resp.Uri)
445
446 secret, err := s.db.GetRegistrationKey(domain)
447 if err != nil {
448 log.Printf("no key found for domain %s: %s\n", domain, err)
449 return
450 }
451
452 ksClient, err := NewSignedClient(domain, secret)
453 if err != nil {
454 log.Println("failed to create client to ", domain)
455 return
456 }
457
458 ksResp, err := ksClient.AddMember(memberIdent.DID.String())
459 if err != nil {
460 log.Printf("failed to make request to %s: %s", domain, err)
461 return
462 }
463
464 if ksResp.StatusCode != http.StatusNoContent {
465 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
466 return
467 }
468
469 err = s.enforcer.AddMember(domain, memberIdent.DID.String())
470 if err != nil {
471 w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
472 return
473 }
474
475 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String())))
476}
477
478func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
479}
480
481func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) {
482 switch r.Method {
483 case http.MethodGet:
484 user := s.auth.GetUser(r)
485 knots, err := s.enforcer.GetDomainsForUser(user.Did)
486
487 if err != nil {
488 s.pages.Notice(w, "repo", "Invalid user account.")
489 return
490 }
491
492 s.pages.NewRepo(w, pages.NewRepoParams{
493 LoggedInUser: user,
494 Knots: knots,
495 })
496 case http.MethodPost:
497 user := s.auth.GetUser(r)
498
499 domain := r.FormValue("domain")
500 if domain == "" {
501 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
502 return
503 }
504
505 repoName := r.FormValue("name")
506 if repoName == "" {
507 s.pages.Notice(w, "repo", "Invalid repo name.")
508 return
509 }
510
511 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
512 if err != nil || !ok {
513 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
514 return
515 }
516
517 secret, err := s.db.GetRegistrationKey(domain)
518 if err != nil {
519 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
520 return
521 }
522
523 client, err := NewSignedClient(domain, secret)
524 if err != nil {
525 s.pages.Notice(w, "repo", "Failed to connect to knot server.")
526 return
527 }
528
529 resp, err := client.NewRepo(user.Did, repoName)
530 if err != nil {
531 s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
532 return
533 }
534
535 switch resp.StatusCode {
536 case http.StatusConflict:
537 s.pages.Notice(w, "repo", "A repository with that name already exists.")
538 return
539 case http.StatusInternalServerError:
540 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
541 case http.StatusNoContent:
542 // continue
543 }
544
545 rkey := s.TID()
546 repo := &db.Repo{
547 Did: user.Did,
548 Name: repoName,
549 Knot: domain,
550 Rkey: rkey,
551 }
552
553 xrpcClient, _ := s.auth.AuthorizedClient(r)
554
555 addedAt := time.Now().Format(time.RFC3339)
556 atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
557 Collection: tangled.RepoNSID,
558 Repo: user.Did,
559 Rkey: rkey,
560 Record: &lexutil.LexiconTypeDecoder{
561 Val: &tangled.Repo{
562 Knot: repo.Knot,
563 Name: repoName,
564 AddedAt: &addedAt,
565 Owner: user.Did,
566 }},
567 })
568 if err != nil {
569 log.Printf("failed to create record: %s", err)
570 s.pages.Notice(w, "repo", "Failed to announce repository creation.")
571 return
572 }
573 log.Println("created repo record: ", atresp.Uri)
574
575 repo.AtUri = 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 var didsToResolve []string
622 for _, r := range collaboratingRepos {
623 didsToResolve = append(didsToResolve, r.Did)
624 }
625 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
626 didHandleMap := make(map[string]string)
627 for _, identity := range resolvedIds {
628 if !identity.Handle.IsInvalidHandle() {
629 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
630 } else {
631 didHandleMap[identity.DID.String()] = identity.DID.String()
632 }
633 }
634
635 followers, following, err := s.db.GetFollowerFollowing(ident.DID.String())
636 if err != nil {
637 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
638 }
639
640 loggedInUser := s.auth.GetUser(r)
641 followStatus := db.IsNotFollowing
642 if loggedInUser != nil {
643 followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String())
644 }
645
646 s.pages.ProfilePage(w, pages.ProfilePageParams{
647 LoggedInUser: loggedInUser,
648 UserDid: ident.DID.String(),
649 UserHandle: ident.Handle.String(),
650 Repos: repos,
651 CollaboratingRepos: collaboratingRepos,
652 ProfileStats: pages.ProfileStats{
653 Followers: followers,
654 Following: following,
655 },
656 FollowStatus: db.FollowStatus(followStatus),
657 DidHandleMap: didHandleMap,
658 })
659}
660
661func (s *State) Router() http.Handler {
662 router := chi.NewRouter()
663
664 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
665 pat := chi.URLParam(r, "*")
666 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
667 s.UserRouter().ServeHTTP(w, r)
668 } else {
669 s.StandardRouter().ServeHTTP(w, r)
670 }
671 })
672
673 return router
674}
675
676func (s *State) UserRouter() http.Handler {
677 r := chi.NewRouter()
678
679 // strip @ from user
680 r.Use(StripLeadingAt)
681
682 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
683 r.Get("/", s.ProfilePage)
684 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
685 r.Get("/", s.RepoIndex)
686 r.Get("/commits/{ref}", s.RepoLog)
687 r.Route("/tree/{ref}", func(r chi.Router) {
688 r.Get("/", s.RepoIndex)
689 r.Get("/*", s.RepoTree)
690 })
691 r.Get("/commit/{ref}", s.RepoCommit)
692 r.Get("/branches", s.RepoBranches)
693 r.Get("/tags", s.RepoTags)
694 r.Get("/blob/{ref}/*", s.RepoBlob)
695
696 r.Route("/issues", func(r chi.Router) {
697 r.Get("/", s.RepoIssues)
698 r.Get("/{issue}", s.RepoSingleIssue)
699 r.Get("/new", s.NewIssue)
700 r.Post("/new", s.NewIssue)
701 r.Post("/{issue}/comment", s.IssueComment)
702 r.Post("/{issue}/close", s.CloseIssue)
703 r.Post("/{issue}/reopen", s.ReopenIssue)
704 })
705
706 r.Route("/pulls", func(r chi.Router) {
707 r.Get("/", s.RepoPulls)
708 })
709
710 // These routes get proxied to the knot
711 r.Get("/info/refs", s.InfoRefs)
712 r.Post("/git-upload-pack", s.UploadPack)
713
714 // settings routes, needs auth
715 r.Group(func(r chi.Router) {
716 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
717 r.Get("/", s.RepoSettings)
718 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
719 })
720 })
721 })
722 })
723
724 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
725 s.pages.Error404(w)
726 })
727
728 return r
729}
730
731func (s *State) StandardRouter() http.Handler {
732 r := chi.NewRouter()
733
734 r.Handle("/static/*", s.pages.Static())
735
736 r.Get("/", s.Timeline)
737
738 r.Get("/logout", s.Logout)
739
740 r.Route("/login", func(r chi.Router) {
741 r.Get("/", s.Login)
742 r.Post("/", s.Login)
743 })
744
745 r.Route("/knots", func(r chi.Router) {
746 r.Use(AuthMiddleware(s))
747 r.Get("/", s.Knots)
748 r.Post("/key", s.RegistrationKey)
749
750 r.Route("/{domain}", func(r chi.Router) {
751 r.Post("/init", s.InitKnotServer)
752 r.Get("/", s.KnotServerInfo)
753 r.Route("/member", func(r chi.Router) {
754 r.Use(RoleMiddleware(s, "server:owner"))
755 r.Get("/", s.ListMembers)
756 r.Put("/", s.AddMember)
757 r.Delete("/", s.RemoveMember)
758 })
759 })
760 })
761
762 r.Route("/repo", func(r chi.Router) {
763 r.Route("/new", func(r chi.Router) {
764 r.Get("/", s.AddRepo)
765 r.Post("/", s.AddRepo)
766 })
767 // r.Post("/import", s.ImportRepo)
768 })
769
770 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
771 r.Post("/", s.Follow)
772 r.Delete("/", s.Follow)
773 })
774
775 r.Route("/settings", func(r chi.Router) {
776 r.Use(AuthMiddleware(s))
777 r.Get("/", s.Settings)
778 r.Put("/keys", s.SettingsKeys)
779 })
780
781 r.Get("/keys/{user}", s.Keys)
782
783 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
784 s.pages.Error404(w)
785 })
786 return r
787}