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