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