792 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
defaultBranch := r.FormValue("branch")
512
if defaultBranch == "" {
513
defaultBranch = "main"
514
}
515
516
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
517
if err != nil || !ok {
518
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
519
return
520
}
521
522
secret, err := s.db.GetRegistrationKey(domain)
523
if err != nil {
524
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
525
return
526
}
527
528
client, err := NewSignedClient(domain, secret)
529
if err != nil {
530
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
531
return
532
}
533
534
resp, err := client.NewRepo(user.Did, repoName, defaultBranch)
535
if err != nil {
536
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
537
return
538
}
539
540
switch resp.StatusCode {
541
case http.StatusConflict:
542
s.pages.Notice(w, "repo", "A repository with that name already exists.")
543
return
544
case http.StatusInternalServerError:
545
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
546
case http.StatusNoContent:
547
// continue
548
}
549
550
rkey := s.TID()
551
repo := &db.Repo{
552
Did: user.Did,
553
Name: repoName,
554
Knot: domain,
555
Rkey: rkey,
556
}
557
558
xrpcClient, _ := s.auth.AuthorizedClient(r)
559
560
addedAt := time.Now().Format(time.RFC3339)
561
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
562
Collection: tangled.RepoNSID,
563
Repo: user.Did,
564
Rkey: rkey,
565
Record: &lexutil.LexiconTypeDecoder{
566
Val: &tangled.Repo{
567
Knot: repo.Knot,
568
Name: repoName,
569
AddedAt: &addedAt,
570
Owner: user.Did,
571
}},
572
})
573
if err != nil {
574
log.Printf("failed to create record: %s", err)
575
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
576
return
577
}
578
log.Println("created repo record: ", atresp.Uri)
579
580
repo.AtUri = atresp.Uri
581
582
err = s.db.AddRepo(repo)
583
if err != nil {
584
log.Println(err)
585
s.pages.Notice(w, "repo", "Failed to save repository information.")
586
return
587
}
588
589
// acls
590
p, _ := securejoin.SecureJoin(user.Did, repoName)
591
err = s.enforcer.AddRepo(user.Did, domain, p)
592
if err != nil {
593
log.Println(err)
594
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
595
return
596
}
597
598
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName))
599
return
600
}
601
}
602
603
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
604
didOrHandle := chi.URLParam(r, "user")
605
if didOrHandle == "" {
606
http.Error(w, "Bad request", http.StatusBadRequest)
607
return
608
}
609
610
ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
611
if err != nil {
612
log.Printf("resolving identity: %s", err)
613
w.WriteHeader(http.StatusNotFound)
614
return
615
}
616
617
repos, err := s.db.GetAllReposByDid(ident.DID.String())
618
if err != nil {
619
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
620
}
621
622
collaboratingRepos, err := s.db.CollaboratingIn(ident.DID.String())
623
if err != nil {
624
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
625
}
626
var didsToResolve []string
627
for _, r := range collaboratingRepos {
628
didsToResolve = append(didsToResolve, r.Did)
629
}
630
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
631
didHandleMap := make(map[string]string)
632
for _, identity := range resolvedIds {
633
if !identity.Handle.IsInvalidHandle() {
634
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
635
} else {
636
didHandleMap[identity.DID.String()] = identity.DID.String()
637
}
638
}
639
640
followers, following, err := s.db.GetFollowerFollowing(ident.DID.String())
641
if err != nil {
642
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
643
}
644
645
loggedInUser := s.auth.GetUser(r)
646
followStatus := db.IsNotFollowing
647
if loggedInUser != nil {
648
followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String())
649
}
650
651
s.pages.ProfilePage(w, pages.ProfilePageParams{
652
LoggedInUser: loggedInUser,
653
UserDid: ident.DID.String(),
654
UserHandle: ident.Handle.String(),
655
Repos: repos,
656
CollaboratingRepos: collaboratingRepos,
657
ProfileStats: pages.ProfileStats{
658
Followers: followers,
659
Following: following,
660
},
661
FollowStatus: db.FollowStatus(followStatus),
662
DidHandleMap: didHandleMap,
663
})
664
}
665
666
func (s *State) Router() http.Handler {
667
router := chi.NewRouter()
668
669
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
670
pat := chi.URLParam(r, "*")
671
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
672
s.UserRouter().ServeHTTP(w, r)
673
} else {
674
s.StandardRouter().ServeHTTP(w, r)
675
}
676
})
677
678
return router
679
}
680
681
func (s *State) UserRouter() http.Handler {
682
r := chi.NewRouter()
683
684
// strip @ from user
685
r.Use(StripLeadingAt)
686
687
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
688
r.Get("/", s.ProfilePage)
689
r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
690
r.Get("/", s.RepoIndex)
691
r.Get("/commits/{ref}", s.RepoLog)
692
r.Route("/tree/{ref}", func(r chi.Router) {
693
r.Get("/", s.RepoIndex)
694
r.Get("/*", s.RepoTree)
695
})
696
r.Get("/commit/{ref}", s.RepoCommit)
697
r.Get("/branches", s.RepoBranches)
698
r.Get("/tags", s.RepoTags)
699
r.Get("/blob/{ref}/*", s.RepoBlob)
700
701
r.Route("/issues", func(r chi.Router) {
702
r.Get("/", s.RepoIssues)
703
r.Get("/{issue}", s.RepoSingleIssue)
704
r.Get("/new", s.NewIssue)
705
r.Post("/new", s.NewIssue)
706
r.Post("/{issue}/comment", s.IssueComment)
707
r.Post("/{issue}/close", s.CloseIssue)
708
r.Post("/{issue}/reopen", s.ReopenIssue)
709
})
710
711
r.Route("/pulls", func(r chi.Router) {
712
r.Get("/", s.RepoPulls)
713
})
714
715
// These routes get proxied to the knot
716
r.Get("/info/refs", s.InfoRefs)
717
r.Post("/git-upload-pack", s.UploadPack)
718
719
// settings routes, needs auth
720
r.Group(func(r chi.Router) {
721
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
722
r.Get("/", s.RepoSettings)
723
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
724
})
725
})
726
})
727
})
728
729
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
730
s.pages.Error404(w)
731
})
732
733
return r
734
}
735
736
func (s *State) StandardRouter() http.Handler {
737
r := chi.NewRouter()
738
739
r.Handle("/static/*", s.pages.Static())
740
741
r.Get("/", s.Timeline)
742
743
r.Get("/logout", s.Logout)
744
745
r.Route("/login", func(r chi.Router) {
746
r.Get("/", s.Login)
747
r.Post("/", s.Login)
748
})
749
750
r.Route("/knots", func(r chi.Router) {
751
r.Use(AuthMiddleware(s))
752
r.Get("/", s.Knots)
753
r.Post("/key", s.RegistrationKey)
754
755
r.Route("/{domain}", func(r chi.Router) {
756
r.Post("/init", s.InitKnotServer)
757
r.Get("/", s.KnotServerInfo)
758
r.Route("/member", func(r chi.Router) {
759
r.Use(RoleMiddleware(s, "server:owner"))
760
r.Get("/", s.ListMembers)
761
r.Put("/", s.AddMember)
762
r.Delete("/", s.RemoveMember)
763
})
764
})
765
})
766
767
r.Route("/repo", func(r chi.Router) {
768
r.Route("/new", func(r chi.Router) {
769
r.Get("/", s.AddRepo)
770
r.Post("/", s.AddRepo)
771
})
772
// r.Post("/import", s.ImportRepo)
773
})
774
775
r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
776
r.Post("/", s.Follow)
777
r.Delete("/", s.Follow)
778
})
779
780
r.Route("/settings", func(r chi.Router) {
781
r.Use(AuthMiddleware(s))
782
r.Get("/", s.Settings)
783
r.Put("/keys", s.SettingsKeys)
784
})
785
786
r.Get("/keys/{user}", s.Keys)
787
788
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
789
s.pages.Error404(w)
790
})
791
return r
792
}
793