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