787 lines
20 kB
1
package state
2
3
import (
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
29
type 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
40
func 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
85
func (s *State) TID() string {
86
return s.tidClock.Next().String()
87
}
88
89
func (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
129
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
130
s.auth.ClearSession(r, w)
131
s.pages.HxRedirect(w, "/")
132
}
133
134
func (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
174
func (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
209
func (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
242
func (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
332
func (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
369
func (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
384
func (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
403
func (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
478
func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
479
}
480
481
func (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
598
func (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
661
func (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
676
func (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
731
func (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
}
788