909 lines
23 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
var didsToResolve []string
646
for _, r := range collaboratingRepos {
647
didsToResolve = append(didsToResolve, r.Did)
648
}
649
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
650
didHandleMap := make(map[string]string)
651
for _, identity := range resolvedIds {
652
if !identity.Handle.IsInvalidHandle() {
653
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
654
} else {
655
didHandleMap[identity.DID.String()] = identity.DID.String()
656
}
657
}
658
659
followers, following, err := s.db.GetFollowerFollowing(ident.DID.String())
660
if err != nil {
661
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
662
}
663
664
loggedInUser := s.auth.GetUser(r)
665
followStatus := db.IsNotFollowing
666
if loggedInUser != nil {
667
followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String())
668
}
669
670
s.pages.ProfilePage(w, pages.ProfilePageParams{
671
LoggedInUser: loggedInUser,
672
UserDid: ident.DID.String(),
673
UserHandle: ident.Handle.String(),
674
Repos: repos,
675
CollaboratingRepos: collaboratingRepos,
676
ProfileStats: pages.ProfileStats{
677
Followers: followers,
678
Following: following,
679
},
680
FollowStatus: db.FollowStatus(followStatus),
681
DidHandleMap: didHandleMap,
682
})
683
}
684
685
func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
686
currentUser := s.auth.GetUser(r)
687
688
subject := r.URL.Query().Get("subject")
689
if subject == "" {
690
log.Println("invalid form")
691
return
692
}
693
694
subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
695
if err != nil {
696
log.Println("failed to follow, invalid did")
697
}
698
699
if currentUser.Did == subjectIdent.DID.String() {
700
log.Println("cant follow or unfollow yourself")
701
return
702
}
703
704
client, _ := s.auth.AuthorizedClient(r)
705
706
switch r.Method {
707
case http.MethodPost:
708
createdAt := time.Now().Format(time.RFC3339)
709
rkey := s.TID()
710
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
711
Collection: tangled.GraphFollowNSID,
712
Repo: currentUser.Did,
713
Rkey: rkey,
714
Record: &lexutil.LexiconTypeDecoder{
715
Val: &tangled.GraphFollow{
716
Subject: subjectIdent.DID.String(),
717
CreatedAt: createdAt,
718
}},
719
})
720
if err != nil {
721
log.Println("failed to create atproto record", err)
722
return
723
}
724
725
err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), rkey)
726
if err != nil {
727
log.Println("failed to follow", err)
728
return
729
}
730
731
log.Println("created atproto record: ", resp.Uri)
732
733
w.Write([]byte(fmt.Sprintf(`
734
<button id="followBtn"
735
class="btn mt-2"
736
hx-delete="/follow?subject=%s"
737
hx-trigger="click"
738
hx-target="#followBtn"
739
hx-swap="outerHTML">
740
Unfollow
741
</button>
742
`, subjectIdent.DID.String())))
743
744
return
745
case http.MethodDelete:
746
// find the record in the db
747
follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())
748
if err != nil {
749
log.Println("failed to get follow relationship")
750
return
751
}
752
753
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
754
Collection: tangled.GraphFollowNSID,
755
Repo: currentUser.Did,
756
Rkey: follow.RKey,
757
})
758
759
if err != nil {
760
log.Println("failed to unfollow")
761
return
762
}
763
764
err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String())
765
if err != nil {
766
log.Println("failed to delete follow from DB")
767
// this is not an issue, the firehose event might have already done this
768
}
769
770
w.Write([]byte(fmt.Sprintf(`
771
<button id="followBtn"
772
class="btn mt-2"
773
hx-post="/follow?subject=%s"
774
hx-trigger="click"
775
hx-target="#followBtn"
776
hx-swap="outerHTML">
777
Follow
778
</button>
779
`, subjectIdent.DID.String())))
780
return
781
}
782
783
}
784
785
func (s *State) Router() http.Handler {
786
router := chi.NewRouter()
787
788
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
789
pat := chi.URLParam(r, "*")
790
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
791
s.UserRouter().ServeHTTP(w, r)
792
} else {
793
s.StandardRouter().ServeHTTP(w, r)
794
}
795
})
796
797
return router
798
}
799
800
func (s *State) UserRouter() http.Handler {
801
r := chi.NewRouter()
802
803
// strip @ from user
804
r.Use(StripLeadingAt)
805
806
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
807
r.Get("/", s.ProfilePage)
808
r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
809
r.Get("/", s.RepoIndex)
810
r.Get("/commits/{ref}", s.RepoLog)
811
r.Route("/tree/{ref}", func(r chi.Router) {
812
r.Get("/", s.RepoIndex)
813
r.Get("/*", s.RepoTree)
814
})
815
r.Get("/commit/{ref}", s.RepoCommit)
816
r.Get("/branches", s.RepoBranches)
817
r.Get("/tags", s.RepoTags)
818
r.Get("/blob/{ref}/*", s.RepoBlob)
819
820
r.Route("/issues", func(r chi.Router) {
821
r.Get("/", s.RepoIssues)
822
r.Get("/{issue}", s.RepoSingleIssue)
823
r.Get("/new", s.NewIssue)
824
r.Post("/new", s.NewIssue)
825
r.Post("/{issue}/comment", s.IssueComment)
826
r.Post("/{issue}/close", s.CloseIssue)
827
r.Post("/{issue}/reopen", s.ReopenIssue)
828
})
829
830
r.Route("/pulls", func(r chi.Router) {
831
r.Get("/", s.RepoPulls)
832
})
833
834
// These routes get proxied to the knot
835
r.Get("/info/refs", s.InfoRefs)
836
r.Post("/git-upload-pack", s.UploadPack)
837
838
// settings routes, needs auth
839
r.Group(func(r chi.Router) {
840
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
841
r.Get("/", s.RepoSettings)
842
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
843
})
844
})
845
})
846
})
847
848
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
849
s.pages.Error404(w)
850
})
851
852
return r
853
}
854
855
func (s *State) StandardRouter() http.Handler {
856
r := chi.NewRouter()
857
858
r.Handle("/static/*", s.pages.Static())
859
860
r.Get("/", s.Timeline)
861
862
r.Get("/logout", s.Logout)
863
864
r.Get("/login", s.Login)
865
r.Post("/login", s.Login)
866
867
r.Route("/knots", func(r chi.Router) {
868
r.Use(AuthMiddleware(s))
869
r.Get("/", s.Knots)
870
r.Post("/key", s.RegistrationKey)
871
872
r.Route("/{domain}", func(r chi.Router) {
873
r.Post("/init", s.InitKnotServer)
874
r.Get("/", s.KnotServerInfo)
875
r.Route("/member", func(r chi.Router) {
876
r.Use(RoleMiddleware(s, "server:owner"))
877
r.Get("/", s.ListMembers)
878
r.Put("/", s.AddMember)
879
r.Delete("/", s.RemoveMember)
880
})
881
})
882
})
883
884
r.Route("/repo", func(r chi.Router) {
885
r.Route("/new", func(r chi.Router) {
886
r.Get("/", s.AddRepo)
887
r.Post("/", s.AddRepo)
888
})
889
// r.Post("/import", s.ImportRepo)
890
})
891
892
r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
893
r.Post("/", s.Follow)
894
r.Delete("/", s.Follow)
895
})
896
897
r.Route("/settings", func(r chi.Router) {
898
r.Use(AuthMiddleware(s))
899
r.Get("/", s.Settings)
900
r.Put("/keys", s.SettingsKeys)
901
})
902
903
r.Get("/keys/{user}", s.Keys)
904
905
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
906
s.pages.Error404(w)
907
})
908
return r
909
}
910