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