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