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