+1
-1
.air/appview.toml
+1
-1
.air/appview.toml
+4
-1
.gitignore
+4
-1
.gitignore
+31
api/tangled/actorprofile.go
+31
api/tangled/actorprofile.go
···
1
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
+
3
+
package tangled
4
+
5
+
// schema: sh.tangled.actor.profile
6
+
7
+
import (
8
+
"github.com/bluesky-social/indigo/lex/util"
9
+
)
10
+
11
+
const (
12
+
ActorProfileNSID = "sh.tangled.actor.profile"
13
+
)
14
+
15
+
func init() {
16
+
util.RegisterType("sh.tangled.actor.profile", &ActorProfile{})
17
+
} //
18
+
// RECORDTYPE: ActorProfile
19
+
type ActorProfile struct {
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.actor.profile" cborgen:"$type,const=sh.tangled.actor.profile"`
21
+
// bluesky: Include link to this account on Bluesky.
22
+
Bluesky bool `json:"bluesky" cborgen:"bluesky"`
23
+
// description: Free-form profile description text.
24
+
Description *string `json:"description,omitempty" cborgen:"description,omitempty"`
25
+
Links []string `json:"links,omitempty" cborgen:"links,omitempty"`
26
+
// location: Free-form location text.
27
+
Location *string `json:"location,omitempty" cborgen:"location,omitempty"`
28
+
// pinnedRepositories: Any ATURI, it is up to appviews to validate these fields.
29
+
PinnedRepositories []string `json:"pinnedRepositories,omitempty" cborgen:"pinnedRepositories,omitempty"`
30
+
Stats []string `json:"stats,omitempty" cborgen:"stats,omitempty"`
31
+
}
+485
api/tangled/cbor_gen.go
+485
api/tangled/cbor_gen.go
···
3389
3389
3390
3390
return nil
3391
3391
}
3392
+
func (t *ActorProfile) MarshalCBOR(w io.Writer) error {
3393
+
if t == nil {
3394
+
_, err := w.Write(cbg.CborNull)
3395
+
return err
3396
+
}
3397
+
3398
+
cw := cbg.NewCborWriter(w)
3399
+
fieldCount := 7
3400
+
3401
+
if t.Description == nil {
3402
+
fieldCount--
3403
+
}
3404
+
3405
+
if t.Links == nil {
3406
+
fieldCount--
3407
+
}
3408
+
3409
+
if t.Location == nil {
3410
+
fieldCount--
3411
+
}
3412
+
3413
+
if t.PinnedRepositories == nil {
3414
+
fieldCount--
3415
+
}
3416
+
3417
+
if t.Stats == nil {
3418
+
fieldCount--
3419
+
}
3420
+
3421
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
3422
+
return err
3423
+
}
3424
+
3425
+
// t.LexiconTypeID (string) (string)
3426
+
if len("$type") > 1000000 {
3427
+
return xerrors.Errorf("Value in field \"$type\" was too long")
3428
+
}
3429
+
3430
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
3431
+
return err
3432
+
}
3433
+
if _, err := cw.WriteString(string("$type")); err != nil {
3434
+
return err
3435
+
}
3436
+
3437
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.actor.profile"))); err != nil {
3438
+
return err
3439
+
}
3440
+
if _, err := cw.WriteString(string("sh.tangled.actor.profile")); err != nil {
3441
+
return err
3442
+
}
3443
+
3444
+
// t.Links ([]string) (slice)
3445
+
if t.Links != nil {
3446
+
3447
+
if len("links") > 1000000 {
3448
+
return xerrors.Errorf("Value in field \"links\" was too long")
3449
+
}
3450
+
3451
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("links"))); err != nil {
3452
+
return err
3453
+
}
3454
+
if _, err := cw.WriteString(string("links")); err != nil {
3455
+
return err
3456
+
}
3457
+
3458
+
if len(t.Links) > 8192 {
3459
+
return xerrors.Errorf("Slice value in field t.Links was too long")
3460
+
}
3461
+
3462
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Links))); err != nil {
3463
+
return err
3464
+
}
3465
+
for _, v := range t.Links {
3466
+
if len(v) > 1000000 {
3467
+
return xerrors.Errorf("Value in field v was too long")
3468
+
}
3469
+
3470
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
3471
+
return err
3472
+
}
3473
+
if _, err := cw.WriteString(string(v)); err != nil {
3474
+
return err
3475
+
}
3476
+
3477
+
}
3478
+
}
3479
+
3480
+
// t.Stats ([]string) (slice)
3481
+
if t.Stats != nil {
3482
+
3483
+
if len("stats") > 1000000 {
3484
+
return xerrors.Errorf("Value in field \"stats\" was too long")
3485
+
}
3486
+
3487
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("stats"))); err != nil {
3488
+
return err
3489
+
}
3490
+
if _, err := cw.WriteString(string("stats")); err != nil {
3491
+
return err
3492
+
}
3493
+
3494
+
if len(t.Stats) > 8192 {
3495
+
return xerrors.Errorf("Slice value in field t.Stats was too long")
3496
+
}
3497
+
3498
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Stats))); err != nil {
3499
+
return err
3500
+
}
3501
+
for _, v := range t.Stats {
3502
+
if len(v) > 1000000 {
3503
+
return xerrors.Errorf("Value in field v was too long")
3504
+
}
3505
+
3506
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
3507
+
return err
3508
+
}
3509
+
if _, err := cw.WriteString(string(v)); err != nil {
3510
+
return err
3511
+
}
3512
+
3513
+
}
3514
+
}
3515
+
3516
+
// t.Bluesky (bool) (bool)
3517
+
if len("bluesky") > 1000000 {
3518
+
return xerrors.Errorf("Value in field \"bluesky\" was too long")
3519
+
}
3520
+
3521
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("bluesky"))); err != nil {
3522
+
return err
3523
+
}
3524
+
if _, err := cw.WriteString(string("bluesky")); err != nil {
3525
+
return err
3526
+
}
3527
+
3528
+
if err := cbg.WriteBool(w, t.Bluesky); err != nil {
3529
+
return err
3530
+
}
3531
+
3532
+
// t.Location (string) (string)
3533
+
if t.Location != nil {
3534
+
3535
+
if len("location") > 1000000 {
3536
+
return xerrors.Errorf("Value in field \"location\" was too long")
3537
+
}
3538
+
3539
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("location"))); err != nil {
3540
+
return err
3541
+
}
3542
+
if _, err := cw.WriteString(string("location")); err != nil {
3543
+
return err
3544
+
}
3545
+
3546
+
if t.Location == nil {
3547
+
if _, err := cw.Write(cbg.CborNull); err != nil {
3548
+
return err
3549
+
}
3550
+
} else {
3551
+
if len(*t.Location) > 1000000 {
3552
+
return xerrors.Errorf("Value in field t.Location was too long")
3553
+
}
3554
+
3555
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Location))); err != nil {
3556
+
return err
3557
+
}
3558
+
if _, err := cw.WriteString(string(*t.Location)); err != nil {
3559
+
return err
3560
+
}
3561
+
}
3562
+
}
3563
+
3564
+
// t.Description (string) (string)
3565
+
if t.Description != nil {
3566
+
3567
+
if len("description") > 1000000 {
3568
+
return xerrors.Errorf("Value in field \"description\" was too long")
3569
+
}
3570
+
3571
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil {
3572
+
return err
3573
+
}
3574
+
if _, err := cw.WriteString(string("description")); err != nil {
3575
+
return err
3576
+
}
3577
+
3578
+
if t.Description == nil {
3579
+
if _, err := cw.Write(cbg.CborNull); err != nil {
3580
+
return err
3581
+
}
3582
+
} else {
3583
+
if len(*t.Description) > 1000000 {
3584
+
return xerrors.Errorf("Value in field t.Description was too long")
3585
+
}
3586
+
3587
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Description))); err != nil {
3588
+
return err
3589
+
}
3590
+
if _, err := cw.WriteString(string(*t.Description)); err != nil {
3591
+
return err
3592
+
}
3593
+
}
3594
+
}
3595
+
3596
+
// t.PinnedRepositories ([]string) (slice)
3597
+
if t.PinnedRepositories != nil {
3598
+
3599
+
if len("pinnedRepositories") > 1000000 {
3600
+
return xerrors.Errorf("Value in field \"pinnedRepositories\" was too long")
3601
+
}
3602
+
3603
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pinnedRepositories"))); err != nil {
3604
+
return err
3605
+
}
3606
+
if _, err := cw.WriteString(string("pinnedRepositories")); err != nil {
3607
+
return err
3608
+
}
3609
+
3610
+
if len(t.PinnedRepositories) > 8192 {
3611
+
return xerrors.Errorf("Slice value in field t.PinnedRepositories was too long")
3612
+
}
3613
+
3614
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.PinnedRepositories))); err != nil {
3615
+
return err
3616
+
}
3617
+
for _, v := range t.PinnedRepositories {
3618
+
if len(v) > 1000000 {
3619
+
return xerrors.Errorf("Value in field v was too long")
3620
+
}
3621
+
3622
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
3623
+
return err
3624
+
}
3625
+
if _, err := cw.WriteString(string(v)); err != nil {
3626
+
return err
3627
+
}
3628
+
3629
+
}
3630
+
}
3631
+
return nil
3632
+
}
3633
+
3634
+
func (t *ActorProfile) UnmarshalCBOR(r io.Reader) (err error) {
3635
+
*t = ActorProfile{}
3636
+
3637
+
cr := cbg.NewCborReader(r)
3638
+
3639
+
maj, extra, err := cr.ReadHeader()
3640
+
if err != nil {
3641
+
return err
3642
+
}
3643
+
defer func() {
3644
+
if err == io.EOF {
3645
+
err = io.ErrUnexpectedEOF
3646
+
}
3647
+
}()
3648
+
3649
+
if maj != cbg.MajMap {
3650
+
return fmt.Errorf("cbor input should be of type map")
3651
+
}
3652
+
3653
+
if extra > cbg.MaxLength {
3654
+
return fmt.Errorf("ActorProfile: map struct too large (%d)", extra)
3655
+
}
3656
+
3657
+
n := extra
3658
+
3659
+
nameBuf := make([]byte, 18)
3660
+
for i := uint64(0); i < n; i++ {
3661
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
3662
+
if err != nil {
3663
+
return err
3664
+
}
3665
+
3666
+
if !ok {
3667
+
// Field doesn't exist on this type, so ignore it
3668
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
3669
+
return err
3670
+
}
3671
+
continue
3672
+
}
3673
+
3674
+
switch string(nameBuf[:nameLen]) {
3675
+
// t.LexiconTypeID (string) (string)
3676
+
case "$type":
3677
+
3678
+
{
3679
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3680
+
if err != nil {
3681
+
return err
3682
+
}
3683
+
3684
+
t.LexiconTypeID = string(sval)
3685
+
}
3686
+
// t.Links ([]string) (slice)
3687
+
case "links":
3688
+
3689
+
maj, extra, err = cr.ReadHeader()
3690
+
if err != nil {
3691
+
return err
3692
+
}
3693
+
3694
+
if extra > 8192 {
3695
+
return fmt.Errorf("t.Links: array too large (%d)", extra)
3696
+
}
3697
+
3698
+
if maj != cbg.MajArray {
3699
+
return fmt.Errorf("expected cbor array")
3700
+
}
3701
+
3702
+
if extra > 0 {
3703
+
t.Links = make([]string, extra)
3704
+
}
3705
+
3706
+
for i := 0; i < int(extra); i++ {
3707
+
{
3708
+
var maj byte
3709
+
var extra uint64
3710
+
var err error
3711
+
_ = maj
3712
+
_ = extra
3713
+
_ = err
3714
+
3715
+
{
3716
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3717
+
if err != nil {
3718
+
return err
3719
+
}
3720
+
3721
+
t.Links[i] = string(sval)
3722
+
}
3723
+
3724
+
}
3725
+
}
3726
+
// t.Stats ([]string) (slice)
3727
+
case "stats":
3728
+
3729
+
maj, extra, err = cr.ReadHeader()
3730
+
if err != nil {
3731
+
return err
3732
+
}
3733
+
3734
+
if extra > 8192 {
3735
+
return fmt.Errorf("t.Stats: array too large (%d)", extra)
3736
+
}
3737
+
3738
+
if maj != cbg.MajArray {
3739
+
return fmt.Errorf("expected cbor array")
3740
+
}
3741
+
3742
+
if extra > 0 {
3743
+
t.Stats = make([]string, extra)
3744
+
}
3745
+
3746
+
for i := 0; i < int(extra); i++ {
3747
+
{
3748
+
var maj byte
3749
+
var extra uint64
3750
+
var err error
3751
+
_ = maj
3752
+
_ = extra
3753
+
_ = err
3754
+
3755
+
{
3756
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3757
+
if err != nil {
3758
+
return err
3759
+
}
3760
+
3761
+
t.Stats[i] = string(sval)
3762
+
}
3763
+
3764
+
}
3765
+
}
3766
+
// t.Bluesky (bool) (bool)
3767
+
case "bluesky":
3768
+
3769
+
maj, extra, err = cr.ReadHeader()
3770
+
if err != nil {
3771
+
return err
3772
+
}
3773
+
if maj != cbg.MajOther {
3774
+
return fmt.Errorf("booleans must be major type 7")
3775
+
}
3776
+
switch extra {
3777
+
case 20:
3778
+
t.Bluesky = false
3779
+
case 21:
3780
+
t.Bluesky = true
3781
+
default:
3782
+
return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra)
3783
+
}
3784
+
// t.Location (string) (string)
3785
+
case "location":
3786
+
3787
+
{
3788
+
b, err := cr.ReadByte()
3789
+
if err != nil {
3790
+
return err
3791
+
}
3792
+
if b != cbg.CborNull[0] {
3793
+
if err := cr.UnreadByte(); err != nil {
3794
+
return err
3795
+
}
3796
+
3797
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3798
+
if err != nil {
3799
+
return err
3800
+
}
3801
+
3802
+
t.Location = (*string)(&sval)
3803
+
}
3804
+
}
3805
+
// t.Description (string) (string)
3806
+
case "description":
3807
+
3808
+
{
3809
+
b, err := cr.ReadByte()
3810
+
if err != nil {
3811
+
return err
3812
+
}
3813
+
if b != cbg.CborNull[0] {
3814
+
if err := cr.UnreadByte(); err != nil {
3815
+
return err
3816
+
}
3817
+
3818
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3819
+
if err != nil {
3820
+
return err
3821
+
}
3822
+
3823
+
t.Description = (*string)(&sval)
3824
+
}
3825
+
}
3826
+
// t.PinnedRepositories ([]string) (slice)
3827
+
case "pinnedRepositories":
3828
+
3829
+
maj, extra, err = cr.ReadHeader()
3830
+
if err != nil {
3831
+
return err
3832
+
}
3833
+
3834
+
if extra > 8192 {
3835
+
return fmt.Errorf("t.PinnedRepositories: array too large (%d)", extra)
3836
+
}
3837
+
3838
+
if maj != cbg.MajArray {
3839
+
return fmt.Errorf("expected cbor array")
3840
+
}
3841
+
3842
+
if extra > 0 {
3843
+
t.PinnedRepositories = make([]string, extra)
3844
+
}
3845
+
3846
+
for i := 0; i < int(extra); i++ {
3847
+
{
3848
+
var maj byte
3849
+
var extra uint64
3850
+
var err error
3851
+
_ = maj
3852
+
_ = extra
3853
+
_ = err
3854
+
3855
+
{
3856
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3857
+
if err != nil {
3858
+
return err
3859
+
}
3860
+
3861
+
t.PinnedRepositories[i] = string(sval)
3862
+
}
3863
+
3864
+
}
3865
+
}
3866
+
3867
+
default:
3868
+
// Field doesn't exist on this type, so ignore it
3869
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
3870
+
return err
3871
+
}
3872
+
}
3873
+
}
3874
+
3875
+
return nil
3876
+
}
-217
appview/auth/auth.go
-217
appview/auth/auth.go
···
1
-
package auth
2
-
3
-
import (
4
-
"context"
5
-
"fmt"
6
-
"net/http"
7
-
"time"
8
-
9
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
10
-
"github.com/bluesky-social/indigo/atproto/identity"
11
-
"github.com/bluesky-social/indigo/xrpc"
12
-
"github.com/gorilla/sessions"
13
-
"tangled.sh/tangled.sh/core/appview"
14
-
)
15
-
16
-
type Auth struct {
17
-
Store *sessions.CookieStore
18
-
}
19
-
20
-
type AtSessionCreate struct {
21
-
comatproto.ServerCreateSession_Output
22
-
PDSEndpoint string
23
-
}
24
-
25
-
type AtSessionRefresh struct {
26
-
comatproto.ServerRefreshSession_Output
27
-
PDSEndpoint string
28
-
}
29
-
30
-
func Make(secret string) (*Auth, error) {
31
-
store := sessions.NewCookieStore([]byte(secret))
32
-
return &Auth{store}, nil
33
-
}
34
-
35
-
func (a *Auth) CreateInitialSession(ctx context.Context, resolved *identity.Identity, appPassword string) (*comatproto.ServerCreateSession_Output, error) {
36
-
37
-
pdsUrl := resolved.PDSEndpoint()
38
-
client := xrpc.Client{
39
-
Host: pdsUrl,
40
-
}
41
-
42
-
atSession, err := comatproto.ServerCreateSession(ctx, &client, &comatproto.ServerCreateSession_Input{
43
-
Identifier: resolved.DID.String(),
44
-
Password: appPassword,
45
-
})
46
-
if err != nil {
47
-
return nil, fmt.Errorf("invalid app password")
48
-
}
49
-
50
-
return atSession, nil
51
-
}
52
-
53
-
// Sessionish is an interface that provides access to the common fields of both types.
54
-
type Sessionish interface {
55
-
GetAccessJwt() string
56
-
GetActive() *bool
57
-
GetDid() string
58
-
GetDidDoc() *interface{}
59
-
GetHandle() string
60
-
GetRefreshJwt() string
61
-
GetStatus() *string
62
-
}
63
-
64
-
// Create a wrapper type for ServerRefreshSession_Output
65
-
type RefreshSessionWrapper struct {
66
-
*comatproto.ServerRefreshSession_Output
67
-
}
68
-
69
-
func (s *RefreshSessionWrapper) GetAccessJwt() string {
70
-
return s.AccessJwt
71
-
}
72
-
73
-
func (s *RefreshSessionWrapper) GetActive() *bool {
74
-
return s.Active
75
-
}
76
-
77
-
func (s *RefreshSessionWrapper) GetDid() string {
78
-
return s.Did
79
-
}
80
-
81
-
func (s *RefreshSessionWrapper) GetDidDoc() *interface{} {
82
-
return s.DidDoc
83
-
}
84
-
85
-
func (s *RefreshSessionWrapper) GetHandle() string {
86
-
return s.Handle
87
-
}
88
-
89
-
func (s *RefreshSessionWrapper) GetRefreshJwt() string {
90
-
return s.RefreshJwt
91
-
}
92
-
93
-
func (s *RefreshSessionWrapper) GetStatus() *string {
94
-
return s.Status
95
-
}
96
-
97
-
// Create a wrapper type for ServerRefreshSession_Output
98
-
type CreateSessionWrapper struct {
99
-
*comatproto.ServerCreateSession_Output
100
-
}
101
-
102
-
func (s *CreateSessionWrapper) GetAccessJwt() string {
103
-
return s.AccessJwt
104
-
}
105
-
106
-
func (s *CreateSessionWrapper) GetActive() *bool {
107
-
return s.Active
108
-
}
109
-
110
-
func (s *CreateSessionWrapper) GetDid() string {
111
-
return s.Did
112
-
}
113
-
114
-
func (s *CreateSessionWrapper) GetDidDoc() *interface{} {
115
-
return s.DidDoc
116
-
}
117
-
118
-
func (s *CreateSessionWrapper) GetHandle() string {
119
-
return s.Handle
120
-
}
121
-
122
-
func (s *CreateSessionWrapper) GetRefreshJwt() string {
123
-
return s.RefreshJwt
124
-
}
125
-
126
-
func (s *CreateSessionWrapper) GetStatus() *string {
127
-
return s.Status
128
-
}
129
-
130
-
func (a *Auth) ClearSession(r *http.Request, w http.ResponseWriter) error {
131
-
clientSession, err := a.Store.Get(r, appview.SessionName)
132
-
if err != nil {
133
-
return fmt.Errorf("invalid session", err)
134
-
}
135
-
if clientSession.IsNew {
136
-
return fmt.Errorf("invalid session")
137
-
}
138
-
clientSession.Options.MaxAge = -1
139
-
return clientSession.Save(r, w)
140
-
}
141
-
142
-
func (a *Auth) StoreSession(r *http.Request, w http.ResponseWriter, atSessionish Sessionish, pdsEndpoint string) error {
143
-
clientSession, _ := a.Store.Get(r, appview.SessionName)
144
-
clientSession.Values[appview.SessionHandle] = atSessionish.GetHandle()
145
-
clientSession.Values[appview.SessionDid] = atSessionish.GetDid()
146
-
clientSession.Values[appview.SessionPds] = pdsEndpoint
147
-
clientSession.Values[appview.SessionAccessJwt] = atSessionish.GetAccessJwt()
148
-
clientSession.Values[appview.SessionRefreshJwt] = atSessionish.GetRefreshJwt()
149
-
clientSession.Values[appview.SessionExpiry] = time.Now().Add(time.Minute * 15).Format(time.RFC3339)
150
-
clientSession.Values[appview.SessionAuthenticated] = true
151
-
return clientSession.Save(r, w)
152
-
}
153
-
154
-
func (a *Auth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) {
155
-
clientSession, err := a.Store.Get(r, "appview-session")
156
-
if err != nil || clientSession.IsNew {
157
-
return nil, err
158
-
}
159
-
160
-
did := clientSession.Values["did"].(string)
161
-
pdsUrl := clientSession.Values["pds"].(string)
162
-
accessJwt := clientSession.Values["accessJwt"].(string)
163
-
refreshJwt := clientSession.Values["refreshJwt"].(string)
164
-
165
-
client := &xrpc.Client{
166
-
Host: pdsUrl,
167
-
Auth: &xrpc.AuthInfo{
168
-
AccessJwt: accessJwt,
169
-
RefreshJwt: refreshJwt,
170
-
Did: did,
171
-
},
172
-
}
173
-
174
-
return client, nil
175
-
}
176
-
177
-
func (a *Auth) GetSession(r *http.Request) (*sessions.Session, error) {
178
-
return a.Store.Get(r, appview.SessionName)
179
-
}
180
-
181
-
func (a *Auth) GetDid(r *http.Request) string {
182
-
clientSession, err := a.Store.Get(r, appview.SessionName)
183
-
if err != nil || clientSession.IsNew {
184
-
return ""
185
-
}
186
-
187
-
return clientSession.Values[appview.SessionDid].(string)
188
-
}
189
-
190
-
func (a *Auth) GetHandle(r *http.Request) string {
191
-
clientSession, err := a.Store.Get(r, appview.SessionName)
192
-
if err != nil || clientSession.IsNew {
193
-
return ""
194
-
}
195
-
196
-
return clientSession.Values[appview.SessionHandle].(string)
197
-
}
198
-
199
-
type User struct {
200
-
Handle string
201
-
Did string
202
-
Pds string
203
-
}
204
-
205
-
func (a *Auth) GetUser(r *http.Request) *User {
206
-
clientSession, err := a.Store.Get(r, appview.SessionName)
207
-
208
-
if err != nil || clientSession.IsNew {
209
-
return nil
210
-
}
211
-
212
-
return &User{
213
-
Handle: clientSession.Values[appview.SessionHandle].(string),
214
-
Did: clientSession.Values[appview.SessionDid].(string),
215
-
Pds: clientSession.Values[appview.SessionPds].(string),
216
-
}
217
-
}
+36
-10
appview/config.go
+36
-10
appview/config.go
···
6
6
"github.com/sethvargo/go-envconfig"
7
7
)
8
8
9
+
type CoreConfig struct {
10
+
CookieSecret string `env:"COOKIE_SECRET, default=00000000000000000000000000000000"`
11
+
DbPath string `env:"DB_PATH, default=appview.db"`
12
+
ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:3000"`
13
+
AppviewHost string `env:"APPVIEW_HOST, default=https://tangled.sh"`
14
+
Dev bool `env:"DEV, default=false"`
15
+
}
16
+
17
+
type OAuthConfig struct {
18
+
Jwks string `env:"JWKS"`
19
+
}
20
+
21
+
type JetstreamConfig struct {
22
+
Endpoint string `env:"ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"`
23
+
}
24
+
25
+
type ResendConfig struct {
26
+
ApiKey string `env:"API_KEY"`
27
+
}
28
+
29
+
type CamoConfig struct {
30
+
Host string `env:"HOST, default=https://camo.tangled.sh"`
31
+
SharedSecret string `env:"SHARED_SECRET"`
32
+
}
33
+
34
+
type AvatarConfig struct {
35
+
Host string `env:"HOST, default=https://avatar.tangled.sh"`
36
+
SharedSecret string `env:"SHARED_SECRET"`
37
+
}
38
+
9
39
type Config struct {
10
-
CookieSecret string `env:"TANGLED_COOKIE_SECRET, default=00000000000000000000000000000000"`
11
-
DbPath string `env:"TANGLED_DB_PATH, default=appview.db"`
12
-
ListenAddr string `env:"TANGLED_LISTEN_ADDR, default=0.0.0.0:3000"`
13
-
Dev bool `env:"TANGLED_DEV, default=false"`
14
-
JetstreamEndpoint string `env:"TANGLED_JETSTREAM_ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"`
15
-
ResendApiKey string `env:"TANGLED_RESEND_API_KEY"`
16
-
CamoHost string `env:"TANGLED_CAMO_HOST, default=https://camo.tangled.sh"`
17
-
CamoSharedSecret string `env:"TANGLED_CAMO_SHARED_SECRET"`
18
-
AvatarSharedSecret string `env:"TANGLED_AVATAR_SHARED_SECRET"`
19
-
AvatarHost string `env:"TANGLED_AVATAR_HOST, default=https://avatar.tangled.sh"`
40
+
Core CoreConfig `env:",prefix=TANGLED_"`
41
+
Jetstream JetstreamConfig `env:",prefix=TANGLED_JETSTREAM_"`
42
+
Resend ResendConfig `env:",prefix=TANGLED_RESEND_"`
43
+
Camo CamoConfig `env:",prefix=TANGLED_CAMO_"`
44
+
Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"`
45
+
OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"`
20
46
}
21
47
22
48
func LoadConfig(ctx context.Context) (*Config, error) {
+3
appview/consts.go
+3
appview/consts.go
-16
appview/db/artifact.go
-16
appview/db/artifact.go
···
57
57
return err
58
58
}
59
59
60
-
type filter struct {
61
-
key string
62
-
arg any
63
-
}
64
-
65
-
func Filter(key string, arg any) filter {
66
-
return filter{
67
-
key: key,
68
-
arg: arg,
69
-
}
70
-
}
71
-
72
-
func (f filter) Condition() string {
73
-
return fmt.Sprintf("%s = ?", f.key)
74
-
}
75
-
76
60
func GetArtifact(e Execer, filters ...filter) ([]Artifact, error) {
77
61
var artifacts []Artifact
78
62
+99
appview/db/db.go
+99
appview/db/db.go
···
3
3
import (
4
4
"context"
5
5
"database/sql"
6
+
"fmt"
6
7
"log"
7
8
8
9
_ "github.com/mattn/go-sqlite3"
···
231
232
foreign key (repo_at) references repos(at_uri) on delete cascade
232
233
);
233
234
235
+
create table if not exists profile (
236
+
-- id
237
+
id integer primary key autoincrement,
238
+
did text not null,
239
+
240
+
-- data
241
+
description text not null,
242
+
include_bluesky integer not null default 0,
243
+
location text,
244
+
245
+
-- constraints
246
+
unique(did)
247
+
);
248
+
create table if not exists profile_links (
249
+
-- id
250
+
id integer primary key autoincrement,
251
+
did text not null,
252
+
253
+
-- data
254
+
link text not null,
255
+
256
+
-- constraints
257
+
foreign key (did) references profile(did) on delete cascade
258
+
);
259
+
create table if not exists profile_stats (
260
+
-- id
261
+
id integer primary key autoincrement,
262
+
did text not null,
263
+
264
+
-- data
265
+
kind text not null check (kind in (
266
+
"merged-pull-request-count",
267
+
"closed-pull-request-count",
268
+
"open-pull-request-count",
269
+
"open-issue-count",
270
+
"closed-issue-count",
271
+
"repository-count"
272
+
)),
273
+
274
+
-- constraints
275
+
foreign key (did) references profile(did) on delete cascade
276
+
);
277
+
create table if not exists profile_pinned_repositories (
278
+
-- id
279
+
id integer primary key autoincrement,
280
+
did text not null,
281
+
282
+
-- data
283
+
at_uri text not null,
284
+
285
+
-- constraints
286
+
unique(did, at_uri),
287
+
foreign key (did) references profile(did) on delete cascade,
288
+
foreign key (at_uri) references repos(at_uri) on delete cascade
289
+
);
290
+
291
+
create table if not exists oauth_requests (
292
+
id integer primary key autoincrement,
293
+
auth_server_iss text not null,
294
+
state text not null,
295
+
did text not null,
296
+
handle text not null,
297
+
pds_url text not null,
298
+
pkce_verifier text not null,
299
+
dpop_auth_server_nonce text not null,
300
+
dpop_private_jwk text not null
301
+
);
302
+
303
+
create table if not exists oauth_sessions (
304
+
id integer primary key autoincrement,
305
+
did text not null,
306
+
handle text not null,
307
+
pds_url text not null,
308
+
auth_server_iss text not null,
309
+
access_jwt text not null,
310
+
refresh_jwt text not null,
311
+
dpop_pds_nonce text,
312
+
dpop_auth_server_nonce text not null,
313
+
dpop_private_jwk text not null,
314
+
expiry text not null
315
+
);
316
+
234
317
create table if not exists migrations (
235
318
id integer primary key autoincrement,
236
319
name text unique
···
348
431
349
432
return nil
350
433
}
434
+
435
+
type filter struct {
436
+
key string
437
+
arg any
438
+
}
439
+
440
+
func Filter(key string, arg any) filter {
441
+
return filter{
442
+
key: key,
443
+
arg: arg,
444
+
}
445
+
}
446
+
447
+
func (f filter) Condition() string {
448
+
return fmt.Sprintf("%s = ?", f.key)
449
+
}
+173
appview/db/oauth.go
+173
appview/db/oauth.go
···
1
+
package db
2
+
3
+
type OAuthRequest struct {
4
+
ID uint
5
+
AuthserverIss string
6
+
Handle string
7
+
State string
8
+
Did string
9
+
PdsUrl string
10
+
PkceVerifier string
11
+
DpopAuthserverNonce string
12
+
DpopPrivateJwk string
13
+
}
14
+
15
+
func SaveOAuthRequest(e Execer, oauthRequest OAuthRequest) error {
16
+
_, err := e.Exec(`
17
+
insert into oauth_requests (
18
+
auth_server_iss,
19
+
state,
20
+
handle,
21
+
did,
22
+
pds_url,
23
+
pkce_verifier,
24
+
dpop_auth_server_nonce,
25
+
dpop_private_jwk
26
+
) values (?, ?, ?, ?, ?, ?, ?, ?)`,
27
+
oauthRequest.AuthserverIss,
28
+
oauthRequest.State,
29
+
oauthRequest.Handle,
30
+
oauthRequest.Did,
31
+
oauthRequest.PdsUrl,
32
+
oauthRequest.PkceVerifier,
33
+
oauthRequest.DpopAuthserverNonce,
34
+
oauthRequest.DpopPrivateJwk,
35
+
)
36
+
return err
37
+
}
38
+
39
+
func GetOAuthRequestByState(e Execer, state string) (OAuthRequest, error) {
40
+
var req OAuthRequest
41
+
err := e.QueryRow(`
42
+
select
43
+
id,
44
+
auth_server_iss,
45
+
handle,
46
+
state,
47
+
did,
48
+
pds_url,
49
+
pkce_verifier,
50
+
dpop_auth_server_nonce,
51
+
dpop_private_jwk
52
+
from oauth_requests
53
+
where state = ?`, state).Scan(
54
+
&req.ID,
55
+
&req.AuthserverIss,
56
+
&req.Handle,
57
+
&req.State,
58
+
&req.Did,
59
+
&req.PdsUrl,
60
+
&req.PkceVerifier,
61
+
&req.DpopAuthserverNonce,
62
+
&req.DpopPrivateJwk,
63
+
)
64
+
return req, err
65
+
}
66
+
67
+
func DeleteOAuthRequestByState(e Execer, state string) error {
68
+
_, err := e.Exec(`
69
+
delete from oauth_requests
70
+
where state = ?`, state)
71
+
return err
72
+
}
73
+
74
+
type OAuthSession struct {
75
+
ID uint
76
+
Handle string
77
+
Did string
78
+
PdsUrl string
79
+
AccessJwt string
80
+
RefreshJwt string
81
+
AuthServerIss string
82
+
DpopPdsNonce string
83
+
DpopAuthserverNonce string
84
+
DpopPrivateJwk string
85
+
Expiry string
86
+
}
87
+
88
+
func SaveOAuthSession(e Execer, session OAuthSession) error {
89
+
_, err := e.Exec(`
90
+
insert into oauth_sessions (
91
+
did,
92
+
handle,
93
+
pds_url,
94
+
access_jwt,
95
+
refresh_jwt,
96
+
auth_server_iss,
97
+
dpop_auth_server_nonce,
98
+
dpop_private_jwk,
99
+
expiry
100
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
101
+
session.Did,
102
+
session.Handle,
103
+
session.PdsUrl,
104
+
session.AccessJwt,
105
+
session.RefreshJwt,
106
+
session.AuthServerIss,
107
+
session.DpopAuthserverNonce,
108
+
session.DpopPrivateJwk,
109
+
session.Expiry,
110
+
)
111
+
return err
112
+
}
113
+
114
+
func RefreshOAuthSession(e Execer, did string, accessJwt, refreshJwt, expiry string) error {
115
+
_, err := e.Exec(`
116
+
update oauth_sessions
117
+
set access_jwt = ?, refresh_jwt = ?, expiry = ?
118
+
where did = ?`,
119
+
accessJwt,
120
+
refreshJwt,
121
+
expiry,
122
+
did,
123
+
)
124
+
return err
125
+
}
126
+
127
+
func GetOAuthSessionByDid(e Execer, did string) (*OAuthSession, error) {
128
+
var session OAuthSession
129
+
err := e.QueryRow(`
130
+
select
131
+
id,
132
+
did,
133
+
handle,
134
+
pds_url,
135
+
access_jwt,
136
+
refresh_jwt,
137
+
auth_server_iss,
138
+
dpop_auth_server_nonce,
139
+
dpop_private_jwk,
140
+
expiry
141
+
from oauth_sessions
142
+
where did = ?`, did).Scan(
143
+
&session.ID,
144
+
&session.Did,
145
+
&session.Handle,
146
+
&session.PdsUrl,
147
+
&session.AccessJwt,
148
+
&session.RefreshJwt,
149
+
&session.AuthServerIss,
150
+
&session.DpopAuthserverNonce,
151
+
&session.DpopPrivateJwk,
152
+
&session.Expiry,
153
+
)
154
+
return &session, err
155
+
}
156
+
157
+
func DeleteOAuthSessionByDid(e Execer, did string) error {
158
+
_, err := e.Exec(`
159
+
delete from oauth_sessions
160
+
where did = ?`, did)
161
+
return err
162
+
}
163
+
164
+
func UpdateDpopPdsNonce(e Execer, did string, dpopPdsNonce string) error {
165
+
_, err := e.Exec(`
166
+
update oauth_sessions
167
+
set dpop_pds_nonce = ?
168
+
where did = ?`,
169
+
dpopPdsNonce,
170
+
did,
171
+
)
172
+
return err
173
+
}
+366
appview/db/profile.go
+366
appview/db/profile.go
···
1
1
package db
2
2
3
3
import (
4
+
"database/sql"
4
5
"fmt"
6
+
"log"
7
+
"net/url"
8
+
"slices"
9
+
"strings"
5
10
"time"
11
+
12
+
"github.com/bluesky-social/indigo/atproto/syntax"
13
+
"tangled.sh/tangled.sh/core/api/tangled"
6
14
)
7
15
8
16
type RepoEvent struct {
···
162
170
163
171
return &timeline, nil
164
172
}
173
+
174
+
type Profile struct {
175
+
// ids
176
+
ID int
177
+
Did string
178
+
179
+
// data
180
+
Description string
181
+
IncludeBluesky bool
182
+
Location string
183
+
Links [5]string
184
+
Stats [2]VanityStat
185
+
PinnedRepos [6]syntax.ATURI
186
+
}
187
+
188
+
func (p Profile) IsLinksEmpty() bool {
189
+
for _, l := range p.Links {
190
+
if l != "" {
191
+
return false
192
+
}
193
+
}
194
+
return true
195
+
}
196
+
197
+
func (p Profile) IsStatsEmpty() bool {
198
+
for _, s := range p.Stats {
199
+
if s.Kind != "" {
200
+
return false
201
+
}
202
+
}
203
+
return true
204
+
}
205
+
206
+
func (p Profile) IsPinnedReposEmpty() bool {
207
+
for _, r := range p.PinnedRepos {
208
+
if r != "" {
209
+
return false
210
+
}
211
+
}
212
+
return true
213
+
}
214
+
215
+
type VanityStatKind string
216
+
217
+
const (
218
+
VanityStatMergedPRCount VanityStatKind = "merged-pull-request-count"
219
+
VanityStatClosedPRCount VanityStatKind = "closed-pull-request-count"
220
+
VanityStatOpenPRCount VanityStatKind = "open-pull-request-count"
221
+
VanityStatOpenIssueCount VanityStatKind = "open-issue-count"
222
+
VanityStatClosedIssueCount VanityStatKind = "closed-issue-count"
223
+
VanityStatRepositoryCount VanityStatKind = "repository-count"
224
+
)
225
+
226
+
func (v VanityStatKind) String() string {
227
+
switch v {
228
+
case VanityStatMergedPRCount:
229
+
return "Merged PRs"
230
+
case VanityStatClosedPRCount:
231
+
return "Closed PRs"
232
+
case VanityStatOpenPRCount:
233
+
return "Open PRs"
234
+
case VanityStatOpenIssueCount:
235
+
return "Open Issues"
236
+
case VanityStatClosedIssueCount:
237
+
return "Closed Issues"
238
+
case VanityStatRepositoryCount:
239
+
return "Repositories"
240
+
}
241
+
return ""
242
+
}
243
+
244
+
type VanityStat struct {
245
+
Kind VanityStatKind
246
+
Value uint64
247
+
}
248
+
249
+
func (p *Profile) ProfileAt() syntax.ATURI {
250
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.ActorProfileNSID, "self"))
251
+
}
252
+
253
+
func UpsertProfile(tx *sql.Tx, profile *Profile) error {
254
+
defer tx.Rollback()
255
+
256
+
// update links
257
+
_, err := tx.Exec(`delete from profile_links where did = ?`, profile.Did)
258
+
if err != nil {
259
+
return err
260
+
}
261
+
// update vanity stats
262
+
_, err = tx.Exec(`delete from profile_stats where did = ?`, profile.Did)
263
+
if err != nil {
264
+
return err
265
+
}
266
+
267
+
// update pinned repos
268
+
_, err = tx.Exec(`delete from profile_pinned_repositories where did = ?`, profile.Did)
269
+
if err != nil {
270
+
return err
271
+
}
272
+
273
+
includeBskyValue := 0
274
+
if profile.IncludeBluesky {
275
+
includeBskyValue = 1
276
+
}
277
+
278
+
_, err = tx.Exec(
279
+
`insert or replace into profile (
280
+
did,
281
+
description,
282
+
include_bluesky,
283
+
location
284
+
)
285
+
values (?, ?, ?, ?)`,
286
+
profile.Did,
287
+
profile.Description,
288
+
includeBskyValue,
289
+
profile.Location,
290
+
)
291
+
292
+
if err != nil {
293
+
log.Println("profile", "err", err)
294
+
return err
295
+
}
296
+
297
+
for _, link := range profile.Links {
298
+
if link == "" {
299
+
continue
300
+
}
301
+
302
+
_, err := tx.Exec(
303
+
`insert into profile_links (did, link) values (?, ?)`,
304
+
profile.Did,
305
+
link,
306
+
)
307
+
308
+
if err != nil {
309
+
log.Println("profile_links", "err", err)
310
+
return err
311
+
}
312
+
}
313
+
314
+
for _, v := range profile.Stats {
315
+
if v.Kind == "" {
316
+
continue
317
+
}
318
+
319
+
_, err := tx.Exec(
320
+
`insert into profile_stats (did, kind) values (?, ?)`,
321
+
profile.Did,
322
+
v.Kind,
323
+
)
324
+
325
+
if err != nil {
326
+
log.Println("profile_stats", "err", err)
327
+
return err
328
+
}
329
+
}
330
+
331
+
for _, pin := range profile.PinnedRepos {
332
+
if pin == "" {
333
+
continue
334
+
}
335
+
336
+
_, err := tx.Exec(
337
+
`insert into profile_pinned_repositories (did, at_uri) values (?, ?)`,
338
+
profile.Did,
339
+
pin,
340
+
)
341
+
342
+
if err != nil {
343
+
log.Println("profile_pinned_repositories", "err", err)
344
+
return err
345
+
}
346
+
}
347
+
348
+
return tx.Commit()
349
+
}
350
+
351
+
func GetProfile(e Execer, did string) (*Profile, error) {
352
+
var profile Profile
353
+
profile.Did = did
354
+
355
+
includeBluesky := 0
356
+
err := e.QueryRow(
357
+
`select description, include_bluesky, location from profile where did = ?`,
358
+
did,
359
+
).Scan(&profile.Description, &includeBluesky, &profile.Location)
360
+
if err == sql.ErrNoRows {
361
+
profile := Profile{}
362
+
profile.Did = did
363
+
return &profile, nil
364
+
}
365
+
366
+
if err != nil {
367
+
return nil, err
368
+
}
369
+
370
+
if includeBluesky != 0 {
371
+
profile.IncludeBluesky = true
372
+
}
373
+
374
+
rows, err := e.Query(`select link from profile_links where did = ?`, did)
375
+
if err != nil {
376
+
return nil, err
377
+
}
378
+
defer rows.Close()
379
+
i := 0
380
+
for rows.Next() {
381
+
if err := rows.Scan(&profile.Links[i]); err != nil {
382
+
return nil, err
383
+
}
384
+
i++
385
+
}
386
+
387
+
rows, err = e.Query(`select kind from profile_stats where did = ?`, did)
388
+
if err != nil {
389
+
return nil, err
390
+
}
391
+
defer rows.Close()
392
+
i = 0
393
+
for rows.Next() {
394
+
if err := rows.Scan(&profile.Stats[i].Kind); err != nil {
395
+
return nil, err
396
+
}
397
+
value, err := GetVanityStat(e, profile.Did, profile.Stats[i].Kind)
398
+
if err != nil {
399
+
return nil, err
400
+
}
401
+
profile.Stats[i].Value = value
402
+
i++
403
+
}
404
+
405
+
rows, err = e.Query(`select at_uri from profile_pinned_repositories where did = ?`, did)
406
+
if err != nil {
407
+
return nil, err
408
+
}
409
+
defer rows.Close()
410
+
i = 0
411
+
for rows.Next() {
412
+
if err := rows.Scan(&profile.PinnedRepos[i]); err != nil {
413
+
return nil, err
414
+
}
415
+
i++
416
+
}
417
+
418
+
return &profile, nil
419
+
}
420
+
421
+
func GetVanityStat(e Execer, did string, stat VanityStatKind) (uint64, error) {
422
+
query := ""
423
+
var args []any
424
+
switch stat {
425
+
case VanityStatMergedPRCount:
426
+
query = `select count(id) from pulls where owner_did = ? and state = ?`
427
+
args = append(args, did, PullMerged)
428
+
case VanityStatClosedPRCount:
429
+
query = `select count(id) from pulls where owner_did = ? and state = ?`
430
+
args = append(args, did, PullClosed)
431
+
case VanityStatOpenPRCount:
432
+
query = `select count(id) from pulls where owner_did = ? and state = ?`
433
+
args = append(args, did, PullOpen)
434
+
case VanityStatOpenIssueCount:
435
+
query = `select count(id) from issues where owner_did = ? and open = 1`
436
+
args = append(args, did)
437
+
case VanityStatClosedIssueCount:
438
+
query = `select count(id) from issues where owner_did = ? and open = 0`
439
+
args = append(args, did)
440
+
case VanityStatRepositoryCount:
441
+
query = `select count(id) from repos where did = ?`
442
+
args = append(args, did)
443
+
}
444
+
445
+
var result uint64
446
+
err := e.QueryRow(query, args...).Scan(&result)
447
+
if err != nil {
448
+
return 0, err
449
+
}
450
+
451
+
return result, nil
452
+
}
453
+
454
+
func ValidateProfile(e Execer, profile *Profile) error {
455
+
// ensure description is not too long
456
+
if len(profile.Description) > 256 {
457
+
return fmt.Errorf("Entered bio is too long.")
458
+
}
459
+
460
+
// ensure description is not too long
461
+
if len(profile.Location) > 40 {
462
+
return fmt.Errorf("Entered location is too long.")
463
+
}
464
+
465
+
// ensure links are in order
466
+
err := validateLinks(profile)
467
+
if err != nil {
468
+
return err
469
+
}
470
+
471
+
// ensure all pinned repos are either own repos or collaborating repos
472
+
repos, err := GetAllReposByDid(e, profile.Did)
473
+
if err != nil {
474
+
log.Printf("getting repos for %s: %s", profile.Did, err)
475
+
}
476
+
477
+
collaboratingRepos, err := CollaboratingIn(e, profile.Did)
478
+
if err != nil {
479
+
log.Printf("getting collaborating repos for %s: %s", profile.Did, err)
480
+
}
481
+
482
+
var validRepos []syntax.ATURI
483
+
for _, r := range repos {
484
+
validRepos = append(validRepos, r.RepoAt())
485
+
}
486
+
for _, r := range collaboratingRepos {
487
+
validRepos = append(validRepos, r.RepoAt())
488
+
}
489
+
490
+
for _, pinned := range profile.PinnedRepos {
491
+
if pinned == "" {
492
+
continue
493
+
}
494
+
if !slices.Contains(validRepos, pinned) {
495
+
return fmt.Errorf("Invalid pinned repo: `%s, does not belong to own or collaborating repos", pinned)
496
+
}
497
+
}
498
+
499
+
return nil
500
+
}
501
+
502
+
func validateLinks(profile *Profile) error {
503
+
for i, link := range profile.Links {
504
+
if link == "" {
505
+
continue
506
+
}
507
+
508
+
parsedURL, err := url.Parse(link)
509
+
if err != nil {
510
+
return fmt.Errorf("Invalid URL '%s': %v\n", link, err)
511
+
}
512
+
513
+
if parsedURL.Scheme == "" {
514
+
if strings.HasPrefix(link, "//") {
515
+
profile.Links[i] = "https:" + link
516
+
} else {
517
+
profile.Links[i] = "https://" + link
518
+
}
519
+
continue
520
+
} else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
521
+
return fmt.Errorf("Warning: URL '%s' has unusual scheme: %s\n", link, parsedURL.Scheme)
522
+
}
523
+
524
+
// catch relative paths
525
+
if parsedURL.Host == "" {
526
+
return fmt.Errorf("Warning: URL '%s' appears to be a relative path\n", link)
527
+
}
528
+
}
529
+
return nil
530
+
}
+1
-11
appview/db/pulls.go
+1
-11
appview/db/pulls.go
···
235
235
}
236
236
237
237
func NewPull(tx *sql.Tx, pull *Pull) error {
238
-
defer tx.Rollback()
239
-
240
238
_, err := tx.Exec(`
241
239
insert or ignore into repo_pull_seqs (repo_at, next_pull_id)
242
240
values (?, 1)
···
291
289
insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
292
290
values (?, ?, ?, ?, ?)
293
291
`, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev)
294
-
if err != nil {
295
-
return err
296
-
}
297
-
298
-
if err := tx.Commit(); err != nil {
299
-
return err
300
-
}
301
-
302
-
return nil
292
+
return err
303
293
}
304
294
305
295
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
+12
appview/db/repos.go
+12
appview/db/repos.go
···
2
2
3
3
import (
4
4
"database/sql"
5
+
"fmt"
5
6
"time"
6
7
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
securejoin "github.com/cyphar/filepath-securejoin"
10
+
"tangled.sh/tangled.sh/core/api/tangled"
8
11
)
9
12
10
13
type Repo struct {
···
21
24
22
25
// optional
23
26
Source string
27
+
}
28
+
29
+
func (r Repo) RepoAt() syntax.ATURI {
30
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey))
31
+
}
32
+
33
+
func (r Repo) DidSlashRepo() string {
34
+
p, _ := securejoin.SecureJoin(r.Did, r.Name)
35
+
return p
24
36
}
25
37
26
38
func GetAllRepos(e Execer, limit int) ([]Repo, error) {
+1
-1
appview/db/star.go
+1
-1
appview/db/star.go
···
71
71
72
72
// Remove a star
73
73
func DeleteStarByRkey(e Execer, starredByDid string, rkey string) error {
74
-
_, err := e.Exec(`delete or ignore from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey)
74
+
_, err := e.Exec(`delete from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey)
75
75
return err
76
76
}
77
77
+104
-5
appview/ingester.go
+104
-5
appview/ingester.go
···
13
13
"github.com/ipfs/go-cid"
14
14
"tangled.sh/tangled.sh/core/api/tangled"
15
15
"tangled.sh/tangled.sh/core/appview/db"
16
+
"tangled.sh/tangled.sh/core/rbac"
16
17
)
17
18
18
19
type Ingester func(ctx context.Context, e *models.Event) error
19
20
20
-
func Ingest(d db.DbWrapper) Ingester {
21
+
func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer) Ingester {
21
22
return func(ctx context.Context, e *models.Event) error {
22
23
var err error
23
24
defer func() {
···
40
41
case tangled.PublicKeyNSID:
41
42
ingestPublicKey(&d, e)
42
43
case tangled.RepoArtifactNSID:
43
-
ingestArtifact(&d, e)
44
+
ingestArtifact(&d, e, enforcer)
45
+
case tangled.ActorProfileNSID:
46
+
ingestProfile(&d, e)
44
47
}
45
48
46
49
return err
···
137
140
return nil
138
141
}
139
142
140
-
func ingestArtifact(d *db.DbWrapper, e *models.Event) error {
143
+
func ingestArtifact(d *db.DbWrapper, e *models.Event, enforcer *rbac.Enforcer) error {
141
144
did := e.Did
142
145
var err error
143
146
144
147
switch e.Commit.Operation {
145
148
case models.CommitOperationCreate, models.CommitOperationUpdate:
146
-
log.Println("processing add of artifact")
147
149
raw := json.RawMessage(e.Commit.Record)
148
150
record := tangled.RepoArtifact{}
149
151
err = json.Unmarshal(raw, &record)
···
157
159
return err
158
160
}
159
161
162
+
repo, err := db.GetRepoByAtUri(d, repoAt.String())
163
+
if err != nil {
164
+
return err
165
+
}
166
+
167
+
ok, err := enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push")
168
+
if err != nil || !ok {
169
+
return err
170
+
}
171
+
160
172
createdAt, err := time.Parse(time.RFC3339, record.CreatedAt)
161
173
if err != nil {
162
174
createdAt = time.Now()
···
176
188
177
189
err = db.AddArtifact(d, artifact)
178
190
case models.CommitOperationDelete:
179
-
log.Println("processing delete of artifact")
180
191
err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey))
181
192
}
182
193
···
186
197
187
198
return nil
188
199
}
200
+
201
+
func ingestProfile(d *db.DbWrapper, e *models.Event) error {
202
+
did := e.Did
203
+
var err error
204
+
205
+
if e.Commit.RKey != "self" {
206
+
return fmt.Errorf("ingestProfile only ingests `self` record")
207
+
}
208
+
209
+
switch e.Commit.Operation {
210
+
case models.CommitOperationCreate, models.CommitOperationUpdate:
211
+
raw := json.RawMessage(e.Commit.Record)
212
+
record := tangled.ActorProfile{}
213
+
err = json.Unmarshal(raw, &record)
214
+
if err != nil {
215
+
log.Printf("invalid record: %s", err)
216
+
return err
217
+
}
218
+
219
+
description := ""
220
+
if record.Description != nil {
221
+
description = *record.Description
222
+
}
223
+
224
+
includeBluesky := record.Bluesky
225
+
226
+
location := ""
227
+
if record.Location != nil {
228
+
location = *record.Location
229
+
}
230
+
231
+
var links [5]string
232
+
for i, l := range record.Links {
233
+
if i < 5 {
234
+
links[i] = l
235
+
}
236
+
}
237
+
238
+
var stats [2]db.VanityStat
239
+
for i, s := range record.Stats {
240
+
if i < 2 {
241
+
stats[i].Kind = db.VanityStatKind(s)
242
+
}
243
+
}
244
+
245
+
var pinned [6]syntax.ATURI
246
+
for i, r := range record.PinnedRepositories {
247
+
if i < 6 {
248
+
pinned[i] = syntax.ATURI(r)
249
+
}
250
+
}
251
+
252
+
profile := db.Profile{
253
+
Did: did,
254
+
Description: description,
255
+
IncludeBluesky: includeBluesky,
256
+
Location: location,
257
+
Links: links,
258
+
Stats: stats,
259
+
PinnedRepos: pinned,
260
+
}
261
+
262
+
ddb, ok := d.Execer.(*db.DB)
263
+
if !ok {
264
+
return fmt.Errorf("failed to index profile record, invalid db cast")
265
+
}
266
+
267
+
tx, err := ddb.Begin()
268
+
if err != nil {
269
+
return fmt.Errorf("failed to start transaction")
270
+
}
271
+
272
+
err = db.ValidateProfile(tx, &profile)
273
+
if err != nil {
274
+
return fmt.Errorf("invalid profile record")
275
+
}
276
+
277
+
err = db.UpsertProfile(tx, &profile)
278
+
case models.CommitOperationDelete:
279
+
err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey))
280
+
}
281
+
282
+
if err != nil {
283
+
return fmt.Errorf("failed to %s profile record: %w", e.Commit.Operation, err)
284
+
}
285
+
286
+
return nil
287
+
}
+489
appview/knotclient/signer.go
+489
appview/knotclient/signer.go
···
1
+
package knotclient
2
+
3
+
import (
4
+
"bytes"
5
+
"crypto/hmac"
6
+
"crypto/sha256"
7
+
"encoding/hex"
8
+
"encoding/json"
9
+
"fmt"
10
+
"io"
11
+
"log"
12
+
"net/http"
13
+
"net/url"
14
+
"strconv"
15
+
"time"
16
+
17
+
"tangled.sh/tangled.sh/core/types"
18
+
)
19
+
20
+
type SignerTransport struct {
21
+
Secret string
22
+
}
23
+
24
+
func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
25
+
timestamp := time.Now().Format(time.RFC3339)
26
+
mac := hmac.New(sha256.New, []byte(s.Secret))
27
+
message := req.Method + req.URL.Path + timestamp
28
+
mac.Write([]byte(message))
29
+
signature := hex.EncodeToString(mac.Sum(nil))
30
+
req.Header.Set("X-Signature", signature)
31
+
req.Header.Set("X-Timestamp", timestamp)
32
+
return http.DefaultTransport.RoundTrip(req)
33
+
}
34
+
35
+
type SignedClient struct {
36
+
Secret string
37
+
Url *url.URL
38
+
client *http.Client
39
+
}
40
+
41
+
func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) {
42
+
client := &http.Client{
43
+
Timeout: 5 * time.Second,
44
+
Transport: SignerTransport{
45
+
Secret: secret,
46
+
},
47
+
}
48
+
49
+
scheme := "https"
50
+
if dev {
51
+
scheme = "http"
52
+
}
53
+
url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
54
+
if err != nil {
55
+
return nil, err
56
+
}
57
+
58
+
signedClient := &SignedClient{
59
+
Secret: secret,
60
+
client: client,
61
+
Url: url,
62
+
}
63
+
64
+
return signedClient, nil
65
+
}
66
+
67
+
func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
68
+
return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
69
+
}
70
+
71
+
func (s *SignedClient) Init(did string) (*http.Response, error) {
72
+
const (
73
+
Method = "POST"
74
+
Endpoint = "/init"
75
+
)
76
+
77
+
body, _ := json.Marshal(map[string]any{
78
+
"did": did,
79
+
})
80
+
81
+
req, err := s.newRequest(Method, Endpoint, body)
82
+
if err != nil {
83
+
return nil, err
84
+
}
85
+
86
+
return s.client.Do(req)
87
+
}
88
+
89
+
func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) {
90
+
const (
91
+
Method = "PUT"
92
+
Endpoint = "/repo/new"
93
+
)
94
+
95
+
body, _ := json.Marshal(map[string]any{
96
+
"did": did,
97
+
"name": repoName,
98
+
"default_branch": defaultBranch,
99
+
})
100
+
101
+
req, err := s.newRequest(Method, Endpoint, body)
102
+
if err != nil {
103
+
return nil, err
104
+
}
105
+
106
+
return s.client.Do(req)
107
+
}
108
+
109
+
func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
110
+
const (
111
+
Method = "POST"
112
+
Endpoint = "/repo/fork"
113
+
)
114
+
115
+
body, _ := json.Marshal(map[string]any{
116
+
"did": ownerDid,
117
+
"source": source,
118
+
"name": name,
119
+
})
120
+
121
+
req, err := s.newRequest(Method, Endpoint, body)
122
+
if err != nil {
123
+
return nil, err
124
+
}
125
+
126
+
return s.client.Do(req)
127
+
}
128
+
129
+
func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
130
+
const (
131
+
Method = "DELETE"
132
+
Endpoint = "/repo"
133
+
)
134
+
135
+
body, _ := json.Marshal(map[string]any{
136
+
"did": did,
137
+
"name": repoName,
138
+
})
139
+
140
+
req, err := s.newRequest(Method, Endpoint, body)
141
+
if err != nil {
142
+
return nil, err
143
+
}
144
+
145
+
return s.client.Do(req)
146
+
}
147
+
148
+
func (s *SignedClient) AddMember(did string) (*http.Response, error) {
149
+
const (
150
+
Method = "PUT"
151
+
Endpoint = "/member/add"
152
+
)
153
+
154
+
body, _ := json.Marshal(map[string]any{
155
+
"did": did,
156
+
})
157
+
158
+
req, err := s.newRequest(Method, Endpoint, body)
159
+
if err != nil {
160
+
return nil, err
161
+
}
162
+
163
+
return s.client.Do(req)
164
+
}
165
+
166
+
func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) {
167
+
const (
168
+
Method = "PUT"
169
+
)
170
+
endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
171
+
172
+
body, _ := json.Marshal(map[string]any{
173
+
"branch": branch,
174
+
})
175
+
176
+
req, err := s.newRequest(Method, endpoint, body)
177
+
if err != nil {
178
+
return nil, err
179
+
}
180
+
181
+
return s.client.Do(req)
182
+
}
183
+
184
+
func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
185
+
const (
186
+
Method = "POST"
187
+
)
188
+
endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
189
+
190
+
body, _ := json.Marshal(map[string]any{
191
+
"did": memberDid,
192
+
})
193
+
194
+
req, err := s.newRequest(Method, endpoint, body)
195
+
if err != nil {
196
+
return nil, err
197
+
}
198
+
199
+
return s.client.Do(req)
200
+
}
201
+
202
+
func (s *SignedClient) Merge(
203
+
patch []byte,
204
+
ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
205
+
) (*http.Response, error) {
206
+
const (
207
+
Method = "POST"
208
+
)
209
+
endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
210
+
211
+
mr := types.MergeRequest{
212
+
Branch: branch,
213
+
CommitMessage: commitMessage,
214
+
CommitBody: commitBody,
215
+
AuthorName: authorName,
216
+
AuthorEmail: authorEmail,
217
+
Patch: string(patch),
218
+
}
219
+
220
+
body, _ := json.Marshal(mr)
221
+
222
+
req, err := s.newRequest(Method, endpoint, body)
223
+
if err != nil {
224
+
return nil, err
225
+
}
226
+
227
+
return s.client.Do(req)
228
+
}
229
+
230
+
func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
231
+
const (
232
+
Method = "POST"
233
+
)
234
+
endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
235
+
236
+
body, _ := json.Marshal(map[string]any{
237
+
"patch": string(patch),
238
+
"branch": branch,
239
+
})
240
+
241
+
req, err := s.newRequest(Method, endpoint, body)
242
+
if err != nil {
243
+
return nil, err
244
+
}
245
+
246
+
return s.client.Do(req)
247
+
}
248
+
249
+
func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) {
250
+
const (
251
+
Method = "POST"
252
+
)
253
+
endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch))
254
+
255
+
req, err := s.newRequest(Method, endpoint, nil)
256
+
if err != nil {
257
+
return nil, err
258
+
}
259
+
260
+
return s.client.Do(req)
261
+
}
262
+
263
+
type UnsignedClient struct {
264
+
Url *url.URL
265
+
client *http.Client
266
+
}
267
+
268
+
func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) {
269
+
client := &http.Client{
270
+
Timeout: 5 * time.Second,
271
+
}
272
+
273
+
scheme := "https"
274
+
if dev {
275
+
scheme = "http"
276
+
}
277
+
url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
278
+
if err != nil {
279
+
return nil, err
280
+
}
281
+
282
+
unsignedClient := &UnsignedClient{
283
+
client: client,
284
+
Url: url,
285
+
}
286
+
287
+
return unsignedClient, nil
288
+
}
289
+
290
+
func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) {
291
+
reqUrl := us.Url.JoinPath(endpoint)
292
+
293
+
// add query parameters
294
+
if query != nil {
295
+
reqUrl.RawQuery = query.Encode()
296
+
}
297
+
298
+
return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body))
299
+
}
300
+
301
+
func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) {
302
+
const (
303
+
Method = "GET"
304
+
)
305
+
306
+
endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref)
307
+
if ref == "" {
308
+
endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName)
309
+
}
310
+
311
+
req, err := us.newRequest(Method, endpoint, nil, nil)
312
+
if err != nil {
313
+
return nil, err
314
+
}
315
+
316
+
return us.client.Do(req)
317
+
}
318
+
319
+
func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*http.Response, error) {
320
+
const (
321
+
Method = "GET"
322
+
)
323
+
324
+
endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref))
325
+
326
+
query := url.Values{}
327
+
query.Add("page", strconv.Itoa(page))
328
+
query.Add("per_page", strconv.Itoa(60))
329
+
330
+
req, err := us.newRequest(Method, endpoint, query, nil)
331
+
if err != nil {
332
+
return nil, err
333
+
}
334
+
335
+
return us.client.Do(req)
336
+
}
337
+
338
+
func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) {
339
+
const (
340
+
Method = "GET"
341
+
)
342
+
343
+
endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName)
344
+
345
+
req, err := us.newRequest(Method, endpoint, nil, nil)
346
+
if err != nil {
347
+
return nil, err
348
+
}
349
+
350
+
return us.client.Do(req)
351
+
}
352
+
353
+
func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) {
354
+
const (
355
+
Method = "GET"
356
+
)
357
+
358
+
endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName)
359
+
360
+
req, err := us.newRequest(Method, endpoint, nil, nil)
361
+
if err != nil {
362
+
return nil, err
363
+
}
364
+
365
+
resp, err := us.client.Do(req)
366
+
if err != nil {
367
+
return nil, err
368
+
}
369
+
370
+
body, err := io.ReadAll(resp.Body)
371
+
if err != nil {
372
+
return nil, err
373
+
}
374
+
375
+
var result types.RepoTagsResponse
376
+
err = json.Unmarshal(body, &result)
377
+
if err != nil {
378
+
return nil, err
379
+
}
380
+
381
+
return &result, nil
382
+
}
383
+
384
+
func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
385
+
const (
386
+
Method = "GET"
387
+
)
388
+
389
+
endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch))
390
+
391
+
req, err := us.newRequest(Method, endpoint, nil, nil)
392
+
if err != nil {
393
+
return nil, err
394
+
}
395
+
396
+
return us.client.Do(req)
397
+
}
398
+
399
+
func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) {
400
+
const (
401
+
Method = "GET"
402
+
)
403
+
404
+
endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
405
+
406
+
req, err := us.newRequest(Method, endpoint, nil, nil)
407
+
if err != nil {
408
+
return nil, err
409
+
}
410
+
411
+
resp, err := us.client.Do(req)
412
+
if err != nil {
413
+
return nil, err
414
+
}
415
+
defer resp.Body.Close()
416
+
417
+
var defaultBranch types.RepoDefaultBranchResponse
418
+
if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil {
419
+
return nil, err
420
+
}
421
+
422
+
return &defaultBranch, nil
423
+
}
424
+
425
+
func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) {
426
+
const (
427
+
Method = "GET"
428
+
Endpoint = "/capabilities"
429
+
)
430
+
431
+
req, err := us.newRequest(Method, Endpoint, nil, nil)
432
+
if err != nil {
433
+
return nil, err
434
+
}
435
+
436
+
resp, err := us.client.Do(req)
437
+
if err != nil {
438
+
return nil, err
439
+
}
440
+
defer resp.Body.Close()
441
+
442
+
var capabilities types.Capabilities
443
+
if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil {
444
+
return nil, err
445
+
}
446
+
447
+
return &capabilities, nil
448
+
}
449
+
450
+
func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) {
451
+
const (
452
+
Method = "GET"
453
+
)
454
+
455
+
endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2))
456
+
457
+
req, err := us.newRequest(Method, endpoint, nil, nil)
458
+
if err != nil {
459
+
return nil, fmt.Errorf("Failed to create request.")
460
+
}
461
+
462
+
compareResp, err := us.client.Do(req)
463
+
if err != nil {
464
+
return nil, fmt.Errorf("Failed to create request.")
465
+
}
466
+
defer compareResp.Body.Close()
467
+
468
+
switch compareResp.StatusCode {
469
+
case 404:
470
+
case 400:
471
+
return nil, fmt.Errorf("Branch comparisons not supported on this knot.")
472
+
}
473
+
474
+
respBody, err := io.ReadAll(compareResp.Body)
475
+
if err != nil {
476
+
log.Println("failed to compare across branches")
477
+
return nil, fmt.Errorf("Failed to compare branches.")
478
+
}
479
+
defer compareResp.Body.Close()
480
+
481
+
var formatPatchResponse types.RepoFormatPatchResponse
482
+
err = json.Unmarshal(respBody, &formatPatchResponse)
483
+
if err != nil {
484
+
log.Println("failed to unmarshal format-patch response", err)
485
+
return nil, fmt.Errorf("failed to compare branches.")
486
+
}
487
+
488
+
return &formatPatchResponse, nil
489
+
}
+5
-58
appview/middleware/middleware.go
+5
-58
appview/middleware/middleware.go
···
5
5
"log"
6
6
"net/http"
7
7
"strconv"
8
-
"time"
9
8
10
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
11
-
"github.com/bluesky-social/indigo/xrpc"
12
-
"tangled.sh/tangled.sh/core/appview"
13
-
"tangled.sh/tangled.sh/core/appview/auth"
9
+
"tangled.sh/tangled.sh/core/appview/oauth"
14
10
"tangled.sh/tangled.sh/core/appview/pagination"
15
11
)
16
12
17
13
type Middleware func(http.Handler) http.Handler
18
14
19
-
func AuthMiddleware(a *auth.Auth) Middleware {
15
+
func AuthMiddleware(a *oauth.OAuth) Middleware {
20
16
return func(next http.Handler) http.Handler {
21
17
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22
18
redirectFunc := func(w http.ResponseWriter, r *http.Request) {
···
29
25
}
30
26
}
31
27
32
-
session, err := a.GetSession(r)
33
-
if session.IsNew || err != nil {
28
+
_, auth, err := a.GetSession(r)
29
+
if err != nil {
34
30
log.Printf("not logged in, redirecting")
35
31
redirectFunc(w, r)
36
32
return
37
33
}
38
34
39
-
authorized, ok := session.Values[appview.SessionAuthenticated].(bool)
40
-
if !ok || !authorized {
35
+
if !auth {
41
36
log.Printf("not logged in, redirecting")
42
37
redirectFunc(w, r)
43
38
return
44
-
}
45
-
46
-
// refresh if nearing expiry
47
-
// TODO: dedup with /login
48
-
expiryStr := session.Values[appview.SessionExpiry].(string)
49
-
expiry, err := time.Parse(time.RFC3339, expiryStr)
50
-
if err != nil {
51
-
log.Println("invalid expiry time", err)
52
-
redirectFunc(w, r)
53
-
return
54
-
}
55
-
pdsUrl, ok1 := session.Values[appview.SessionPds].(string)
56
-
did, ok2 := session.Values[appview.SessionDid].(string)
57
-
refreshJwt, ok3 := session.Values[appview.SessionRefreshJwt].(string)
58
-
59
-
if !ok1 || !ok2 || !ok3 {
60
-
log.Println("invalid expiry time", err)
61
-
redirectFunc(w, r)
62
-
return
63
-
}
64
-
65
-
if time.Now().After(expiry) {
66
-
log.Println("token expired, refreshing ...")
67
-
68
-
client := xrpc.Client{
69
-
Host: pdsUrl,
70
-
Auth: &xrpc.AuthInfo{
71
-
Did: did,
72
-
AccessJwt: refreshJwt,
73
-
RefreshJwt: refreshJwt,
74
-
},
75
-
}
76
-
atSession, err := comatproto.ServerRefreshSession(r.Context(), &client)
77
-
if err != nil {
78
-
log.Println("failed to refresh session", err)
79
-
redirectFunc(w, r)
80
-
return
81
-
}
82
-
83
-
sessionish := auth.RefreshSessionWrapper{atSession}
84
-
85
-
err = a.StoreSession(r, w, &sessionish, pdsUrl)
86
-
if err != nil {
87
-
log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err)
88
-
return
89
-
}
90
-
91
-
log.Println("successfully refreshed token")
92
39
}
93
40
94
41
next.ServeHTTP(w, r)
+24
appview/oauth/client/oauth_client.go
+24
appview/oauth/client/oauth_client.go
···
1
+
package client
2
+
3
+
import (
4
+
oauth "github.com/haileyok/atproto-oauth-golang"
5
+
"github.com/haileyok/atproto-oauth-golang/helpers"
6
+
)
7
+
8
+
type OAuthClient struct {
9
+
*oauth.Client
10
+
}
11
+
12
+
func NewClient(clientId, clientJwk, redirectUri string) (*OAuthClient, error) {
13
+
k, err := helpers.ParseJWKFromBytes([]byte(clientJwk))
14
+
if err != nil {
15
+
return nil, err
16
+
}
17
+
18
+
cli, err := oauth.NewClient(oauth.ClientArgs{
19
+
ClientId: clientId,
20
+
ClientJwk: k,
21
+
RedirectUri: redirectUri,
22
+
})
23
+
return &OAuthClient{cli}, err
24
+
}
+309
appview/oauth/handler/handler.go
+309
appview/oauth/handler/handler.go
···
1
+
package oauth
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"log"
7
+
"net/http"
8
+
"net/url"
9
+
"strings"
10
+
11
+
"github.com/go-chi/chi/v5"
12
+
"github.com/gorilla/sessions"
13
+
"github.com/haileyok/atproto-oauth-golang/helpers"
14
+
"github.com/lestrrat-go/jwx/v2/jwk"
15
+
"tangled.sh/tangled.sh/core/appview"
16
+
"tangled.sh/tangled.sh/core/appview/db"
17
+
"tangled.sh/tangled.sh/core/appview/knotclient"
18
+
"tangled.sh/tangled.sh/core/appview/middleware"
19
+
"tangled.sh/tangled.sh/core/appview/oauth"
20
+
"tangled.sh/tangled.sh/core/appview/oauth/client"
21
+
"tangled.sh/tangled.sh/core/appview/pages"
22
+
"tangled.sh/tangled.sh/core/rbac"
23
+
)
24
+
25
+
const (
26
+
oauthScope = "atproto transition:generic"
27
+
)
28
+
29
+
type OAuthHandler struct {
30
+
Config *appview.Config
31
+
Pages *pages.Pages
32
+
Resolver *appview.Resolver
33
+
Db *db.DB
34
+
Store *sessions.CookieStore
35
+
OAuth *oauth.OAuth
36
+
Enforcer *rbac.Enforcer
37
+
}
38
+
39
+
func (o *OAuthHandler) Router() http.Handler {
40
+
r := chi.NewRouter()
41
+
42
+
r.Get("/login", o.login)
43
+
r.Post("/login", o.login)
44
+
45
+
r.With(middleware.AuthMiddleware(o.OAuth)).Post("/logout", o.logout)
46
+
47
+
r.Get("/oauth/client-metadata.json", o.clientMetadata)
48
+
r.Get("/oauth/jwks.json", o.jwks)
49
+
r.Get("/oauth/callback", o.callback)
50
+
return r
51
+
}
52
+
53
+
func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) {
54
+
w.Header().Set("Content-Type", "application/json")
55
+
w.WriteHeader(http.StatusOK)
56
+
json.NewEncoder(w).Encode(o.OAuth.ClientMetadata())
57
+
}
58
+
59
+
func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) {
60
+
jwks := o.Config.OAuth.Jwks
61
+
pubKey, err := pubKeyFromJwk(jwks)
62
+
if err != nil {
63
+
log.Printf("error parsing public key: %v", err)
64
+
http.Error(w, err.Error(), http.StatusInternalServerError)
65
+
return
66
+
}
67
+
68
+
response := helpers.CreateJwksResponseObject(pubKey)
69
+
70
+
w.Header().Set("Content-Type", "application/json")
71
+
w.WriteHeader(http.StatusOK)
72
+
json.NewEncoder(w).Encode(response)
73
+
}
74
+
75
+
func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) {
76
+
switch r.Method {
77
+
case http.MethodGet:
78
+
o.Pages.Login(w, pages.LoginParams{})
79
+
case http.MethodPost:
80
+
handle := strings.TrimPrefix(r.FormValue("handle"), "@")
81
+
82
+
resolved, err := o.Resolver.ResolveIdent(r.Context(), handle)
83
+
if err != nil {
84
+
log.Println("failed to resolve handle:", err)
85
+
o.Pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
86
+
return
87
+
}
88
+
self := o.OAuth.ClientMetadata()
89
+
oauthClient, err := client.NewClient(
90
+
self.ClientID,
91
+
o.Config.OAuth.Jwks,
92
+
self.RedirectURIs[0],
93
+
)
94
+
95
+
if err != nil {
96
+
log.Println("failed to create oauth client:", err)
97
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
98
+
return
99
+
}
100
+
101
+
authServer, err := oauthClient.ResolvePdsAuthServer(r.Context(), resolved.PDSEndpoint())
102
+
if err != nil {
103
+
log.Println("failed to resolve auth server:", err)
104
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
105
+
return
106
+
}
107
+
108
+
authMeta, err := oauthClient.FetchAuthServerMetadata(r.Context(), authServer)
109
+
if err != nil {
110
+
log.Println("failed to fetch auth server metadata:", err)
111
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
112
+
return
113
+
}
114
+
115
+
dpopKey, err := helpers.GenerateKey(nil)
116
+
if err != nil {
117
+
log.Println("failed to generate dpop key:", err)
118
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
119
+
return
120
+
}
121
+
122
+
dpopKeyJson, err := json.Marshal(dpopKey)
123
+
if err != nil {
124
+
log.Println("failed to marshal dpop key:", err)
125
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
126
+
return
127
+
}
128
+
129
+
parResp, err := oauthClient.SendParAuthRequest(r.Context(), authServer, authMeta, handle, oauthScope, dpopKey)
130
+
if err != nil {
131
+
log.Println("failed to send par auth request:", err)
132
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
133
+
return
134
+
}
135
+
136
+
err = db.SaveOAuthRequest(o.Db, db.OAuthRequest{
137
+
Did: resolved.DID.String(),
138
+
PdsUrl: resolved.PDSEndpoint(),
139
+
Handle: handle,
140
+
AuthserverIss: authMeta.Issuer,
141
+
PkceVerifier: parResp.PkceVerifier,
142
+
DpopAuthserverNonce: parResp.DpopAuthserverNonce,
143
+
DpopPrivateJwk: string(dpopKeyJson),
144
+
State: parResp.State,
145
+
})
146
+
if err != nil {
147
+
log.Println("failed to save oauth request:", err)
148
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
149
+
return
150
+
}
151
+
152
+
u, _ := url.Parse(authMeta.AuthorizationEndpoint)
153
+
query := url.Values{}
154
+
query.Add("client_id", self.ClientID)
155
+
query.Add("request_uri", parResp.RequestUri)
156
+
u.RawQuery = query.Encode()
157
+
o.Pages.HxRedirect(w, u.String())
158
+
}
159
+
}
160
+
161
+
func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) {
162
+
state := r.FormValue("state")
163
+
164
+
oauthRequest, err := db.GetOAuthRequestByState(o.Db, state)
165
+
if err != nil {
166
+
log.Println("failed to get oauth request:", err)
167
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
168
+
return
169
+
}
170
+
171
+
defer func() {
172
+
err := db.DeleteOAuthRequestByState(o.Db, state)
173
+
if err != nil {
174
+
log.Println("failed to delete oauth request for state:", state, err)
175
+
}
176
+
}()
177
+
178
+
error := r.FormValue("error")
179
+
errorDescription := r.FormValue("error_description")
180
+
if error != "" || errorDescription != "" {
181
+
log.Printf("error: %s, %s", error, errorDescription)
182
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
183
+
return
184
+
}
185
+
186
+
code := r.FormValue("code")
187
+
if code == "" {
188
+
log.Println("missing code for state: ", state)
189
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
190
+
return
191
+
}
192
+
193
+
iss := r.FormValue("iss")
194
+
if iss == "" {
195
+
log.Println("missing iss for state: ", state)
196
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
197
+
return
198
+
}
199
+
200
+
self := o.OAuth.ClientMetadata()
201
+
202
+
oauthClient, err := client.NewClient(
203
+
self.ClientID,
204
+
o.Config.OAuth.Jwks,
205
+
self.RedirectURIs[0],
206
+
)
207
+
208
+
if err != nil {
209
+
log.Println("failed to create oauth client:", err)
210
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
211
+
return
212
+
}
213
+
214
+
jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk))
215
+
if err != nil {
216
+
log.Println("failed to parse jwk:", err)
217
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
218
+
return
219
+
}
220
+
221
+
tokenResp, err := oauthClient.InitialTokenRequest(
222
+
r.Context(),
223
+
code,
224
+
oauthRequest.AuthserverIss,
225
+
oauthRequest.PkceVerifier,
226
+
oauthRequest.DpopAuthserverNonce,
227
+
jwk,
228
+
)
229
+
if err != nil {
230
+
log.Println("failed to get token:", err)
231
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
232
+
return
233
+
}
234
+
235
+
if tokenResp.Scope != oauthScope {
236
+
log.Println("scope doesn't match:", tokenResp.Scope)
237
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
238
+
return
239
+
}
240
+
241
+
err = o.OAuth.SaveSession(w, r, oauthRequest, tokenResp)
242
+
if err != nil {
243
+
log.Println("failed to save session:", err)
244
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
245
+
return
246
+
}
247
+
248
+
log.Println("session saved successfully")
249
+
go o.addToDefaultKnot(oauthRequest.Did)
250
+
251
+
http.Redirect(w, r, "/", http.StatusFound)
252
+
}
253
+
254
+
func (o *OAuthHandler) logout(w http.ResponseWriter, r *http.Request) {
255
+
err := o.OAuth.ClearSession(r, w)
256
+
if err != nil {
257
+
log.Println("failed to clear session:", err)
258
+
http.Redirect(w, r, "/", http.StatusFound)
259
+
return
260
+
}
261
+
262
+
log.Println("session cleared successfully")
263
+
http.Redirect(w, r, "/", http.StatusFound)
264
+
}
265
+
266
+
func pubKeyFromJwk(jwks string) (jwk.Key, error) {
267
+
k, err := helpers.ParseJWKFromBytes([]byte(jwks))
268
+
if err != nil {
269
+
return nil, err
270
+
}
271
+
pubKey, err := k.PublicKey()
272
+
if err != nil {
273
+
return nil, err
274
+
}
275
+
return pubKey, nil
276
+
}
277
+
278
+
func (o *OAuthHandler) addToDefaultKnot(did string) {
279
+
defaultKnot := "knot1.tangled.sh"
280
+
281
+
log.Printf("adding %s to default knot", did)
282
+
err := o.Enforcer.AddMember(defaultKnot, did)
283
+
if err != nil {
284
+
log.Println("failed to add user to knot1.tangled.sh: ", err)
285
+
return
286
+
}
287
+
err = o.Enforcer.E.SavePolicy()
288
+
if err != nil {
289
+
log.Println("failed to add user to knot1.tangled.sh: ", err)
290
+
return
291
+
}
292
+
293
+
secret, err := db.GetRegistrationKey(o.Db, defaultKnot)
294
+
if err != nil {
295
+
log.Println("failed to get registration key for knot1.tangled.sh")
296
+
return
297
+
}
298
+
signedClient, err := knotclient.NewSignedClient(defaultKnot, secret, o.Config.Core.Dev)
299
+
resp, err := signedClient.AddMember(did)
300
+
if err != nil {
301
+
log.Println("failed to add user to knot1.tangled.sh: ", err)
302
+
return
303
+
}
304
+
305
+
if resp.StatusCode != http.StatusNoContent {
306
+
log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)
307
+
return
308
+
}
309
+
}
+268
appview/oauth/oauth.go
+268
appview/oauth/oauth.go
···
1
+
package oauth
2
+
3
+
import (
4
+
"fmt"
5
+
"log"
6
+
"net/http"
7
+
"net/url"
8
+
"time"
9
+
10
+
"github.com/gorilla/sessions"
11
+
oauth "github.com/haileyok/atproto-oauth-golang"
12
+
"github.com/haileyok/atproto-oauth-golang/helpers"
13
+
"tangled.sh/tangled.sh/core/appview"
14
+
"tangled.sh/tangled.sh/core/appview/db"
15
+
"tangled.sh/tangled.sh/core/appview/oauth/client"
16
+
xrpc "tangled.sh/tangled.sh/core/appview/xrpcclient"
17
+
)
18
+
19
+
type OAuthRequest struct {
20
+
ID uint
21
+
AuthserverIss string
22
+
State string
23
+
Did string
24
+
PdsUrl string
25
+
PkceVerifier string
26
+
DpopAuthserverNonce string
27
+
DpopPrivateJwk string
28
+
}
29
+
30
+
type OAuth struct {
31
+
Store *sessions.CookieStore
32
+
Db *db.DB
33
+
Config *appview.Config
34
+
}
35
+
36
+
func NewOAuth(db *db.DB, config *appview.Config) *OAuth {
37
+
return &OAuth{
38
+
Store: sessions.NewCookieStore([]byte(config.Core.CookieSecret)),
39
+
Db: db,
40
+
Config: config,
41
+
}
42
+
}
43
+
44
+
func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq db.OAuthRequest, oresp *oauth.TokenResponse) error {
45
+
// first we save the did in the user session
46
+
userSession, err := o.Store.Get(r, appview.SessionName)
47
+
if err != nil {
48
+
return err
49
+
}
50
+
51
+
userSession.Values[appview.SessionDid] = oreq.Did
52
+
userSession.Values[appview.SessionHandle] = oreq.Handle
53
+
userSession.Values[appview.SessionPds] = oreq.PdsUrl
54
+
userSession.Values[appview.SessionAuthenticated] = true
55
+
err = userSession.Save(r, w)
56
+
if err != nil {
57
+
return fmt.Errorf("error saving user session: %w", err)
58
+
}
59
+
60
+
// then save the whole thing in the db
61
+
session := db.OAuthSession{
62
+
Did: oreq.Did,
63
+
Handle: oreq.Handle,
64
+
PdsUrl: oreq.PdsUrl,
65
+
DpopAuthserverNonce: oreq.DpopAuthserverNonce,
66
+
AuthServerIss: oreq.AuthserverIss,
67
+
DpopPrivateJwk: oreq.DpopPrivateJwk,
68
+
AccessJwt: oresp.AccessToken,
69
+
RefreshJwt: oresp.RefreshToken,
70
+
Expiry: time.Now().Add(time.Duration(oresp.ExpiresIn) * time.Second).Format(time.RFC3339),
71
+
}
72
+
73
+
return db.SaveOAuthSession(o.Db, session)
74
+
}
75
+
76
+
func (o *OAuth) ClearSession(r *http.Request, w http.ResponseWriter) error {
77
+
userSession, err := o.Store.Get(r, appview.SessionName)
78
+
if err != nil || userSession.IsNew {
79
+
return fmt.Errorf("error getting user session (or new session?): %w", err)
80
+
}
81
+
82
+
did := userSession.Values[appview.SessionDid].(string)
83
+
84
+
err = db.DeleteOAuthSessionByDid(o.Db, did)
85
+
if err != nil {
86
+
return fmt.Errorf("error deleting oauth session: %w", err)
87
+
}
88
+
89
+
userSession.Options.MaxAge = -1
90
+
91
+
return userSession.Save(r, w)
92
+
}
93
+
94
+
func (o *OAuth) GetSession(r *http.Request) (*db.OAuthSession, bool, error) {
95
+
userSession, err := o.Store.Get(r, appview.SessionName)
96
+
if err != nil || userSession.IsNew {
97
+
return nil, false, fmt.Errorf("error getting user session (or new session?): %w", err)
98
+
}
99
+
100
+
did := userSession.Values[appview.SessionDid].(string)
101
+
auth := userSession.Values[appview.SessionAuthenticated].(bool)
102
+
103
+
session, err := db.GetOAuthSessionByDid(o.Db, did)
104
+
if err != nil {
105
+
return nil, false, fmt.Errorf("error getting oauth session: %w", err)
106
+
}
107
+
108
+
expiry, err := time.Parse(time.RFC3339, session.Expiry)
109
+
if err != nil {
110
+
return nil, false, fmt.Errorf("error parsing expiry time: %w", err)
111
+
}
112
+
if expiry.Sub(time.Now()) <= 5*time.Minute {
113
+
privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk))
114
+
if err != nil {
115
+
return nil, false, err
116
+
}
117
+
118
+
self := o.ClientMetadata()
119
+
120
+
oauthClient, err := client.NewClient(
121
+
self.ClientID,
122
+
o.Config.OAuth.Jwks,
123
+
self.RedirectURIs[0],
124
+
)
125
+
126
+
if err != nil {
127
+
return nil, false, err
128
+
}
129
+
130
+
resp, err := oauthClient.RefreshTokenRequest(r.Context(), session.RefreshJwt, session.AuthServerIss, session.DpopAuthserverNonce, privateJwk)
131
+
if err != nil {
132
+
return nil, false, err
133
+
}
134
+
135
+
newExpiry := time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second).Format(time.RFC3339)
136
+
err = db.RefreshOAuthSession(o.Db, did, resp.AccessToken, resp.RefreshToken, newExpiry)
137
+
if err != nil {
138
+
return nil, false, fmt.Errorf("error refreshing oauth session: %w", err)
139
+
}
140
+
141
+
// update the current session
142
+
session.AccessJwt = resp.AccessToken
143
+
session.RefreshJwt = resp.RefreshToken
144
+
session.DpopAuthserverNonce = resp.DpopAuthserverNonce
145
+
session.Expiry = newExpiry
146
+
}
147
+
148
+
return session, auth, nil
149
+
}
150
+
151
+
type User struct {
152
+
Handle string
153
+
Did string
154
+
Pds string
155
+
}
156
+
157
+
func (a *OAuth) GetUser(r *http.Request) *User {
158
+
clientSession, err := a.Store.Get(r, appview.SessionName)
159
+
160
+
if err != nil || clientSession.IsNew {
161
+
return nil
162
+
}
163
+
164
+
return &User{
165
+
Handle: clientSession.Values[appview.SessionHandle].(string),
166
+
Did: clientSession.Values[appview.SessionDid].(string),
167
+
Pds: clientSession.Values[appview.SessionPds].(string),
168
+
}
169
+
}
170
+
171
+
func (a *OAuth) GetDid(r *http.Request) string {
172
+
clientSession, err := a.Store.Get(r, appview.SessionName)
173
+
174
+
if err != nil || clientSession.IsNew {
175
+
return ""
176
+
}
177
+
178
+
return clientSession.Values[appview.SessionDid].(string)
179
+
}
180
+
181
+
func (o *OAuth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) {
182
+
session, auth, err := o.GetSession(r)
183
+
if err != nil {
184
+
return nil, fmt.Errorf("error getting session: %w", err)
185
+
}
186
+
if !auth {
187
+
return nil, fmt.Errorf("not authorized")
188
+
}
189
+
190
+
client := &oauth.XrpcClient{
191
+
OnDpopPdsNonceChanged: func(did, newNonce string) {
192
+
err := db.UpdateDpopPdsNonce(o.Db, did, newNonce)
193
+
if err != nil {
194
+
log.Printf("error updating dpop pds nonce: %v", err)
195
+
}
196
+
},
197
+
}
198
+
199
+
privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk))
200
+
if err != nil {
201
+
return nil, fmt.Errorf("error parsing private jwk: %w", err)
202
+
}
203
+
204
+
xrpcClient := xrpc.NewClient(client, &oauth.XrpcAuthedRequestArgs{
205
+
Did: session.Did,
206
+
PdsUrl: session.PdsUrl,
207
+
DpopPdsNonce: session.PdsUrl,
208
+
AccessToken: session.AccessJwt,
209
+
Issuer: session.AuthServerIss,
210
+
DpopPrivateJwk: privateJwk,
211
+
})
212
+
213
+
return xrpcClient, nil
214
+
}
215
+
216
+
type ClientMetadata struct {
217
+
ClientID string `json:"client_id"`
218
+
ClientName string `json:"client_name"`
219
+
SubjectType string `json:"subject_type"`
220
+
ClientURI string `json:"client_uri"`
221
+
RedirectURIs []string `json:"redirect_uris"`
222
+
GrantTypes []string `json:"grant_types"`
223
+
ResponseTypes []string `json:"response_types"`
224
+
ApplicationType string `json:"application_type"`
225
+
DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"`
226
+
JwksURI string `json:"jwks_uri"`
227
+
Scope string `json:"scope"`
228
+
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
229
+
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
230
+
}
231
+
232
+
func (o *OAuth) ClientMetadata() ClientMetadata {
233
+
makeRedirectURIs := func(c string) []string {
234
+
return []string{fmt.Sprintf("%s/oauth/callback", c)}
235
+
}
236
+
237
+
clientURI := o.Config.Core.AppviewHost
238
+
clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI)
239
+
redirectURIs := makeRedirectURIs(clientURI)
240
+
241
+
if o.Config.Core.Dev {
242
+
clientURI = fmt.Sprintf("http://127.0.0.1:3000")
243
+
redirectURIs = makeRedirectURIs(clientURI)
244
+
245
+
query := url.Values{}
246
+
query.Add("redirect_uri", redirectURIs[0])
247
+
query.Add("scope", "atproto transition:generic")
248
+
clientID = fmt.Sprintf("http://localhost?%s", query.Encode())
249
+
}
250
+
251
+
jwksURI := fmt.Sprintf("%s/oauth/jwks.json", clientURI)
252
+
253
+
return ClientMetadata{
254
+
ClientID: clientID,
255
+
ClientName: "Tangled",
256
+
SubjectType: "public",
257
+
ClientURI: clientURI,
258
+
RedirectURIs: redirectURIs,
259
+
GrantTypes: []string{"authorization_code", "refresh_token"},
260
+
ResponseTypes: []string{"code"},
261
+
ApplicationType: "web",
262
+
DpopBoundAccessTokens: true,
263
+
JwksURI: jwksURI,
264
+
Scope: "atproto transition:generic",
265
+
TokenEndpointAuthMethod: "private_key_jwt",
266
+
TokenEndpointAuthSigningAlg: "ES256",
267
+
}
268
+
}
+2
-1
appview/pages/funcmap.go
+2
-1
appview/pages/funcmap.go
···
13
13
"time"
14
14
15
15
"github.com/dustin/go-humanize"
16
+
"github.com/microcosm-cc/bluemonday"
16
17
"tangled.sh/tangled.sh/core/appview/filetree"
17
18
"tangled.sh/tangled.sh/core/appview/pages/markup"
18
19
)
···
144
145
},
145
146
"markdown": func(text string) template.HTML {
146
147
rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault}
147
-
return template.HTML(rctx.RenderMarkdown(text))
148
+
return template.HTML(bluemonday.UGCPolicy().Sanitize(rctx.RenderMarkdown(text)))
148
149
},
149
150
"isNil": func(t any) bool {
150
151
// returns false for other "zero" values
+2
appview/pages/markup/markdown.go
+2
appview/pages/markup/markdown.go
···
10
10
"github.com/yuin/goldmark/ast"
11
11
"github.com/yuin/goldmark/extension"
12
12
"github.com/yuin/goldmark/parser"
13
+
"github.com/yuin/goldmark/renderer/html"
13
14
"github.com/yuin/goldmark/text"
14
15
"github.com/yuin/goldmark/util"
15
16
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
···
41
42
goldmark.WithParserOptions(
42
43
parser.WithAutoHeadingID(),
43
44
),
45
+
goldmark.WithRendererOptions(html.WithUnsafe()),
44
46
)
45
47
46
48
if rctx != nil {
+82
-43
appview/pages/pages.go
+82
-43
appview/pages/pages.go
···
16
16
"strings"
17
17
18
18
"tangled.sh/tangled.sh/core/appview"
19
-
"tangled.sh/tangled.sh/core/appview/auth"
20
19
"tangled.sh/tangled.sh/core/appview/db"
20
+
"tangled.sh/tangled.sh/core/appview/oauth"
21
21
"tangled.sh/tangled.sh/core/appview/pages/markup"
22
22
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
23
23
"tangled.sh/tangled.sh/core/appview/pagination"
···
48
48
func NewPages(config *appview.Config) *Pages {
49
49
// initialized with safe defaults, can be overriden per use
50
50
rctx := &markup.RenderContext{
51
-
IsDev: config.Dev,
52
-
CamoUrl: config.CamoHost,
53
-
CamoSecret: config.CamoSharedSecret,
51
+
IsDev: config.Core.Dev,
52
+
CamoUrl: config.Camo.Host,
53
+
CamoSecret: config.Camo.SharedSecret,
54
54
}
55
55
56
56
p := &Pages{
57
57
t: make(map[string]*template.Template),
58
-
dev: config.Dev,
58
+
dev: config.Core.Dev,
59
59
embedFS: Files,
60
60
rctx: rctx,
61
61
templateDir: "appview/pages",
···
250
250
}
251
251
252
252
type TimelineParams struct {
253
-
LoggedInUser *auth.User
253
+
LoggedInUser *oauth.User
254
254
Timeline []db.TimelineEvent
255
255
DidHandleMap map[string]string
256
256
}
···
260
260
}
261
261
262
262
type SettingsParams struct {
263
-
LoggedInUser *auth.User
263
+
LoggedInUser *oauth.User
264
264
PubKeys []db.PublicKey
265
265
Emails []db.Email
266
266
}
···
270
270
}
271
271
272
272
type KnotsParams struct {
273
-
LoggedInUser *auth.User
273
+
LoggedInUser *oauth.User
274
274
Registrations []db.Registration
275
275
}
276
276
···
279
279
}
280
280
281
281
type KnotParams struct {
282
-
LoggedInUser *auth.User
282
+
LoggedInUser *oauth.User
283
283
DidHandleMap map[string]string
284
284
Registration *db.Registration
285
285
Members []string
···
291
291
}
292
292
293
293
type NewRepoParams struct {
294
-
LoggedInUser *auth.User
294
+
LoggedInUser *oauth.User
295
295
Knots []string
296
296
}
297
297
···
300
300
}
301
301
302
302
type ForkRepoParams struct {
303
-
LoggedInUser *auth.User
303
+
LoggedInUser *oauth.User
304
304
Knots []string
305
305
RepoInfo repoinfo.RepoInfo
306
306
}
···
310
310
}
311
311
312
312
type ProfilePageParams struct {
313
-
LoggedInUser *auth.User
314
-
UserDid string
315
-
UserHandle string
313
+
LoggedInUser *oauth.User
316
314
Repos []db.Repo
317
315
CollaboratingRepos []db.Repo
318
-
ProfileStats ProfileStats
319
-
FollowStatus db.FollowStatus
320
-
AvatarUri string
321
316
ProfileTimeline *db.ProfileTimeline
317
+
Card ProfileCard
322
318
323
319
DidHandleMap map[string]string
324
320
}
325
321
326
-
type ProfileStats struct {
327
-
Followers int
328
-
Following int
322
+
type ProfileCard struct {
323
+
UserDid string
324
+
UserHandle string
325
+
FollowStatus db.FollowStatus
326
+
AvatarUri string
327
+
Followers int
328
+
Following int
329
+
330
+
Profile *db.Profile
329
331
}
330
332
331
333
func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
332
334
return p.execute("user/profile", w, params)
333
335
}
334
336
337
+
type ReposPageParams struct {
338
+
LoggedInUser *oauth.User
339
+
Repos []db.Repo
340
+
Card ProfileCard
341
+
342
+
DidHandleMap map[string]string
343
+
}
344
+
345
+
func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error {
346
+
return p.execute("user/repos", w, params)
347
+
}
348
+
335
349
type FollowFragmentParams struct {
336
350
UserDid string
337
351
FollowStatus db.FollowStatus
···
341
355
return p.executePlain("user/fragments/follow", w, params)
342
356
}
343
357
358
+
type EditBioParams struct {
359
+
LoggedInUser *oauth.User
360
+
Profile *db.Profile
361
+
}
362
+
363
+
func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error {
364
+
return p.executePlain("user/fragments/editBio", w, params)
365
+
}
366
+
367
+
type EditPinsParams struct {
368
+
LoggedInUser *oauth.User
369
+
Profile *db.Profile
370
+
AllRepos []PinnedRepo
371
+
DidHandleMap map[string]string
372
+
}
373
+
374
+
type PinnedRepo struct {
375
+
IsPinned bool
376
+
db.Repo
377
+
}
378
+
379
+
func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error {
380
+
return p.executePlain("user/fragments/editPins", w, params)
381
+
}
382
+
344
383
type RepoActionsFragmentParams struct {
345
384
IsStarred bool
346
385
RepoAt syntax.ATURI
···
364
403
}
365
404
366
405
type RepoIndexParams struct {
367
-
LoggedInUser *auth.User
406
+
LoggedInUser *oauth.User
368
407
RepoInfo repoinfo.RepoInfo
369
408
Active string
370
409
TagMap map[string][]string
···
405
444
}
406
445
407
446
type RepoLogParams struct {
408
-
LoggedInUser *auth.User
447
+
LoggedInUser *oauth.User
409
448
RepoInfo repoinfo.RepoInfo
410
449
TagMap map[string][]string
411
450
types.RepoLogResponse
···
419
458
}
420
459
421
460
type RepoCommitParams struct {
422
-
LoggedInUser *auth.User
461
+
LoggedInUser *oauth.User
423
462
RepoInfo repoinfo.RepoInfo
424
463
Active string
425
464
EmailToDidOrHandle map[string]string
···
433
472
}
434
473
435
474
type RepoTreeParams struct {
436
-
LoggedInUser *auth.User
475
+
LoggedInUser *oauth.User
437
476
RepoInfo repoinfo.RepoInfo
438
477
Active string
439
478
BreadCrumbs [][]string
···
469
508
}
470
509
471
510
type RepoBranchesParams struct {
472
-
LoggedInUser *auth.User
511
+
LoggedInUser *oauth.User
473
512
RepoInfo repoinfo.RepoInfo
474
513
Active string
475
514
types.RepoBranchesResponse
···
481
520
}
482
521
483
522
type RepoTagsParams struct {
484
-
LoggedInUser *auth.User
523
+
LoggedInUser *oauth.User
485
524
RepoInfo repoinfo.RepoInfo
486
525
Active string
487
526
types.RepoTagsResponse
···
495
534
}
496
535
497
536
type RepoArtifactParams struct {
498
-
LoggedInUser *auth.User
537
+
LoggedInUser *oauth.User
499
538
RepoInfo repoinfo.RepoInfo
500
539
Artifact db.Artifact
501
540
}
···
505
544
}
506
545
507
546
type RepoBlobParams struct {
508
-
LoggedInUser *auth.User
547
+
LoggedInUser *oauth.User
509
548
RepoInfo repoinfo.RepoInfo
510
549
Active string
511
550
BreadCrumbs [][]string
···
523
562
case markup.FormatMarkdown:
524
563
p.rctx.RepoInfo = params.RepoInfo
525
564
p.rctx.RendererType = markup.RendererTypeRepoMarkdown
526
-
params.RenderedContents = template.HTML(p.rctx.RenderMarkdown(params.Contents))
565
+
params.RenderedContents = template.HTML(bluemonday.UGCPolicy().Sanitize(p.rctx.RenderMarkdown(params.Contents)))
527
566
}
528
567
}
529
568
···
567
606
}
568
607
569
608
type RepoSettingsParams struct {
570
-
LoggedInUser *auth.User
609
+
LoggedInUser *oauth.User
571
610
RepoInfo repoinfo.RepoInfo
572
611
Collaborators []Collaborator
573
612
Active string
···
583
622
}
584
623
585
624
type RepoIssuesParams struct {
586
-
LoggedInUser *auth.User
625
+
LoggedInUser *oauth.User
587
626
RepoInfo repoinfo.RepoInfo
588
627
Active string
589
628
Issues []db.Issue
···
598
637
}
599
638
600
639
type RepoSingleIssueParams struct {
601
-
LoggedInUser *auth.User
640
+
LoggedInUser *oauth.User
602
641
RepoInfo repoinfo.RepoInfo
603
642
Active string
604
643
Issue db.Issue
···
620
659
}
621
660
622
661
type RepoNewIssueParams struct {
623
-
LoggedInUser *auth.User
662
+
LoggedInUser *oauth.User
624
663
RepoInfo repoinfo.RepoInfo
625
664
Active string
626
665
}
···
631
670
}
632
671
633
672
type EditIssueCommentParams struct {
634
-
LoggedInUser *auth.User
673
+
LoggedInUser *oauth.User
635
674
RepoInfo repoinfo.RepoInfo
636
675
Issue *db.Issue
637
676
Comment *db.Comment
···
642
681
}
643
682
644
683
type SingleIssueCommentParams struct {
645
-
LoggedInUser *auth.User
684
+
LoggedInUser *oauth.User
646
685
DidHandleMap map[string]string
647
686
RepoInfo repoinfo.RepoInfo
648
687
Issue *db.Issue
···
654
693
}
655
694
656
695
type RepoNewPullParams struct {
657
-
LoggedInUser *auth.User
696
+
LoggedInUser *oauth.User
658
697
RepoInfo repoinfo.RepoInfo
659
698
Branches []types.Branch
660
699
Active string
···
666
705
}
667
706
668
707
type RepoPullsParams struct {
669
-
LoggedInUser *auth.User
708
+
LoggedInUser *oauth.User
670
709
RepoInfo repoinfo.RepoInfo
671
710
Pulls []*db.Pull
672
711
Active string
···
698
737
}
699
738
700
739
type RepoSinglePullParams struct {
701
-
LoggedInUser *auth.User
740
+
LoggedInUser *oauth.User
702
741
RepoInfo repoinfo.RepoInfo
703
742
Active string
704
743
DidHandleMap map[string]string
···
713
752
}
714
753
715
754
type RepoPullPatchParams struct {
716
-
LoggedInUser *auth.User
755
+
LoggedInUser *oauth.User
717
756
DidHandleMap map[string]string
718
757
RepoInfo repoinfo.RepoInfo
719
758
Pull *db.Pull
···
728
767
}
729
768
730
769
type RepoPullInterdiffParams struct {
731
-
LoggedInUser *auth.User
770
+
LoggedInUser *oauth.User
732
771
DidHandleMap map[string]string
733
772
RepoInfo repoinfo.RepoInfo
734
773
Pull *db.Pull
···
778
817
}
779
818
780
819
type PullResubmitParams struct {
781
-
LoggedInUser *auth.User
820
+
LoggedInUser *oauth.User
782
821
RepoInfo repoinfo.RepoInfo
783
822
Pull *db.Pull
784
823
SubmissionId int
···
789
828
}
790
829
791
830
type PullActionsParams struct {
792
-
LoggedInUser *auth.User
831
+
LoggedInUser *oauth.User
793
832
RepoInfo repoinfo.RepoInfo
794
833
Pull *db.Pull
795
834
RoundNumber int
···
802
841
}
803
842
804
843
type PullNewCommentParams struct {
805
-
LoggedInUser *auth.User
844
+
LoggedInUser *oauth.User
806
845
RepoInfo repoinfo.RepoInfo
807
846
Pull *db.Pull
808
847
RoundNumber int
+1
appview/pages/templates/layouts/base.html
+1
appview/pages/templates/layouts/base.html
···
7
7
name="viewport"
8
8
content="width=device-width, initial-scale=1.0"
9
9
/>
10
+
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
10
11
<script src="/static/htmx.min.js"></script>
11
12
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
12
13
<title>{{ block "title" . }}{{ end }} ยท tangled</title>
+1
-1
appview/pages/templates/repo/fragments/cloneInstructions.html
+1
-1
appview/pages/templates/repo/fragments/cloneInstructions.html
···
4
4
{{ $knot = "tangled.sh" }}
5
5
{{ end }}
6
6
<section
7
-
class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto flex flex-col gap-4"
7
+
class="mt-4 p-6 rounded drop-shadow-sm bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto flex flex-col gap-4"
8
8
>
9
9
<div class="flex flex-col gap-2">
10
10
<strong>push</strong>
+6
-6
appview/pages/templates/repo/fragments/filetree.html
+6
-6
appview/pages/templates/repo/fragments/filetree.html
···
2
2
{{ if and .Name .IsDirectory }}
3
3
<details open>
4
4
<summary class="cursor-pointer list-none pt-1">
5
-
<span class="inline-flex items-center gap-2 ">
6
-
{{ i "folder" "w-3 h-3 fill-current" }}
7
-
<span class="text-black dark:text-white">{{ .Name }}</span>
5
+
<span class="tree-directory inline-flex items-center gap-2 ">
6
+
{{ i "folder" "size-4 fill-current" }}
7
+
<span class="filename text-black dark:text-white">{{ .Name }}</span>
8
8
</span>
9
9
</summary>
10
10
<div class="ml-1 pl-4 border-l border-gray-200 dark:border-gray-700">
···
14
14
</div>
15
15
</details>
16
16
{{ else if .Name }}
17
-
<div class="flex items-center gap-2 pt-1">
18
-
{{ i "file" "w-3 h-3" }}
19
-
<a href="#file-{{ .Path }}" class="text-black dark:text-white no-underline hover:underline">{{ .Name }}</a>
17
+
<div class="tree-file flex items-center gap-2 pt-1">
18
+
{{ i "file" "size-4" }}
19
+
<a href="#file-{{ .Path }}" class="filename text-black dark:text-white no-underline hover:underline">{{ .Name }}</a>
20
20
</div>
21
21
{{ else }}
22
22
{{ range $child := .Children }}
+13
-12
appview/pages/templates/repo/fragments/repoActions.html
+13
-12
appview/pages/templates/repo/fragments/repoActions.html
···
2
2
<div class="flex items-center gap-2 z-auto">
3
3
<button
4
4
id="starBtn"
5
-
class="btn disabled:opacity-50 disabled:cursor-not-allowed"
5
+
class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group"
6
6
{{ if .IsStarred }}
7
7
hx-delete="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}"
8
8
{{ else }}
···
14
14
hx-swap="outerHTML"
15
15
hx-disabled-elt="#starBtn"
16
16
>
17
-
<div class="flex gap-2 items-center">
18
-
{{ if .IsStarred }}
19
-
{{ i "star" "w-4 h-4 fill-current" }}
20
-
{{ else }}
21
-
{{ i "star" "w-4 h-4" }}
22
-
{{ end }}
23
-
<span class="text-sm">
24
-
{{ .Stats.StarCount }}
25
-
</span>
26
-
</div>
17
+
{{ if .IsStarred }}
18
+
{{ i "star" "w-4 h-4 fill-current" }}
19
+
{{ else }}
20
+
{{ i "star" "w-4 h-4" }}
21
+
{{ end }}
22
+
<span class="text-sm">
23
+
{{ .Stats.StarCount }}
24
+
</span>
25
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
27
26
</button>
28
27
{{ if .DisableFork }}
29
28
<button
···
36
35
</button>
37
36
{{ else }}
38
37
<a
39
-
class="btn text-sm no-underline hover:no-underline flex items-center gap-2"
38
+
class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group"
39
+
hx-boost="true"
40
40
href="/{{ .FullName }}/fork"
41
41
>
42
42
{{ i "git-fork" "w-4 h-4" }}
43
43
fork
44
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
44
45
</a>
45
46
{{ end }}
46
47
</div>
+3
-3
appview/pages/templates/repo/index.html
+3
-3
appview/pages/templates/repo/index.html
···
103
103
class="{{ $linkstyle }}"
104
104
>
105
105
<div class="flex items-center gap-2">
106
-
{{ i "folder" "w-3 h-3 fill-current" }}
106
+
{{ i "folder" "size-4 fill-current" }}
107
107
{{ .Name }}
108
108
</div>
109
109
</a>
···
125
125
class="{{ $linkstyle }}"
126
126
>
127
127
<div class="flex items-center gap-2">
128
-
{{ i "file" "w-3 h-3" }}{{ .Name }}
128
+
{{ i "file" "size-4" }}{{ .Name }}
129
129
</div>
130
130
</a>
131
131
···
320
320
{{ define "repoAfter" }}
321
321
{{- if .HTMLReadme }}
322
322
<section
323
-
class="p-6 mt-4 rounded-br rounded-bl bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto {{ if not .Raw }}
323
+
class="p-6 mt-4 rounded-br rounded-bl bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm w-full mx-auto overflow-auto {{ if not .Raw }}
324
324
prose dark:prose-invert dark:[&_pre]:bg-gray-900
325
325
dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900
326
326
dark:[&_pre]:border dark:[&_pre]:border-gray-700
+26
-17
appview/pages/templates/repo/issues/issues.html
+26
-17
appview/pages/templates/repo/issues/issues.html
···
1
1
{{ define "title" }}issues · {{ .RepoInfo.FullName }}{{ end }}
2
2
3
3
{{ define "repoContent" }}
4
-
<div class="flex justify-between items-center">
5
-
<p>
6
-
filtering
7
-
<select class="border p-1 bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700" onchange="window.location.href = '/{{ .RepoInfo.FullName }}/issues?state=' + this.value">
8
-
<option value="open" {{ if .FilteringByOpen }}selected{{ end }}>open ({{ .RepoInfo.Stats.IssueCount.Open }})</option>
9
-
<option value="closed" {{ if not .FilteringByOpen }}selected{{ end }}>closed ({{ .RepoInfo.Stats.IssueCount.Closed }})</option>
10
-
</select>
11
-
issues
12
-
</p>
13
-
<a
14
-
href="/{{ .RepoInfo.FullName }}/issues/new"
15
-
class="btn text-sm flex items-center gap-2 no-underline hover:no-underline">
16
-
{{ i "circle-plus" "w-4 h-4" }}
17
-
<span>new</span>
18
-
</a>
19
-
</div>
20
-
<div class="error" id="issues"></div>
4
+
<div class="flex justify-between items-center gap-4">
5
+
<div class="flex gap-4">
6
+
<a
7
+
href="?state=open"
8
+
class="flex items-center gap-2 {{ if .FilteringByOpen }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
9
+
>
10
+
{{ i "circle-dot" "w-4 h-4" }}
11
+
<span>{{ .RepoInfo.Stats.IssueCount.Open }} open</span>
12
+
</a>
13
+
<a
14
+
href="?state=closed"
15
+
class="flex items-center gap-2 {{ if not .FilteringByOpen }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
16
+
>
17
+
{{ i "ban" "w-4 h-4" }}
18
+
<span>{{ .RepoInfo.Stats.IssueCount.Closed }} closed</span>
19
+
</a>
20
+
</div>
21
+
<a
22
+
href="/{{ .RepoInfo.FullName }}/issues/new"
23
+
class="btn text-sm flex items-center justify-center gap-2 no-underline hover:no-underline"
24
+
>
25
+
{{ i "circle-plus" "w-4 h-4" }}
26
+
<span>new</span>
27
+
</a>
28
+
</div>
29
+
<div class="error" id="issues"></div>
21
30
{{ end }}
22
31
23
32
{{ define "repoAfter" }}
+7
-2
appview/pages/templates/repo/new.html
+7
-2
appview/pages/templates/repo/new.html
···
5
5
<p class="text-xl font-bold dark:text-white">Create a new repository</p>
6
6
</div>
7
7
<div class="p-6 bg-white dark:bg-gray-800 drop-shadow-sm rounded">
8
-
<form hx-post="/repo/new" class="space-y-12" hx-swap="none">
8
+
<form hx-post="/repo/new" class="space-y-12" hx-swap="none" hx-indicator="#spinner">
9
9
<div class="space-y-2">
10
10
<label for="name" class="-mb-1 dark:text-white">Repository name</label>
11
11
<input
···
60
60
</fieldset>
61
61
62
62
<div class="space-y-2">
63
-
<button type="submit" class="btn">create repo</button>
63
+
<button type="submit" class="btn flex gap-2 items-center">
64
+
create repo
65
+
<span id="spinner" class="group">
66
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
67
+
</span>
68
+
</button>
64
69
<div id="repo" class="error"></div>
65
70
</div>
66
71
</form>
+14
-9
appview/pages/templates/repo/pulls/fragments/pullActions.html
+14
-9
appview/pages/templates/repo/pulls/fragments/pullActions.html
···
17
17
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment"
18
18
hx-target="#actions-{{$roundNumber}}"
19
19
hx-swap="outerHtml"
20
-
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline">
20
+
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group">
21
21
{{ i "message-square-plus" "w-4 h-4" }}
22
22
<span>comment</span>
23
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
23
24
</button>
24
25
{{ if and $isPushAllowed $isOpen $isLastRound }}
25
26
{{ $disabled := "" }}
···
30
31
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
31
32
hx-swap="none"
32
33
hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?"
33
-
class="btn p-2 flex items-center gap-2" {{ $disabled }}>
34
+
class="btn p-2 flex items-center gap-2 group" {{ $disabled }}>
34
35
{{ i "git-merge" "w-4 h-4" }}
35
-
<span>merge</span>
36
+
<span>merge</span>
37
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
36
38
</button>
37
39
{{ end }}
38
40
···
51
53
{{ end }}
52
54
53
55
hx-disabled-elt="#resubmitBtn"
54
-
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }}
56
+
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }}
55
57
56
58
{{ if $disabled }}
57
59
title="Update this branch to resubmit this pull request"
···
59
61
title="Resubmit this pull request"
60
62
{{ end }}
61
63
>
62
-
{{ i "rotate-ccw" "w-4 h-4" }}
64
+
{{ i "rotate-ccw" "w-4 h-4" }}
63
65
<span>resubmit</span>
66
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
64
67
</button>
65
68
{{ end }}
66
69
···
68
71
<button
69
72
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close"
70
73
hx-swap="none"
71
-
class="btn p-2 flex items-center gap-2">
72
-
{{ i "ban" "w-4 h-4" }}
74
+
class="btn p-2 flex items-center gap-2 group">
75
+
{{ i "ban" "w-4 h-4" }}
73
76
<span>close</span>
77
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
74
78
</button>
75
79
{{ end }}
76
80
···
78
82
<button
79
83
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen"
80
84
hx-swap="none"
81
-
class="btn p-2 flex items-center gap-2">
82
-
{{ i "refresh-ccw-dot" "w-4 h-4" }}
85
+
class="btn p-2 flex items-center gap-2 group">
86
+
{{ i "refresh-ccw-dot" "w-4 h-4" }}
83
87
<span>reopen</span>
88
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
84
89
</button>
85
90
{{ end }}
86
91
</div>
+9
-11
appview/pages/templates/repo/pulls/fragments/pullHeader.html
+9
-11
appview/pages/templates/repo/pulls/fragments/pullHeader.html
···
42
42
</span>
43
43
</span>
44
44
{{ if not .Pull.IsPatchBased }}
45
-
<span>from
46
-
{{ if .Pull.IsForkBased }}
47
-
{{ if .Pull.PullSource.Repo }}
48
-
<a href="/{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}</a>
49
-
{{ else }}
50
-
<span class="italic">[deleted fork]</span>
51
-
{{ end }}
52
-
{{ end }}
53
-
45
+
from
54
46
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
55
-
{{ .Pull.PullSource.Branch }}
47
+
{{ if .Pull.IsForkBased }}
48
+
{{ if .Pull.PullSource.Repo }}
49
+
<a href="/{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}</a>:
50
+
{{- else -}}
51
+
<span class="italic">[deleted fork]</span>
52
+
{{- end -}}
53
+
{{- end -}}
54
+
{{- .Pull.PullSource.Branch -}}
56
55
</span>
57
-
</span>
58
56
{{ end }}
59
57
</span>
60
58
</div>
+1
-1
appview/pages/templates/repo/pulls/new.html
+1
-1
appview/pages/templates/repo/pulls/new.html
···
18
18
>
19
19
<option disabled selected>target branch</option>
20
20
{{ range .Branches }}
21
-
<option value="{{ .Reference.Name }}" class="py-1">
21
+
<option value="{{ .Reference.Name }}" class="py-1" {{if .IsDefault}}selected{{end}}>
22
22
{{ .Reference.Name }}
23
23
</option>
24
24
{{ end }}
+8
-4
appview/pages/templates/repo/pulls/pull.html
+8
-4
appview/pages/templates/repo/pulls/pull.html
···
51
51
</span>
52
52
</div>
53
53
54
-
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2"
54
+
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
55
55
hx-boost="true"
56
56
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}">
57
-
{{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">view patch</span>
57
+
{{ i "file-diff" "w-4 h-4" }}
58
+
<span class="hidden md:inline">diff</span>
59
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
58
60
</a>
59
61
{{ if not (eq .RoundNumber 0) }}
60
-
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2"
62
+
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
61
63
hx-boost="true"
62
64
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff">
63
-
{{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">interdiff</span>
65
+
{{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }}
66
+
<span class="hidden md:inline">interdiff</span>
67
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
64
68
</a>
65
69
<span id="interdiff-error-{{.RoundNumber}}"></span>
66
70
{{ end }}
+32
-29
appview/pages/templates/repo/pulls/pulls.html
+32
-29
appview/pages/templates/repo/pulls/pulls.html
···
2
2
3
3
{{ define "repoContent" }}
4
4
<div class="flex justify-between items-center">
5
-
<p class="dark:text-white">
6
-
filtering
7
-
<select
8
-
class="border p-1 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600 dark:text-white"
9
-
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/pulls?state=' + this.value"
5
+
<div class="flex gap-4">
6
+
<a
7
+
href="?state=open"
8
+
class="flex items-center gap-2 {{ if .FilteringBy.IsOpen }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
9
+
>
10
+
{{ i "git-pull-request" "w-4 h-4" }}
11
+
<span>{{ .RepoInfo.Stats.PullCount.Open }} open</span>
12
+
</a>
13
+
<a
14
+
href="?state=merged"
15
+
class="flex items-center gap-2 {{ if .FilteringBy.IsMerged }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
16
+
>
17
+
{{ i "git-merge" "w-4 h-4" }}
18
+
<span>{{ .RepoInfo.Stats.PullCount.Merged }} merged</span>
19
+
</a>
20
+
<a
21
+
href="?state=closed"
22
+
class="flex items-center gap-2 {{ if .FilteringBy.IsClosed }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
10
23
>
11
-
<option value="open" {{ if .FilteringBy.IsOpen }}selected{{ end }}>
12
-
open ({{ .RepoInfo.Stats.PullCount.Open }})
13
-
</option>
14
-
<option value="merged" {{ if .FilteringBy.IsMerged }}selected{{ end }}>
15
-
merged ({{ .RepoInfo.Stats.PullCount.Merged }})
16
-
</option>
17
-
<option value="closed" {{ if .FilteringBy.IsClosed }}selected{{ end }}>
18
-
closed ({{ .RepoInfo.Stats.PullCount.Closed }})
19
-
</option>
20
-
</select>
21
-
pull requests
22
-
</p>
24
+
{{ i "ban" "w-4 h-4" }}
25
+
<span>{{ .RepoInfo.Stats.PullCount.Closed }} closed</span>
26
+
</a>
27
+
</div>
23
28
<a
24
29
href="/{{ .RepoInfo.FullName }}/pulls/new"
25
30
class="btn text-sm flex items-center gap-2 no-underline hover:no-underline"
···
79
84
</span>
80
85
</span>
81
86
{{ if not .IsPatchBased }}
82
-
<span>from
83
-
{{ if .IsForkBased }}
84
-
{{ if .PullSource.Repo }}
85
-
<a href="/{{ $owner }}/{{ .PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSource.Repo.Name }}</a>
86
-
{{ else }}
87
-
<span class="italic">[deleted fork]</span>
88
-
{{ end }}
89
-
{{ end }}
90
-
91
-
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
92
-
{{ .PullSource.Branch }}
93
-
</span>
87
+
from
88
+
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
89
+
{{ if .IsForkBased }}
90
+
{{ if .PullSource.Repo }}
91
+
<a href="/{{ $owner }}/{{ .PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSource.Repo.Name }}</a>:
92
+
{{- else -}}
93
+
<span class="italic">[deleted fork]</span>
94
+
{{- end -}}
95
+
{{- end -}}
96
+
{{- .PullSource.Branch -}}
94
97
</span>
95
98
{{ end }}
96
99
<span class="before:content-['ยท']">
+2
-2
appview/pages/templates/repo/tree.html
+2
-2
appview/pages/templates/repo/tree.html
···
54
54
<div class="flex justify-between items-center">
55
55
<a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}">
56
56
<div class="flex items-center gap-2">
57
-
{{ i "folder" "w-3 h-3 fill-current" }}{{ .Name }}
57
+
{{ i "folder" "size-4 fill-current" }}{{ .Name }}
58
58
</div>
59
59
</a>
60
60
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
···
69
69
<div class="flex justify-between items-center">
70
70
<a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}">
71
71
<div class="flex items-center gap-2">
72
-
{{ i "file" "w-3 h-3" }}{{ .Name }}
72
+
{{ i "file" "size-4" }}{{ .Name }}
73
73
</div>
74
74
</a>
75
75
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
+1
-1
appview/pages/templates/timeline.html
+1
-1
appview/pages/templates/timeline.html
···
23
23
</div>
24
24
<div class="italic text-lg">
25
25
tightly-knit social coding, <a href="/login" class="underline inline-flex gap-1 items-center">join now {{ i "arrow-right" "w-4 h-4" }}</a>
26
-
<p class="pt-5 px-10 text-sm text-gray-500 dark:text-gray-400">Join our <a href="https://chat.tangled.sh">Discord</a>or IRC channel: <a href="https://web.libera.chat/#tangled"><code>#tangled</code> on Libera Chat</a>.
26
+
<p class="pt-5 px-10 text-sm text-gray-500 dark:text-gray-400">Join our <a href="https://chat.tangled.sh">Discord</a> or IRC channel: <a href="https://web.libera.chat/#tangled"><code>#tangled</code> on Libera Chat</a>.
27
27
Read an introduction to Tangled <a href="https://blog.tangled.sh/intro">here</a>.</p>
28
28
</div>
29
29
</div>
+6
appview/pages/templates/user/fragments/bluesky.html
+6
appview/pages/templates/user/fragments/bluesky.html
···
1
+
{{ define "user/fragments/bluesky" }}
2
+
<svg class="{{.}}" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="-3 -3 30 30">
3
+
<title>Bluesky</title>
4
+
<path fill="none" stroke="currentColor" d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" stroke-width="2.25"/>
5
+
</svg>
6
+
{{ end }}
+111
appview/pages/templates/user/fragments/editBio.html
+111
appview/pages/templates/user/fragments/editBio.html
···
1
+
{{ define "user/fragments/editBio" }}
2
+
<form
3
+
hx-post="/profile/bio"
4
+
class="flex flex-col gap-4 my-2 max-w-full"
5
+
hx-disabled-elt="#save-btn,#cancel-btn"
6
+
hx-swap="none"
7
+
hx-indicator="#spinner">
8
+
<div class="flex flex-col gap-1">
9
+
{{ $description := "" }}
10
+
{{ if and .Profile .Profile.Description }}
11
+
{{ $description = .Profile.Description }}
12
+
{{ end }}
13
+
<label class="m-0 p-0" for="description">bio</label>
14
+
<textarea
15
+
type="text"
16
+
class="py-1 px-1 w-full"
17
+
name="description"
18
+
rows="3"
19
+
placeholder="write a bio">{{ $description }}</textarea>
20
+
</div>
21
+
22
+
<div class="flex flex-col gap-1">
23
+
<label class="m-0 p-0" for="location">location</label>
24
+
<div class="flex items-center gap-2 w-full">
25
+
{{ $location := "" }}
26
+
{{ if and .Profile .Profile.Location }}
27
+
{{ $location = .Profile.Location }}
28
+
{{ end }}
29
+
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
30
+
<input type="text" class="py-1 px-1 w-full" name="location" value="{{ $location }}">
31
+
</div>
32
+
</div>
33
+
34
+
<div class="flex flex-col gap-1">
35
+
<label class="m-0 p-0">social links</label>
36
+
<div class="flex items-center gap-2 py-1">
37
+
{{ $includeBsky := false }}
38
+
{{ if and .Profile .Profile.IncludeBluesky }}
39
+
{{ $includeBsky = true }}
40
+
{{ end }}
41
+
<input type="checkbox" id="includeBluesky" name="includeBluesky" value="on" {{if $includeBsky}}checked{{end}}>
42
+
<label for="includeBluesky" class="my-0 py-0 normal-case font-normal">Link to Bluesky account</label>
43
+
</div>
44
+
45
+
{{ $profile := .Profile }}
46
+
{{ range $idx, $s := (sequence 5) }}
47
+
{{ $link := "" }}
48
+
{{ if and $profile $profile.Links }}
49
+
{{ if lt $idx (len $profile.Links) }}
50
+
{{ $link = index $profile.Links $idx }}
51
+
{{ end }}
52
+
{{ end }}
53
+
54
+
<div class="flex items-center gap-2 w-full">
55
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
56
+
<input type="text" class="py-1 px-1 w-full" name="link{{$idx}}" value="{{ $link }}" placeholder="social link {{add $idx 1}}">
57
+
</div>
58
+
{{ end }}
59
+
</div>
60
+
61
+
<div class="flex flex-col gap-1">
62
+
<label class="m-0 p-0">vanity stats</label>
63
+
{{ range $idx, $s := (sequence 2) }}
64
+
{{ $stat := "" }}
65
+
{{ if and $profile $profile.Stats }}
66
+
{{ if lt $idx (len $profile.Stats) }}
67
+
{{ $s := index $profile.Stats $idx }}
68
+
{{ $stat = $s.Kind }}
69
+
{{ end }}
70
+
{{ end }}
71
+
72
+
{{ block "stat" (list $idx $stat) }} {{ end }}
73
+
{{ end }}
74
+
</div>
75
+
76
+
<div class="flex items-center gap-2 justify-between">
77
+
<button id="save-btn" type="submit" class="btn p-1 w-full flex items-center gap-2 no-underline text-sm">
78
+
{{ i "check" "size-4" }} save
79
+
<span id="spinner" class="group">
80
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
81
+
</span>
82
+
</button>
83
+
<a href="/{{.LoggedInUser.Did}}" class="w-full no-underline hover:no-underline">
84
+
<button id="cancel-btn" type="button" class="btn p-1 w-full flex items-center gap-2 no-underline text-sm">
85
+
{{ i "x" "size-4" }} cancel
86
+
</button>
87
+
</a>
88
+
</div>
89
+
</form>
90
+
{{ end }}
91
+
92
+
{{ define "stat" }}
93
+
{{ $id := index . 0 }}
94
+
{{ $stat := index . 1 }}
95
+
<select class="stat-group w-full p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700 text-sm" id="stat{{$id}}" name="stat{{$id}}">
96
+
<option value="">choose stat</option>
97
+
{{ $stats := assoc
98
+
"merged-pull-request-count" "Merged PR Count"
99
+
"closed-pull-request-count" "Closed PR Count"
100
+
"open-pull-request-count" "Open PR Count"
101
+
"open-issue-count" "Open Issue Count"
102
+
"closed-issue-count" "Closed Issue Count"
103
+
"repository-count" "Repository Count"
104
+
}}
105
+
{{ range $s := $stats }}
106
+
{{ $value := index $s 0 }}
107
+
{{ $label := index $s 1 }}
108
+
<option value="{{ $value }}"{{ if eq $stat $value }} selected{{ end }}>{{ $label }}</option>
109
+
{{ end }}
110
+
</select>
111
+
{{ end }}
+42
appview/pages/templates/user/fragments/editPins.html
+42
appview/pages/templates/user/fragments/editPins.html
···
1
+
{{ define "user/fragments/editPins" }}
2
+
{{ $profile := .Profile }}
3
+
<form
4
+
hx-post="/profile/pins"
5
+
hx-disabled-elt="#save-btn,#cancel-btn"
6
+
hx-swap="none"
7
+
hx-indicator="#spinner">
8
+
<div class="flex items-center justify-between mb-2">
9
+
<p class="text-sm font-bold p-2 dark:text-white">SELECT PINNED REPOS</p>
10
+
<div class="flex items-center gap-2">
11
+
<button id="save-btn" type="submit" class="btn px-2 flex items-center gap-2 no-underline text-sm">
12
+
{{ i "check" "w-3 h-3" }} save
13
+
<span id="spinner" class="group">
14
+
{{ i "loader-circle" "w-3 h-3 animate-spin hidden group-[.htmx-request]:inline" }}
15
+
</span>
16
+
</button>
17
+
<a href="/{{.LoggedInUser.Did}}" class="w-full no-underline hover:no-underline">
18
+
<button id="cancel-btn" type="button" class="btn px-2 w-full flex items-center gap-2 no-underline text-sm">
19
+
{{ i "x" "w-3 h-3" }} cancel
20
+
</button>
21
+
</a>
22
+
</div>
23
+
</div>
24
+
<div id="repos" class="grid grid-cols-1 gap-1 mb-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
25
+
{{ range $idx, $r := .AllRepos }}
26
+
<div class="flex items-center gap-2 text-base p-2 border-b border-gray-200 dark:border-gray-700">
27
+
<input type="checkbox" id="repo-{{$idx}}" name="pinnedRepo{{$idx}}" value="{{.RepoAt}}" {{if .IsPinned}}checked{{end}}>
28
+
<label for="repo-{{$idx}}" class="my-0 py-0 normal-case font-normal w-full">
29
+
<div class="flex justify-between items-center w-full">
30
+
<span class="flex-shrink-0 overflow-hidden text-ellipsis ">{{ index $.DidHandleMap .Did }}/{{.Name}}</span>
31
+
<div class="flex gap-1 items-center">
32
+
{{ i "star" "size-4 fill-current" }}
33
+
<span>{{ .RepoStats.StarCount }}</span>
34
+
</div>
35
+
</div>
36
+
</label>
37
+
</div>
38
+
{{ end }}
39
+
</div>
40
+
41
+
</form>
42
+
{{ end }}
+97
appview/pages/templates/user/fragments/profileCard.html
+97
appview/pages/templates/user/fragments/profileCard.html
···
1
+
{{ define "user/fragments/profileCard" }}
2
+
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
3
+
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
+
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
+
{{ if .AvatarUri }}
6
+
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
7
+
{{ end }}
8
+
</div>
9
+
<div class="col-span-2">
10
+
<p title="{{ didOrHandle .UserDid .UserHandle }}"
11
+
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
12
+
{{ didOrHandle .UserDid .UserHandle }}
13
+
</p>
14
+
15
+
<div class="md:hidden">
16
+
{{ block "followerFollowing" (list .Followers .Following) }} {{ end }}
17
+
</div>
18
+
</div>
19
+
<div class="col-span-3 md:col-span-full">
20
+
<div id="profile-bio" class="text-sm">
21
+
{{ $profile := .Profile }}
22
+
{{ with .Profile }}
23
+
24
+
{{ if .Description }}
25
+
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
26
+
{{ end }}
27
+
28
+
<div class="hidden md:block">
29
+
{{ block "followerFollowing" (list $.Followers $.Following) }} {{ end }}
30
+
</div>
31
+
32
+
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
33
+
{{ if .Location }}
34
+
<div class="flex items-center gap-2">
35
+
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
36
+
<span>{{ .Location }}</span>
37
+
</div>
38
+
{{ end }}
39
+
{{ if .IncludeBluesky }}
40
+
<div class="flex items-center gap-2">
41
+
<span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span>
42
+
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ didOrHandle $.UserDid $.UserHandle }}</a>
43
+
</div>
44
+
{{ end }}
45
+
{{ range $link := .Links }}
46
+
{{ if $link }}
47
+
<div class="flex items-center gap-2">
48
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
49
+
<a href="{{ $link }}">{{ $link }}</a>
50
+
</div>
51
+
{{ end }}
52
+
{{ end }}
53
+
{{ if not $profile.IsStatsEmpty }}
54
+
<div class="flex items-center justify-evenly gap-2 py-2">
55
+
{{ range $stat := .Stats }}
56
+
{{ if $stat.Kind }}
57
+
<div class="flex flex-col items-center gap-2">
58
+
<span class="text-xl font-bold">{{ $stat.Value }}</span>
59
+
<span>{{ $stat.Kind.String }}</span>
60
+
</div>
61
+
{{ end }}
62
+
{{ end }}
63
+
</div>
64
+
{{ end }}
65
+
</div>
66
+
{{ end }}
67
+
{{ if ne .FollowStatus.String "IsSelf" }}
68
+
{{ template "user/fragments/follow" . }}
69
+
{{ else }}
70
+
<button id="editBtn"
71
+
class="btn mt-2 w-full flex items-center gap-2 group"
72
+
hx-target="#profile-bio"
73
+
hx-get="/profile/edit-bio"
74
+
hx-swap="innerHTML">
75
+
{{ i "pencil" "w-4 h-4" }}
76
+
edit
77
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
78
+
</button>
79
+
{{ end }}
80
+
</div>
81
+
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
82
+
</div>
83
+
</div>
84
+
</div>
85
+
{{ end }}
86
+
87
+
{{ define "followerFollowing" }}
88
+
{{ $followers := index . 0 }}
89
+
{{ $following := index . 1 }}
90
+
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
91
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
92
+
<span id="followers">{{ $followers }} followers</span>
93
+
<span class="select-none after:content-['ยท']"></span>
94
+
<span id="following">{{ $following }} following</span>
95
+
</div>
96
+
{{ end }}
97
+
+22
-33
appview/pages/templates/user/login.html
+22
-33
appview/pages/templates/user/login.html
···
8
8
content="width=device-width, initial-scale=1.0"
9
9
/>
10
10
<script src="/static/htmx.min.js"></script>
11
-
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
11
+
<link
12
+
rel="stylesheet"
13
+
href="/static/tw.css?{{ cssContentHash }}"
14
+
type="text/css"
15
+
/>
12
16
<title>login</title>
13
17
</head>
14
18
<body class="flex items-center justify-center min-h-screen">
15
-
<main class="max-w-7xl px-6 -mt-4">
16
-
<h1 class="text-center text-2xl font-semibold italic dark:text-white">
19
+
<main class="max-w-md px-6 -mt-4">
20
+
<h1
21
+
class="text-center text-2xl font-semibold italic dark:text-white"
22
+
>
17
23
tangled
18
24
</h1>
19
25
<h2 class="text-center text-xl italic dark:text-white">
20
26
tightly-knit social coding.
21
27
</h2>
22
28
<form
23
-
class="w-full mt-4"
29
+
class="mt-4 max-w-sm mx-auto"
24
30
hx-post="/login"
25
31
hx-swap="none"
26
-
hx-disabled-elt="this"
32
+
hx-disabled-elt="#login-button"
27
33
>
28
34
<div class="flex flex-col">
29
35
<label for="handle">handle</label>
30
-
<input
31
-
type="text"
32
-
id="handle"
33
-
name="handle"
34
-
tabindex="1"
35
-
required
36
-
/>
37
-
<span class="text-xs text-gray-500 mt-1">
38
-
You need to use your
39
-
<a href="https://bsky.app">Bluesky</a> handle to log
40
-
in.
41
-
</span>
42
-
</div>
43
-
44
-
<div class="flex flex-col mt-2">
45
-
<label for="app_password">app password</label>
46
36
<input
47
-
type="password"
48
-
id="app_password"
49
-
name="app_password"
50
-
tabindex="2"
37
+
type="text"
38
+
id="handle"
39
+
name="handle"
40
+
tabindex="1"
51
41
required
52
42
/>
53
-
<span class="text-xs text-gray-500 mt-1">
54
-
Generate an app password
55
-
<a
56
-
href="https://bsky.app/settings/app-passwords"
57
-
target="_blank"
58
-
>here</a
59
-
>.
43
+
<span class="text-sm text-gray-500 mt-1">
44
+
Use your
45
+
<a href="https://bsky.app">Bluesky</a> handle to log
46
+
in. You will then be redirected to your PDS to
47
+
complete authentication.
60
48
</span>
61
49
</div>
62
50
···
70
58
</button>
71
59
</form>
72
60
<p class="text-sm text-gray-500">
73
-
Join our <a href="https://chat.tangled.sh">Discord</a> or IRC channel:
61
+
Join our <a href="https://chat.tangled.sh">Discord</a> or
62
+
IRC channel:
74
63
<a href="https://web.libera.chat/#tangled"
75
64
><code>#tangled</code> on Libera Chat</a
76
65
>.
+80
-91
appview/pages/templates/user/profile.html
+80
-91
appview/pages/templates/user/profile.html
···
1
-
{{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }}
1
+
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<div class="grid grid-cols-1 md:grid-cols-5 gap-6">
5
-
<div class="md:col-span-1 order-1 md:order-1">
6
-
{{ block "profileCard" . }}{{ end }}
4
+
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
5
+
<div class="md:col-span-2 order-1 md:order-1">
6
+
{{ template "user/fragments/profileCard" .Card }}
7
7
</div>
8
-
<div class="md:col-span-2 order-2 md:order-2">
8
+
<div id="all-repos" class="md:col-span-3 order-2 md:order-2">
9
9
{{ block "ownRepos" . }}{{ end }}
10
10
{{ block "collaboratingRepos" . }}{{ end }}
11
11
</div>
12
-
<div class="md:col-span-2 order-3 md:order-3">
12
+
<div class="md:col-span-3 order-3 md:order-3">
13
13
{{ block "profileTimeline" . }}{{ end }}
14
14
</div>
15
15
</div>
16
16
{{ end }}
17
17
18
18
{{ define "profileTimeline" }}
19
-
<p class="text-sm font-bold py-2 dark:text-white px-6">ACTIVITY</p>
19
+
<p class="text-sm font-bold p-2 dark:text-white">ACTIVITY</p>
20
20
<div class="flex flex-col gap-6 relative">
21
21
{{ with .ProfileTimeline }}
22
22
{{ range $idx, $byMonth := .ByMonth }}
···
225
225
{{ end }}
226
226
{{ end }}
227
227
228
-
{{ define "profileCard" }}
229
-
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
230
-
<div class="grid grid-cols-3 md:grid-cols-1 gap-3 items-center">
231
-
<div id="avatar" class="col-span-1 md-col-span-full flex justify-center items-center">
232
-
{{ if .AvatarUri }}
233
-
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
234
-
{{ end }}
228
+
{{ define "ownRepos" }}
229
+
<div class="text-sm font-bold p-2 pr-0 dark:text-white flex items-center justify-between gap-2">
230
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos"
231
+
class="flex text-black dark:text-white items-center gap-4 no-underline hover:no-underline group">
232
+
<span>PINNED REPOS</span>
233
+
<span class="flex md:hidden group-hover:flex gap-2 items-center font-normal text-sm text-gray-500 dark:text-gray-400 ">
234
+
view all {{ i "chevron-right" "w-4 h-4" }}
235
+
</span>
236
+
</a>
237
+
{{ if and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }}
238
+
<button
239
+
hx-get="profile/edit-pins"
240
+
hx-target="#all-repos"
241
+
class="btn font-normal text-sm flex gap-2 items-center group">
242
+
{{ i "pencil" "w-3 h-3" }}
243
+
edit
244
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
245
+
</button>
246
+
{{ end }}
247
+
</div>
248
+
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
249
+
{{ range .Repos }}
250
+
<div
251
+
id="repo-card"
252
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
253
+
<div id="repo-card-name" class="font-medium">
254
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
255
+
>{{ .Name }}</a
256
+
>
257
+
</div>
258
+
{{ if .Description }}
259
+
<div class="text-gray-600 dark:text-gray-300 text-sm">
260
+
{{ .Description }}
261
+
</div>
262
+
{{ end }}
263
+
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
264
+
{{ if .RepoStats.StarCount }}
265
+
<div class="flex gap-1 items-center text-sm">
266
+
{{ i "star" "w-3 h-3 fill-current" }}
267
+
<span>{{ .RepoStats.StarCount }}</span>
268
+
</div>
269
+
{{ end }}
270
+
</div>
235
271
</div>
236
-
<div id="text" class="col-span-2 md:col-span-full">
237
-
<p
238
-
title="{{ didOrHandle .UserDid .UserHandle }}"
239
-
class="text-lg font-bold md:text-center dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
240
-
{{ didOrHandle .UserDid .UserHandle }}
241
-
</p>
242
-
<div class="text-sm md:text-center dark:text-gray-300">
243
-
<span id="followers">{{ .ProfileStats.Followers }} followers</span>
244
-
<span class="px-1 select-none after:content-['ยท']"></span>
245
-
<span id="following">{{ .ProfileStats.Following }} following</span>
246
-
</div>
247
-
248
-
{{ if ne .FollowStatus.String "IsSelf" }}
249
-
{{ template "user/fragments/follow" . }}
250
-
{{ end }}
251
-
</div>
252
-
</div>
272
+
{{ else }}
273
+
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
274
+
{{ end }}
253
275
</div>
254
276
{{ end }}
255
277
256
-
{{ define "ownRepos" }}
257
-
<p class="text-sm font-bold py-2 px-6 dark:text-white">REPOS</p>
258
-
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
259
-
{{ range .Repos }}
260
-
<div
261
-
id="repo-card"
262
-
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800"
263
-
>
264
-
<div id="repo-card-name" class="font-medium dark:text-white">
265
-
<a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}"
266
-
>{{ .Name }}</a
267
-
>
268
-
</div>
269
-
{{ if .Description }}
270
-
<div class="text-gray-600 dark:text-gray-300 text-sm">
271
-
{{ .Description }}
272
-
</div>
273
-
{{ end }}
274
-
<div
275
-
class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto"
276
-
>
277
-
278
-
{{ if .RepoStats.StarCount }}
279
-
<div class="flex gap-1 items-center text-sm">
280
-
{{ i "star" "w-3 h-3 fill-current" }}
281
-
<span>{{ .RepoStats.StarCount }}</span>
282
-
</div>
283
-
{{ end }}
284
-
</div>
278
+
{{ define "collaboratingRepos" }}
279
+
{{ if gt (len .CollaboratingRepos) 0 }}
280
+
<p class="text-sm font-bold p-2 dark:text-white">COLLABORATING ON</p>
281
+
<div id="collaborating" class="grid grid-cols-1 gap-4 mb-6">
282
+
{{ range .CollaboratingRepos }}
283
+
<div
284
+
id="repo-card"
285
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex flex-col">
286
+
<div id="repo-card-name" class="font-medium dark:text-white">
287
+
<a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">
288
+
{{ index $.DidHandleMap .Did }}/{{ .Name }}
289
+
</a>
285
290
</div>
286
-
{{ else }}
287
-
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
288
-
{{ end }}
289
-
</div>
290
-
291
-
<p class="text-sm font-bold py-2 px-6 dark:text-white">COLLABORATING ON</p>
292
-
<div id="collaborating" class="grid grid-cols-1 gap-4 mb-6">
293
-
{{ range .CollaboratingRepos }}
294
-
<div
295
-
id="repo-card"
296
-
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex flex-col"
297
-
>
298
-
<div id="repo-card-name" class="font-medium dark:text-white">
299
-
<a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">
300
-
{{ index $.DidHandleMap .Did }}/{{ .Name }}
301
-
</a>
291
+
{{ if .Description }}
292
+
<div class="text-gray-600 dark:text-gray-300 text-sm">
293
+
{{ .Description }}
302
294
</div>
303
-
{{ if .Description }}
304
-
<div class="text-gray-600 dark:text-gray-300 text-sm">
305
-
{{ .Description }}
295
+
{{ end }}
296
+
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
297
+
298
+
{{ if .RepoStats.StarCount }}
299
+
<div class="flex gap-1 items-center text-sm">
300
+
{{ i "star" "w-3 h-3 fill-current" }}
301
+
<span>{{ .RepoStats.StarCount }}</span>
306
302
</div>
307
303
{{ end }}
308
-
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
309
-
310
-
{{ if .RepoStats.StarCount }}
311
-
<div class="flex gap-1 items-center text-sm">
312
-
{{ i "star" "w-3 h-3 fill-current" }}
313
-
<span>{{ .RepoStats.StarCount }}</span>
314
-
</div>
315
-
{{ end }}
316
-
</div>
317
304
</div>
318
-
{{ else }}
319
-
<p class="px-6 dark:text-white">This user is not collaborating.</p>
320
-
{{ end }}
305
+
</div>
306
+
{{ else }}
307
+
<p class="px-6 dark:text-white">This user is not collaborating.</p>
308
+
{{ end }}
321
309
</div>
310
+
{{ end }}
322
311
{{ end }}
+44
appview/pages/templates/user/repos.html
+44
appview/pages/templates/user/repos.html
···
1
+
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท repos {{ end }}
2
+
3
+
{{ define "content" }}
4
+
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
5
+
<div class="md:col-span-2 order-1 md:order-1">
6
+
{{ template "user/fragments/profileCard" .Card }}
7
+
</div>
8
+
<div id="all-repos" class="md:col-span-6 order-2 md:order-2">
9
+
{{ block "ownRepos" . }}{{ end }}
10
+
</div>
11
+
</div>
12
+
{{ end }}
13
+
14
+
{{ define "ownRepos" }}
15
+
<p class="text-sm font-bold p-2 dark:text-white">ALL REPOSITORIES</p>
16
+
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
17
+
{{ range .Repos }}
18
+
<div
19
+
id="repo-card"
20
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
21
+
<div id="repo-card-name" class="font-medium">
22
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
23
+
>{{ .Name }}</a
24
+
>
25
+
</div>
26
+
{{ if .Description }}
27
+
<div class="text-gray-600 dark:text-gray-300 text-sm">
28
+
{{ .Description }}
29
+
</div>
30
+
{{ end }}
31
+
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
32
+
{{ if .RepoStats.StarCount }}
33
+
<div class="flex gap-1 items-center text-sm">
34
+
{{ i "star" "w-3 h-3 fill-current" }}
35
+
<span>{{ .RepoStats.StarCount }}</span>
36
+
</div>
37
+
{{ end }}
38
+
</div>
39
+
</div>
40
+
{{ else }}
41
+
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
42
+
{{ end }}
43
+
</div>
44
+
{{ end }}
+27
-18
appview/settings/settings.go
+27
-18
appview/settings/settings.go
···
13
13
"github.com/go-chi/chi/v5"
14
14
"tangled.sh/tangled.sh/core/api/tangled"
15
15
"tangled.sh/tangled.sh/core/appview"
16
-
"tangled.sh/tangled.sh/core/appview/auth"
17
16
"tangled.sh/tangled.sh/core/appview/db"
18
17
"tangled.sh/tangled.sh/core/appview/email"
19
18
"tangled.sh/tangled.sh/core/appview/middleware"
19
+
"tangled.sh/tangled.sh/core/appview/oauth"
20
20
"tangled.sh/tangled.sh/core/appview/pages"
21
21
22
22
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
27
27
28
28
type Settings struct {
29
29
Db *db.DB
30
-
Auth *auth.Auth
30
+
OAuth *oauth.OAuth
31
31
Pages *pages.Pages
32
32
Config *appview.Config
33
33
}
···
35
35
func (s *Settings) Router() http.Handler {
36
36
r := chi.NewRouter()
37
37
38
-
r.Use(middleware.AuthMiddleware(s.Auth))
38
+
r.Use(middleware.AuthMiddleware(s.OAuth))
39
39
40
40
r.Get("/", s.settings)
41
41
···
56
56
}
57
57
58
58
func (s *Settings) settings(w http.ResponseWriter, r *http.Request) {
59
-
user := s.Auth.GetUser(r)
59
+
user := s.OAuth.GetUser(r)
60
60
pubKeys, err := db.GetPublicKeys(s.Db, user.Did)
61
61
if err != nil {
62
62
log.Println(err)
···
79
79
verifyURL := s.verifyUrl(did, emailAddr, code)
80
80
81
81
return email.Email{
82
-
APIKey: s.Config.ResendApiKey,
82
+
APIKey: s.Config.Resend.ApiKey,
83
83
From: "noreply@notifs.tangled.sh",
84
84
To: emailAddr,
85
85
Subject: "Verify your Tangled email",
···
111
111
log.Println("unimplemented")
112
112
return
113
113
case http.MethodPut:
114
-
did := s.Auth.GetDid(r)
114
+
did := s.OAuth.GetDid(r)
115
115
emAddr := r.FormValue("email")
116
116
emAddr = strings.TrimSpace(emAddr)
117
117
···
174
174
s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.")
175
175
return
176
176
case http.MethodDelete:
177
-
did := s.Auth.GetDid(r)
177
+
did := s.OAuth.GetDid(r)
178
178
emailAddr := r.FormValue("email")
179
179
emailAddr = strings.TrimSpace(emailAddr)
180
180
···
207
207
208
208
func (s *Settings) verifyUrl(did string, email string, code string) string {
209
209
var appUrl string
210
-
if s.Config.Dev {
211
-
appUrl = "http://" + s.Config.ListenAddr
210
+
if s.Config.Core.Dev {
211
+
appUrl = "http://" + s.Config.Core.ListenAddr
212
212
} else {
213
213
appUrl = "https://tangled.sh"
214
214
}
···
252
252
return
253
253
}
254
254
255
-
did := s.Auth.GetDid(r)
255
+
did := s.OAuth.GetDid(r)
256
256
emAddr := r.FormValue("email")
257
257
emAddr = strings.TrimSpace(emAddr)
258
258
···
323
323
}
324
324
325
325
func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) {
326
-
did := s.Auth.GetDid(r)
326
+
did := s.OAuth.GetDid(r)
327
327
emailAddr := r.FormValue("email")
328
328
emailAddr = strings.TrimSpace(emailAddr)
329
329
···
348
348
log.Println("unimplemented")
349
349
return
350
350
case http.MethodPut:
351
-
did := s.Auth.GetDid(r)
351
+
did := s.OAuth.GetDid(r)
352
352
key := r.FormValue("key")
353
353
key = strings.TrimSpace(key)
354
354
name := r.FormValue("name")
355
-
client, _ := s.Auth.AuthorizedClient(r)
355
+
client, err := s.OAuth.AuthorizedClient(r)
356
+
if err != nil {
357
+
s.Pages.Notice(w, "settings-keys", "Failed to authorize. Try again later.")
358
+
return
359
+
}
356
360
357
-
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
361
+
_, _, _, _, err = ssh.ParseAuthorizedKey([]byte(key))
358
362
if err != nil {
359
363
log.Printf("parsing public key: %s", err)
360
364
s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.")
···
378
382
}
379
383
380
384
// store in pds too
381
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
385
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
382
386
Collection: tangled.PublicKeyNSID,
383
387
Repo: did,
384
388
Rkey: rkey,
···
409
413
return
410
414
411
415
case http.MethodDelete:
412
-
did := s.Auth.GetDid(r)
416
+
did := s.OAuth.GetDid(r)
413
417
q := r.URL.Query()
414
418
415
419
name := q.Get("name")
···
420
424
log.Println(rkey)
421
425
log.Println(key)
422
426
423
-
client, _ := s.Auth.AuthorizedClient(r)
427
+
client, err := s.OAuth.AuthorizedClient(r)
428
+
if err != nil {
429
+
log.Printf("failed to authorize client: %s", err)
430
+
s.Pages.Notice(w, "settings-keys", "Failed to authorize client.")
431
+
return
432
+
}
424
433
425
434
if err := db.DeletePublicKey(s.Db, did, name, key); err != nil {
426
435
log.Printf("removing public key: %s", err)
···
430
439
431
440
if rkey != "" {
432
441
// remove from pds too
433
-
_, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
442
+
_, err := client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
434
443
Collection: tangled.PublicKeyNSID,
435
444
Repo: did,
436
445
Rkey: rkey,
+20
-10
appview/state/artifact.go
+20
-10
appview/state/artifact.go
···
16
16
"tangled.sh/tangled.sh/core/api/tangled"
17
17
"tangled.sh/tangled.sh/core/appview"
18
18
"tangled.sh/tangled.sh/core/appview/db"
19
+
"tangled.sh/tangled.sh/core/appview/knotclient"
19
20
"tangled.sh/tangled.sh/core/appview/pages"
20
21
"tangled.sh/tangled.sh/core/types"
21
22
)
22
23
23
24
// TODO: proper statuses here on early exit
24
25
func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) {
25
-
user := s.auth.GetUser(r)
26
+
user := s.oauth.GetUser(r)
26
27
tagParam := chi.URLParam(r, "tag")
27
28
f, err := s.fullyResolvedRepo(r)
28
29
if err != nil {
···
46
47
}
47
48
defer file.Close()
48
49
49
-
client, _ := s.auth.AuthorizedClient(r)
50
+
client, err := s.oauth.AuthorizedClient(r)
51
+
if err != nil {
52
+
log.Println("failed to get authorized client", err)
53
+
s.pages.Notice(w, "upload", "failed to get authorized client")
54
+
return
55
+
}
50
56
51
-
uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file)
57
+
uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file)
52
58
if err != nil {
53
59
log.Println("failed to upload blob", err)
54
60
s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.")
···
60
66
rkey := appview.TID()
61
67
createdAt := time.Now()
62
68
63
-
putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
69
+
putRecordResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
64
70
Collection: tangled.RepoArtifactNSID,
65
71
Repo: user.Did,
66
72
Rkey: rkey,
···
140
146
return
141
147
}
142
148
143
-
client, _ := s.auth.AuthorizedClient(r)
149
+
client, err := s.oauth.AuthorizedClient(r)
150
+
if err != nil {
151
+
log.Println("failed to get authorized client", err)
152
+
return
153
+
}
144
154
145
155
artifacts, err := db.GetArtifact(
146
156
s.db,
···
159
169
160
170
artifact := artifacts[0]
161
171
162
-
getBlobResp, err := comatproto.SyncGetBlob(r.Context(), client, artifact.BlobCid.String(), artifact.Did)
172
+
getBlobResp, err := client.SyncGetBlob(r.Context(), artifact.BlobCid.String(), artifact.Did)
163
173
if err != nil {
164
174
log.Println("failed to get blob from pds", err)
165
175
return
···
171
181
172
182
// TODO: proper statuses here on early exit
173
183
func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
174
-
user := s.auth.GetUser(r)
184
+
user := s.oauth.GetUser(r)
175
185
tagParam := chi.URLParam(r, "tag")
176
186
filename := chi.URLParam(r, "file")
177
187
f, err := s.fullyResolvedRepo(r)
···
180
190
return
181
191
}
182
192
183
-
client, _ := s.auth.AuthorizedClient(r)
193
+
client, _ := s.oauth.AuthorizedClient(r)
184
194
185
195
tag := plumbing.NewHash(tagParam)
186
196
···
208
218
return
209
219
}
210
220
211
-
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
221
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
212
222
Collection: tangled.RepoArtifactNSID,
213
223
Repo: user.Did,
214
224
Rkey: artifact.Rkey,
···
254
264
return nil, err
255
265
}
256
266
257
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
267
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
258
268
if err != nil {
259
269
return nil, err
260
270
}
+8
-4
appview/state/follow.go
+8
-4
appview/state/follow.go
···
14
14
)
15
15
16
16
func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
17
-
currentUser := s.auth.GetUser(r)
17
+
currentUser := s.oauth.GetUser(r)
18
18
19
19
subject := r.URL.Query().Get("subject")
20
20
if subject == "" {
···
32
32
return
33
33
}
34
34
35
-
client, _ := s.auth.AuthorizedClient(r)
35
+
client, err := s.oauth.AuthorizedClient(r)
36
+
if err != nil {
37
+
log.Println("failed to authorize client")
38
+
return
39
+
}
36
40
37
41
switch r.Method {
38
42
case http.MethodPost:
39
43
createdAt := time.Now().Format(time.RFC3339)
40
44
rkey := appview.TID()
41
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
45
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
42
46
Collection: tangled.GraphFollowNSID,
43
47
Repo: currentUser.Did,
44
48
Rkey: rkey,
···
75
79
return
76
80
}
77
81
78
-
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
82
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
79
83
Collection: tangled.GraphFollowNSID,
80
84
Repo: currentUser.Did,
81
85
Rkey: follow.Rkey,
+2
-2
appview/state/git_http.go
+2
-2
appview/state/git_http.go
···
15
15
repo := chi.URLParam(r, "repo")
16
16
17
17
scheme := "https"
18
-
if s.config.Dev {
18
+
if s.config.Core.Dev {
19
19
scheme = "http"
20
20
}
21
21
targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, knot, user.DID, repo, r.URL.RawQuery)
···
52
52
repo := chi.URLParam(r, "repo")
53
53
54
54
scheme := "https"
55
-
if s.config.Dev {
55
+
if s.config.Core.Dev {
56
56
scheme = "http"
57
57
}
58
58
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery)
+36
-3
appview/state/middleware.go
+36
-3
appview/state/middleware.go
···
2
2
3
3
import (
4
4
"context"
5
+
"fmt"
5
6
"log"
6
7
"net/http"
7
8
"strconv"
···
20
21
return func(next http.Handler) http.Handler {
21
22
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22
23
// requires auth also
23
-
actor := s.auth.GetUser(r)
24
+
actor := s.oauth.GetUser(r)
24
25
if actor == nil {
25
26
// we need a logged in user
26
27
log.Printf("not logged in, redirecting")
···
54
55
return func(next http.Handler) http.Handler {
55
56
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56
57
// requires auth also
57
-
actor := s.auth.GetUser(r)
58
+
actor := s.oauth.GetUser(r)
58
59
if actor == nil {
59
60
// we need a logged in user
60
61
log.Printf("not logged in, redirecting")
···
131
132
if err != nil {
132
133
// invalid did or handle
133
134
log.Println("failed to resolve repo")
134
-
w.WriteHeader(http.StatusNotFound)
135
+
s.pages.Error404(w)
135
136
return
136
137
}
137
138
···
175
176
})
176
177
}
177
178
}
179
+
180
+
// this should serve the go-import meta tag even if the path is technically
181
+
// a 404 like tangled.sh/oppi.li/go-git/v5
182
+
func GoImport(s *State) middleware.Middleware {
183
+
return func(next http.Handler) http.Handler {
184
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
185
+
f, err := s.fullyResolvedRepo(r)
186
+
if err != nil {
187
+
log.Println("failed to fully resolve repo", err)
188
+
http.Error(w, "invalid repo url", http.StatusNotFound)
189
+
return
190
+
}
191
+
192
+
fullName := f.OwnerHandle() + "/" + f.RepoName
193
+
194
+
if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
195
+
if r.URL.Query().Get("go-get") == "1" {
196
+
html := fmt.Sprintf(
197
+
`<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/>`,
198
+
fullName,
199
+
fullName,
200
+
)
201
+
w.Header().Set("Content-Type", "text/html")
202
+
w.Write([]byte(html))
203
+
return
204
+
}
205
+
}
206
+
207
+
next.ServeHTTP(w, r)
208
+
})
209
+
}
210
+
}
+328
-14
appview/state/profile.go
+328
-14
appview/state/profile.go
···
7
7
"fmt"
8
8
"log"
9
9
"net/http"
10
+
"slices"
11
+
"strings"
10
12
13
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
11
14
"github.com/bluesky-social/indigo/atproto/identity"
15
+
"github.com/bluesky-social/indigo/atproto/syntax"
16
+
lexutil "github.com/bluesky-social/indigo/lex/util"
12
17
"github.com/go-chi/chi/v5"
18
+
"tangled.sh/tangled.sh/core/api/tangled"
13
19
"tangled.sh/tangled.sh/core/appview/db"
14
20
"tangled.sh/tangled.sh/core/appview/pages"
15
21
)
16
22
17
-
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
23
+
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
24
+
tabVal := r.URL.Query().Get("tab")
25
+
switch tabVal {
26
+
case "":
27
+
s.profilePage(w, r)
28
+
case "repos":
29
+
s.reposPage(w, r)
30
+
}
31
+
}
32
+
33
+
func (s *State) profilePage(w http.ResponseWriter, r *http.Request) {
18
34
didOrHandle := chi.URLParam(r, "user")
19
35
if didOrHandle == "" {
20
36
http.Error(w, "Bad request", http.StatusBadRequest)
···
27
43
return
28
44
}
29
45
46
+
profile, err := db.GetProfile(s.db, ident.DID.String())
47
+
if err != nil {
48
+
log.Printf("getting profile data for %s: %s", ident.DID.String(), err)
49
+
}
50
+
30
51
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
31
52
if err != nil {
32
53
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
33
54
}
34
55
56
+
// filter out ones that are pinned
57
+
pinnedRepos := []db.Repo{}
58
+
for i, r := range repos {
59
+
// if this is a pinned repo, add it
60
+
if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) {
61
+
pinnedRepos = append(pinnedRepos, r)
62
+
}
63
+
64
+
// if there are no saved pins, add the first 4 repos
65
+
if profile.IsPinnedReposEmpty() && i < 4 {
66
+
pinnedRepos = append(pinnedRepos, r)
67
+
}
68
+
}
69
+
35
70
collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String())
36
71
if err != nil {
37
72
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
38
73
}
39
74
75
+
pinnedCollaboratingRepos := []db.Repo{}
76
+
for _, r := range collaboratingRepos {
77
+
// if this is a pinned repo, add it
78
+
if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) {
79
+
pinnedCollaboratingRepos = append(pinnedCollaboratingRepos, r)
80
+
}
81
+
}
82
+
40
83
timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String())
41
84
if err != nil {
42
85
log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err)
···
76
119
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
77
120
}
78
121
79
-
loggedInUser := s.auth.GetUser(r)
122
+
loggedInUser := s.oauth.GetUser(r)
80
123
followStatus := db.IsNotFollowing
81
124
if loggedInUser != nil {
82
125
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
···
85
128
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
86
129
s.pages.ProfilePage(w, pages.ProfilePageParams{
87
130
LoggedInUser: loggedInUser,
88
-
UserDid: ident.DID.String(),
89
-
UserHandle: ident.Handle.String(),
90
-
Repos: repos,
91
-
CollaboratingRepos: collaboratingRepos,
92
-
ProfileStats: pages.ProfileStats{
93
-
Followers: followers,
94
-
Following: following,
131
+
Repos: pinnedRepos,
132
+
CollaboratingRepos: pinnedCollaboratingRepos,
133
+
DidHandleMap: didHandleMap,
134
+
Card: pages.ProfileCard{
135
+
UserDid: ident.DID.String(),
136
+
UserHandle: ident.Handle.String(),
137
+
AvatarUri: profileAvatarUri,
138
+
Profile: profile,
139
+
FollowStatus: followStatus,
140
+
Followers: followers,
141
+
Following: following,
95
142
},
96
-
FollowStatus: db.FollowStatus(followStatus),
97
-
DidHandleMap: didHandleMap,
98
-
AvatarUri: profileAvatarUri,
99
143
ProfileTimeline: timeline,
100
144
})
101
145
}
102
146
147
+
func (s *State) reposPage(w http.ResponseWriter, r *http.Request) {
148
+
ident, ok := r.Context().Value("resolvedId").(identity.Identity)
149
+
if !ok {
150
+
s.pages.Error404(w)
151
+
return
152
+
}
153
+
154
+
profile, err := db.GetProfile(s.db, ident.DID.String())
155
+
if err != nil {
156
+
log.Printf("getting profile data for %s: %s", ident.DID.String(), err)
157
+
}
158
+
159
+
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
160
+
if err != nil {
161
+
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
162
+
}
163
+
164
+
loggedInUser := s.oauth.GetUser(r)
165
+
followStatus := db.IsNotFollowing
166
+
if loggedInUser != nil {
167
+
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
168
+
}
169
+
170
+
followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String())
171
+
if err != nil {
172
+
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
173
+
}
174
+
175
+
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
176
+
177
+
s.pages.ReposPage(w, pages.ReposPageParams{
178
+
LoggedInUser: loggedInUser,
179
+
Repos: repos,
180
+
Card: pages.ProfileCard{
181
+
UserDid: ident.DID.String(),
182
+
UserHandle: ident.Handle.String(),
183
+
AvatarUri: profileAvatarUri,
184
+
Profile: profile,
185
+
FollowStatus: followStatus,
186
+
Followers: followers,
187
+
Following: following,
188
+
},
189
+
})
190
+
}
191
+
103
192
func (s *State) GetAvatarUri(handle string) string {
104
-
secret := s.config.AvatarSharedSecret
193
+
secret := s.config.Avatar.SharedSecret
105
194
h := hmac.New(sha256.New, []byte(secret))
106
195
h.Write([]byte(handle))
107
196
signature := hex.EncodeToString(h.Sum(nil))
108
-
return fmt.Sprintf("%s/%s/%s", s.config.AvatarHost, signature, handle)
197
+
return fmt.Sprintf("%s/%s/%s", s.config.Avatar.Host, signature, handle)
198
+
}
199
+
200
+
func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) {
201
+
user := s.oauth.GetUser(r)
202
+
203
+
err := r.ParseForm()
204
+
if err != nil {
205
+
log.Println("invalid profile update form", err)
206
+
s.pages.Notice(w, "update-profile", "Invalid form.")
207
+
return
208
+
}
209
+
210
+
profile, err := db.GetProfile(s.db, user.Did)
211
+
if err != nil {
212
+
log.Printf("getting profile data for %s: %s", user.Did, err)
213
+
}
214
+
215
+
profile.Description = r.FormValue("description")
216
+
profile.IncludeBluesky = r.FormValue("includeBluesky") == "on"
217
+
profile.Location = r.FormValue("location")
218
+
219
+
var links [5]string
220
+
for i := range 5 {
221
+
iLink := r.FormValue(fmt.Sprintf("link%d", i))
222
+
links[i] = iLink
223
+
}
224
+
profile.Links = links
225
+
226
+
// Parse stats (exactly 2)
227
+
stat0 := r.FormValue("stat0")
228
+
stat1 := r.FormValue("stat1")
229
+
230
+
if stat0 != "" {
231
+
profile.Stats[0].Kind = db.VanityStatKind(stat0)
232
+
}
233
+
234
+
if stat1 != "" {
235
+
profile.Stats[1].Kind = db.VanityStatKind(stat1)
236
+
}
237
+
238
+
if err := db.ValidateProfile(s.db, profile); err != nil {
239
+
log.Println("invalid profile", err)
240
+
s.pages.Notice(w, "update-profile", err.Error())
241
+
return
242
+
}
243
+
244
+
s.updateProfile(profile, w, r)
245
+
return
246
+
}
247
+
248
+
func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) {
249
+
user := s.oauth.GetUser(r)
250
+
251
+
err := r.ParseForm()
252
+
if err != nil {
253
+
log.Println("invalid profile update form", err)
254
+
s.pages.Notice(w, "update-profile", "Invalid form.")
255
+
return
256
+
}
257
+
258
+
profile, err := db.GetProfile(s.db, user.Did)
259
+
if err != nil {
260
+
log.Printf("getting profile data for %s: %s", user.Did, err)
261
+
}
262
+
263
+
i := 0
264
+
var pinnedRepos [6]syntax.ATURI
265
+
for key, values := range r.Form {
266
+
if i >= 6 {
267
+
log.Println("invalid pin update form", err)
268
+
s.pages.Notice(w, "update-profile", "Only 6 repositories can be pinned at a time.")
269
+
return
270
+
}
271
+
if strings.HasPrefix(key, "pinnedRepo") && len(values) > 0 && values[0] != "" && i < 6 {
272
+
aturi, err := syntax.ParseATURI(values[0])
273
+
if err != nil {
274
+
log.Println("invalid profile update form", err)
275
+
s.pages.Notice(w, "update-profile", "Invalid form.")
276
+
return
277
+
}
278
+
pinnedRepos[i] = aturi
279
+
i++
280
+
}
281
+
}
282
+
profile.PinnedRepos = pinnedRepos
283
+
284
+
s.updateProfile(profile, w, r)
285
+
return
286
+
}
287
+
288
+
func (s *State) updateProfile(profile *db.Profile, w http.ResponseWriter, r *http.Request) {
289
+
user := s.oauth.GetUser(r)
290
+
tx, err := s.db.BeginTx(r.Context(), nil)
291
+
if err != nil {
292
+
log.Println("failed to start transaction", err)
293
+
s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.")
294
+
return
295
+
}
296
+
297
+
client, err := s.oauth.AuthorizedClient(r)
298
+
if err != nil {
299
+
log.Println("failed to get authorized client", err)
300
+
s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.")
301
+
return
302
+
}
303
+
304
+
// yeah... lexgen dose not support syntax.ATURI in the record for some reason,
305
+
// nor does it support exact size arrays
306
+
var pinnedRepoStrings []string
307
+
for _, r := range profile.PinnedRepos {
308
+
pinnedRepoStrings = append(pinnedRepoStrings, r.String())
309
+
}
310
+
311
+
var vanityStats []string
312
+
for _, v := range profile.Stats {
313
+
vanityStats = append(vanityStats, string(v.Kind))
314
+
}
315
+
316
+
ex, _ := client.RepoGetRecord(r.Context(), "", tangled.ActorProfileNSID, user.Did, "self")
317
+
var cid *string
318
+
if ex != nil {
319
+
cid = ex.Cid
320
+
}
321
+
322
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
323
+
Collection: tangled.ActorProfileNSID,
324
+
Repo: user.Did,
325
+
Rkey: "self",
326
+
Record: &lexutil.LexiconTypeDecoder{
327
+
Val: &tangled.ActorProfile{
328
+
Bluesky: profile.IncludeBluesky,
329
+
Description: &profile.Description,
330
+
Links: profile.Links[:],
331
+
Location: &profile.Location,
332
+
PinnedRepositories: pinnedRepoStrings,
333
+
Stats: vanityStats[:],
334
+
}},
335
+
SwapRecord: cid,
336
+
})
337
+
if err != nil {
338
+
log.Println("failed to update profile", err)
339
+
s.pages.Notice(w, "update-profile", "Failed to update PDS, try again later.")
340
+
return
341
+
}
342
+
343
+
err = db.UpsertProfile(tx, profile)
344
+
if err != nil {
345
+
log.Println("failed to update profile", err)
346
+
s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.")
347
+
return
348
+
}
349
+
350
+
s.pages.HxRedirect(w, "/"+user.Did)
351
+
return
352
+
}
353
+
354
+
func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) {
355
+
user := s.oauth.GetUser(r)
356
+
357
+
profile, err := db.GetProfile(s.db, user.Did)
358
+
if err != nil {
359
+
log.Printf("getting profile data for %s: %s", user.Did, err)
360
+
}
361
+
362
+
s.pages.EditBioFragment(w, pages.EditBioParams{
363
+
LoggedInUser: user,
364
+
Profile: profile,
365
+
})
366
+
}
367
+
368
+
func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) {
369
+
user := s.oauth.GetUser(r)
370
+
371
+
profile, err := db.GetProfile(s.db, user.Did)
372
+
if err != nil {
373
+
log.Printf("getting profile data for %s: %s", user.Did, err)
374
+
}
375
+
376
+
repos, err := db.GetAllReposByDid(s.db, user.Did)
377
+
if err != nil {
378
+
log.Printf("getting repos for %s: %s", user.Did, err)
379
+
}
380
+
381
+
collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did)
382
+
if err != nil {
383
+
log.Printf("getting collaborating repos for %s: %s", user.Did, err)
384
+
}
385
+
386
+
allRepos := []pages.PinnedRepo{}
387
+
388
+
for _, r := range repos {
389
+
isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt())
390
+
allRepos = append(allRepos, pages.PinnedRepo{
391
+
IsPinned: isPinned,
392
+
Repo: r,
393
+
})
394
+
}
395
+
for _, r := range collaboratingRepos {
396
+
isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt())
397
+
allRepos = append(allRepos, pages.PinnedRepo{
398
+
IsPinned: isPinned,
399
+
Repo: r,
400
+
})
401
+
}
402
+
403
+
var didsToResolve []string
404
+
for _, r := range allRepos {
405
+
didsToResolve = append(didsToResolve, r.Did)
406
+
}
407
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
408
+
didHandleMap := make(map[string]string)
409
+
for _, identity := range resolvedIds {
410
+
if !identity.Handle.IsInvalidHandle() {
411
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
412
+
} else {
413
+
didHandleMap[identity.DID.String()] = identity.DID.String()
414
+
}
415
+
}
416
+
417
+
s.pages.EditPinsFragment(w, pages.EditPinsParams{
418
+
LoggedInUser: user,
419
+
Profile: profile,
420
+
AllRepos: allRepos,
421
+
DidHandleMap: didHandleMap,
422
+
})
109
423
}
+84
-53
appview/state/pull.go
+84
-53
appview/state/pull.go
···
13
13
14
14
"tangled.sh/tangled.sh/core/api/tangled"
15
15
"tangled.sh/tangled.sh/core/appview"
16
-
"tangled.sh/tangled.sh/core/appview/auth"
17
16
"tangled.sh/tangled.sh/core/appview/db"
17
+
"tangled.sh/tangled.sh/core/appview/knotclient"
18
+
"tangled.sh/tangled.sh/core/appview/oauth"
18
19
"tangled.sh/tangled.sh/core/appview/pages"
19
20
"tangled.sh/tangled.sh/core/patchutil"
20
21
"tangled.sh/tangled.sh/core/types"
···
29
30
func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {
30
31
switch r.Method {
31
32
case http.MethodGet:
32
-
user := s.auth.GetUser(r)
33
+
user := s.oauth.GetUser(r)
33
34
f, err := s.fullyResolvedRepo(r)
34
35
if err != nil {
35
36
log.Println("failed to get repo and knot", err)
···
73
74
}
74
75
75
76
func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
76
-
user := s.auth.GetUser(r)
77
+
user := s.oauth.GetUser(r)
77
78
f, err := s.fullyResolvedRepo(r)
78
79
if err != nil {
79
80
log.Println("failed to get repo and knot", err)
···
143
144
}
144
145
}
145
146
146
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
147
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
147
148
if err != nil {
148
149
log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err)
149
150
return types.MergeCheckResponse{
···
215
216
repoName = f.RepoName
216
217
}
217
218
218
-
us, err := NewUnsignedClient(knot, s.config.Dev)
219
+
us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
219
220
if err != nil {
220
221
log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
221
222
return pages.Unknown
···
250
251
}
251
252
252
253
func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
253
-
user := s.auth.GetUser(r)
254
+
user := s.oauth.GetUser(r)
254
255
f, err := s.fullyResolvedRepo(r)
255
256
if err != nil {
256
257
log.Println("failed to get repo and knot", err)
···
298
299
}
299
300
300
301
func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
301
-
user := s.auth.GetUser(r)
302
+
user := s.oauth.GetUser(r)
302
303
303
304
f, err := s.fullyResolvedRepo(r)
304
305
if err != nil {
···
355
356
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
356
357
357
358
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
358
-
LoggedInUser: s.auth.GetUser(r),
359
+
LoggedInUser: s.oauth.GetUser(r),
359
360
RepoInfo: f.RepoInfo(s, user),
360
361
Pull: pull,
361
362
Round: roundIdInt,
···
397
398
}
398
399
399
400
func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
400
-
user := s.auth.GetUser(r)
401
+
user := s.oauth.GetUser(r)
401
402
params := r.URL.Query()
402
403
403
404
state := db.PullOpen
···
451
452
}
452
453
453
454
s.pages.RepoPulls(w, pages.RepoPullsParams{
454
-
LoggedInUser: s.auth.GetUser(r),
455
+
LoggedInUser: s.oauth.GetUser(r),
455
456
RepoInfo: f.RepoInfo(s, user),
456
457
Pulls: pulls,
457
458
DidHandleMap: didHandleMap,
···
461
462
}
462
463
463
464
func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {
464
-
user := s.auth.GetUser(r)
465
+
user := s.oauth.GetUser(r)
465
466
f, err := s.fullyResolvedRepo(r)
466
467
if err != nil {
467
468
log.Println("failed to get repo and knot", err)
···
519
520
}
520
521
521
522
atUri := f.RepoAt.String()
522
-
client, _ := s.auth.AuthorizedClient(r)
523
-
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
523
+
client, err := s.oauth.AuthorizedClient(r)
524
+
if err != nil {
525
+
log.Println("failed to get authorized client", err)
526
+
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
527
+
return
528
+
}
529
+
atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
524
530
Collection: tangled.RepoPullCommentNSID,
525
531
Repo: user.Did,
526
532
Rkey: appview.TID(),
···
568
574
}
569
575
570
576
func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
571
-
user := s.auth.GetUser(r)
577
+
user := s.oauth.GetUser(r)
572
578
f, err := s.fullyResolvedRepo(r)
573
579
if err != nil {
574
580
log.Println("failed to get repo and knot", err)
···
577
583
578
584
switch r.Method {
579
585
case http.MethodGet:
580
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
586
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
581
587
if err != nil {
582
588
log.Printf("failed to create unsigned client for %s", f.Knot)
583
589
s.pages.Error503(w)
···
646
652
return
647
653
}
648
654
649
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
655
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
650
656
if err != nil {
651
657
log.Printf("failed to create unsigned client to %s: %v", f.Knot, err)
652
658
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
···
689
695
}
690
696
}
691
697
692
-
func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) {
698
+
func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, sourceBranch string) {
693
699
pullSource := &db.PullSource{
694
700
Branch: sourceBranch,
695
701
}
···
698
704
}
699
705
700
706
// Generate a patch using /compare
701
-
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
707
+
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
702
708
if err != nil {
703
709
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
704
710
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
723
729
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource)
724
730
}
725
731
726
-
func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) {
732
+
func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, patch string) {
727
733
if !patchutil.IsPatchValid(patch) {
728
734
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
729
735
return
···
732
738
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil)
733
739
}
734
740
735
-
func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) {
741
+
func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string) {
736
742
fork, err := db.GetForkByDid(s.db, user.Did, forkRepo)
737
743
if errors.Is(err, sql.ErrNoRows) {
738
744
s.pages.Notice(w, "pull", "No such fork.")
···
750
756
return
751
757
}
752
758
753
-
sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev)
759
+
sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev)
754
760
if err != nil {
755
761
log.Println("failed to create signed client:", err)
756
762
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
757
763
return
758
764
}
759
765
760
-
us, err := NewUnsignedClient(fork.Knot, s.config.Dev)
766
+
us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev)
761
767
if err != nil {
762
768
log.Println("failed to create unsigned client:", err)
763
769
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
816
822
w http.ResponseWriter,
817
823
r *http.Request,
818
824
f *FullyResolvedRepo,
819
-
user *auth.User,
825
+
user *oauth.User,
820
826
title, body, targetBranch string,
821
827
patch string,
822
828
sourceRev string,
···
870
876
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
871
877
return
872
878
}
873
-
client, _ := s.auth.AuthorizedClient(r)
874
-
pullId, err := db.NextPullId(s.db, f.RepoAt)
879
+
client, err := s.oauth.AuthorizedClient(r)
880
+
if err != nil {
881
+
log.Println("failed to get authorized client", err)
882
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
883
+
return
884
+
}
885
+
pullId, err := db.NextPullId(tx, f.RepoAt)
875
886
if err != nil {
876
887
log.Println("failed to get pull id", err)
877
888
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
878
889
return
879
890
}
880
891
881
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
892
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
882
893
Collection: tangled.RepoPullNSID,
883
894
Repo: user.Did,
884
895
Rkey: rkey,
···
893
904
},
894
905
},
895
906
})
907
+
if err != nil {
908
+
log.Println("failed to create pull request", err)
909
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
910
+
return
911
+
}
896
912
897
-
if err != nil {
913
+
if err = tx.Commit(); err != nil {
898
914
log.Println("failed to create pull request", err)
899
915
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
900
916
return
···
929
945
}
930
946
931
947
func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
932
-
user := s.auth.GetUser(r)
948
+
user := s.oauth.GetUser(r)
933
949
f, err := s.fullyResolvedRepo(r)
934
950
if err != nil {
935
951
log.Println("failed to get repo and knot", err)
···
942
958
}
943
959
944
960
func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
945
-
user := s.auth.GetUser(r)
961
+
user := s.oauth.GetUser(r)
946
962
f, err := s.fullyResolvedRepo(r)
947
963
if err != nil {
948
964
log.Println("failed to get repo and knot", err)
949
965
return
950
966
}
951
967
952
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
968
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
953
969
if err != nil {
954
970
log.Printf("failed to create unsigned client for %s", f.Knot)
955
971
s.pages.Error503(w)
···
982
998
}
983
999
984
1000
func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
985
-
user := s.auth.GetUser(r)
1001
+
user := s.oauth.GetUser(r)
986
1002
f, err := s.fullyResolvedRepo(r)
987
1003
if err != nil {
988
1004
log.Println("failed to get repo and knot", err)
···
1002
1018
}
1003
1019
1004
1020
func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1005
-
user := s.auth.GetUser(r)
1021
+
user := s.oauth.GetUser(r)
1006
1022
1007
1023
f, err := s.fullyResolvedRepo(r)
1008
1024
if err != nil {
···
1019
1035
return
1020
1036
}
1021
1037
1022
-
sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev)
1038
+
sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev)
1023
1039
if err != nil {
1024
1040
log.Printf("failed to create unsigned client for %s", repo.Knot)
1025
1041
s.pages.Error503(w)
···
1046
1062
return
1047
1063
}
1048
1064
1049
-
targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
1065
+
targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1050
1066
if err != nil {
1051
1067
log.Printf("failed to create unsigned client for target knot %s", f.Knot)
1052
1068
s.pages.Error503(w)
···
1081
1097
}
1082
1098
1083
1099
func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1084
-
user := s.auth.GetUser(r)
1100
+
user := s.oauth.GetUser(r)
1085
1101
f, err := s.fullyResolvedRepo(r)
1086
1102
if err != nil {
1087
1103
log.Println("failed to get repo and knot", err)
···
1117
1133
}
1118
1134
1119
1135
func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1120
-
user := s.auth.GetUser(r)
1136
+
user := s.oauth.GetUser(r)
1121
1137
1122
1138
pull, ok := r.Context().Value("pull").(*db.Pull)
1123
1139
if !ok {
···
1159
1175
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.")
1160
1176
return
1161
1177
}
1162
-
client, _ := s.auth.AuthorizedClient(r)
1178
+
client, err := s.oauth.AuthorizedClient(r)
1179
+
if err != nil {
1180
+
log.Println("failed to get authorized client", err)
1181
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1182
+
return
1183
+
}
1163
1184
1164
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1185
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1165
1186
if err != nil {
1166
1187
// failed to get record
1167
1188
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
1168
1189
return
1169
1190
}
1170
1191
1171
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1192
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1172
1193
Collection: tangled.RepoPullNSID,
1173
1194
Repo: user.Did,
1174
1195
Rkey: pull.Rkey,
···
1200
1221
}
1201
1222
1202
1223
func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1203
-
user := s.auth.GetUser(r)
1224
+
user := s.oauth.GetUser(r)
1204
1225
1205
1226
pull, ok := r.Context().Value("pull").(*db.Pull)
1206
1227
if !ok {
···
1227
1248
return
1228
1249
}
1229
1250
1230
-
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
1251
+
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1231
1252
if err != nil {
1232
1253
log.Printf("failed to create client for %s: %s", f.Knot, err)
1233
1254
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
1268
1289
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1269
1290
return
1270
1291
}
1271
-
client, _ := s.auth.AuthorizedClient(r)
1292
+
client, err := s.oauth.AuthorizedClient(r)
1293
+
if err != nil {
1294
+
log.Println("failed to authorize client")
1295
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1296
+
return
1297
+
}
1272
1298
1273
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1299
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1274
1300
if err != nil {
1275
1301
// failed to get record
1276
1302
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
···
1280
1306
recordPullSource := &tangled.RepoPull_Source{
1281
1307
Branch: pull.PullSource.Branch,
1282
1308
}
1283
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1309
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1284
1310
Collection: tangled.RepoPullNSID,
1285
1311
Repo: user.Did,
1286
1312
Rkey: pull.Rkey,
···
1313
1339
}
1314
1340
1315
1341
func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {
1316
-
user := s.auth.GetUser(r)
1342
+
user := s.oauth.GetUser(r)
1317
1343
1318
1344
pull, ok := r.Context().Value("pull").(*db.Pull)
1319
1345
if !ok {
···
1342
1368
}
1343
1369
1344
1370
// extract patch by performing compare
1345
-
ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev)
1371
+
ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev)
1346
1372
if err != nil {
1347
1373
log.Printf("failed to create client for %s: %s", forkRepo.Knot, err)
1348
1374
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
1357
1383
}
1358
1384
1359
1385
// update the hidden tracking branch to latest
1360
-
signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev)
1386
+
signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev)
1361
1387
if err != nil {
1362
1388
log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err)
1363
1389
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
1406
1432
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1407
1433
return
1408
1434
}
1409
-
client, _ := s.auth.AuthorizedClient(r)
1435
+
client, err := s.oauth.AuthorizedClient(r)
1436
+
if err != nil {
1437
+
log.Println("failed to get client")
1438
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1439
+
return
1440
+
}
1410
1441
1411
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1442
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1412
1443
if err != nil {
1413
1444
// failed to get record
1414
1445
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
···
1420
1451
Branch: pull.PullSource.Branch,
1421
1452
Repo: &repoAt,
1422
1453
}
1423
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1454
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1424
1455
Collection: tangled.RepoPullNSID,
1425
1456
Repo: user.Did,
1426
1457
Rkey: pull.Rkey,
···
1503
1534
log.Printf("failed to get primary email: %s", err)
1504
1535
}
1505
1536
1506
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
1537
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
1507
1538
if err != nil {
1508
1539
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
1509
1540
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
1533
1564
}
1534
1565
1535
1566
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
1536
-
user := s.auth.GetUser(r)
1567
+
user := s.oauth.GetUser(r)
1537
1568
1538
1569
f, err := s.fullyResolvedRepo(r)
1539
1570
if err != nil {
···
1587
1618
}
1588
1619
1589
1620
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
1590
-
user := s.auth.GetUser(r)
1621
+
user := s.oauth.GetUser(r)
1591
1622
1592
1623
f, err := s.fullyResolvedRepo(r)
1593
1624
if err != nil {
+99
-60
appview/state/repo.go
+99
-60
appview/state/repo.go
···
18
18
19
19
"tangled.sh/tangled.sh/core/api/tangled"
20
20
"tangled.sh/tangled.sh/core/appview"
21
-
"tangled.sh/tangled.sh/core/appview/auth"
22
21
"tangled.sh/tangled.sh/core/appview/db"
22
+
"tangled.sh/tangled.sh/core/appview/knotclient"
23
+
"tangled.sh/tangled.sh/core/appview/oauth"
23
24
"tangled.sh/tangled.sh/core/appview/pages"
24
25
"tangled.sh/tangled.sh/core/appview/pages/markup"
25
26
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
···
45
46
return
46
47
}
47
48
48
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
49
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
49
50
if err != nil {
50
51
log.Printf("failed to create unsigned client for %s", f.Knot)
51
52
s.pages.Error503(w)
···
119
120
120
121
emails := uniqueEmails(commitsTrunc)
121
122
122
-
user := s.auth.GetUser(r)
123
+
user := s.oauth.GetUser(r)
123
124
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
124
125
LoggedInUser: user,
125
126
RepoInfo: f.RepoInfo(s, user),
···
150
151
151
152
ref := chi.URLParam(r, "ref")
152
153
153
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
154
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
154
155
if err != nil {
155
156
log.Println("failed to create unsigned client", err)
156
157
return
···
190
191
tagMap[hash] = append(tagMap[hash], tag.Name)
191
192
}
192
193
193
-
user := s.auth.GetUser(r)
194
+
user := s.oauth.GetUser(r)
194
195
s.pages.RepoLog(w, pages.RepoLogParams{
195
196
LoggedInUser: user,
196
197
TagMap: tagMap,
···
209
210
return
210
211
}
211
212
212
-
user := s.auth.GetUser(r)
213
+
user := s.oauth.GetUser(r)
213
214
s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
214
215
RepoInfo: f.RepoInfo(s, user),
215
216
})
···
232
233
return
233
234
}
234
235
235
-
user := s.auth.GetUser(r)
236
+
user := s.oauth.GetUser(r)
236
237
237
238
switch r.Method {
238
239
case http.MethodGet:
···
241
242
})
242
243
return
243
244
case http.MethodPut:
244
-
user := s.auth.GetUser(r)
245
+
user := s.oauth.GetUser(r)
245
246
newDescription := r.FormValue("description")
246
-
client, _ := s.auth.AuthorizedClient(r)
247
+
client, err := s.oauth.AuthorizedClient(r)
248
+
if err != nil {
249
+
log.Println("failed to get client")
250
+
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
251
+
return
252
+
}
247
253
248
254
// optimistic update
249
255
err = db.UpdateDescription(s.db, string(repoAt), newDescription)
···
256
262
// this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
257
263
//
258
264
// SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
259
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey)
265
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
260
266
if err != nil {
261
267
// failed to get record
262
268
s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
263
269
return
264
270
}
265
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
271
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
266
272
Collection: tangled.RepoNSID,
267
273
Repo: user.Did,
268
274
Rkey: rkey,
···
303
309
}
304
310
ref := chi.URLParam(r, "ref")
305
311
protocol := "http"
306
-
if !s.config.Dev {
312
+
if !s.config.Core.Dev {
307
313
protocol = "https"
308
314
}
309
315
···
331
337
return
332
338
}
333
339
334
-
user := s.auth.GetUser(r)
340
+
user := s.oauth.GetUser(r)
335
341
s.pages.RepoCommit(w, pages.RepoCommitParams{
336
342
LoggedInUser: user,
337
343
RepoInfo: f.RepoInfo(s, user),
···
351
357
ref := chi.URLParam(r, "ref")
352
358
treePath := chi.URLParam(r, "*")
353
359
protocol := "http"
354
-
if !s.config.Dev {
360
+
if !s.config.Core.Dev {
355
361
protocol = "https"
356
362
}
357
363
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
···
380
386
return
381
387
}
382
388
383
-
user := s.auth.GetUser(r)
389
+
user := s.oauth.GetUser(r)
384
390
385
391
var breadcrumbs [][]string
386
392
breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
···
411
417
return
412
418
}
413
419
414
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
420
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
415
421
if err != nil {
416
422
log.Println("failed to create unsigned client", err)
417
423
return
···
451
457
}
452
458
}
453
459
454
-
user := s.auth.GetUser(r)
460
+
user := s.oauth.GetUser(r)
455
461
s.pages.RepoTags(w, pages.RepoTagsParams{
456
462
LoggedInUser: user,
457
463
RepoInfo: f.RepoInfo(s, user),
···
469
475
return
470
476
}
471
477
472
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
478
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
473
479
if err != nil {
474
480
log.Println("failed to create unsigned client", err)
475
481
return
···
511
517
return strings.Compare(a.Name, b.Name) * -1
512
518
})
513
519
514
-
user := s.auth.GetUser(r)
520
+
user := s.oauth.GetUser(r)
515
521
s.pages.RepoBranches(w, pages.RepoBranchesParams{
516
522
LoggedInUser: user,
517
523
RepoInfo: f.RepoInfo(s, user),
···
530
536
ref := chi.URLParam(r, "ref")
531
537
filePath := chi.URLParam(r, "*")
532
538
protocol := "http"
533
-
if !s.config.Dev {
539
+
if !s.config.Core.Dev {
534
540
protocol = "https"
535
541
}
536
542
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
568
574
showRendered = r.URL.Query().Get("code") != "true"
569
575
}
570
576
571
-
user := s.auth.GetUser(r)
577
+
user := s.oauth.GetUser(r)
572
578
s.pages.RepoBlob(w, pages.RepoBlobParams{
573
579
LoggedInUser: user,
574
580
RepoInfo: f.RepoInfo(s, user),
···
591
597
filePath := chi.URLParam(r, "*")
592
598
593
599
protocol := "http"
594
-
if !s.config.Dev {
600
+
if !s.config.Core.Dev {
595
601
protocol = "https"
596
602
}
597
603
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
652
658
return
653
659
}
654
660
655
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
661
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
656
662
if err != nil {
657
663
log.Println("failed to create client to ", f.Knot)
658
664
return
···
714
720
}
715
721
716
722
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
717
-
user := s.auth.GetUser(r)
723
+
user := s.oauth.GetUser(r)
718
724
719
725
f, err := s.fullyResolvedRepo(r)
720
726
if err != nil {
···
723
729
}
724
730
725
731
// remove record from pds
726
-
xrpcClient, _ := s.auth.AuthorizedClient(r)
732
+
xrpcClient, err := s.oauth.AuthorizedClient(r)
733
+
if err != nil {
734
+
log.Println("failed to get authorized client", err)
735
+
return
736
+
}
727
737
repoRkey := f.RepoAt.RecordKey().String()
728
-
_, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{
738
+
_, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
729
739
Collection: tangled.RepoNSID,
730
740
Repo: user.Did,
731
741
Rkey: repoRkey,
···
743
753
return
744
754
}
745
755
746
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
756
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
747
757
if err != nil {
748
758
log.Println("failed to create client to ", f.Knot)
749
759
return
···
838
848
return
839
849
}
840
850
841
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
851
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
842
852
if err != nil {
843
853
log.Println("failed to create client to ", f.Knot)
844
854
return
···
868
878
switch r.Method {
869
879
case http.MethodGet:
870
880
// for now, this is just pubkeys
871
-
user := s.auth.GetUser(r)
881
+
user := s.oauth.GetUser(r)
872
882
repoCollaborators, err := f.Collaborators(r.Context(), s)
873
883
if err != nil {
874
884
log.Println("failed to get collaborators", err)
···
884
894
885
895
var branchNames []string
886
896
var defaultBranch string
887
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
897
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
888
898
if err != nil {
889
899
log.Println("failed to create unsigned client", err)
890
900
} else {
···
1008
1018
return collaborators, nil
1009
1019
}
1010
1020
1011
-
func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo {
1021
+
func (f *FullyResolvedRepo) RepoInfo(s *State, u *oauth.User) repoinfo.RepoInfo {
1012
1022
isStarred := false
1013
1023
if u != nil {
1014
1024
isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
···
1051
1061
1052
1062
knot := f.Knot
1053
1063
var disableFork bool
1054
-
us, err := NewUnsignedClient(knot, s.config.Dev)
1064
+
us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
1055
1065
if err != nil {
1056
1066
log.Printf("failed to create unsigned client for %s: %v", knot, err)
1057
1067
} else {
···
1105
1115
}
1106
1116
1107
1117
func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
1108
-
user := s.auth.GetUser(r)
1118
+
user := s.oauth.GetUser(r)
1109
1119
f, err := s.fullyResolvedRepo(r)
1110
1120
if err != nil {
1111
1121
log.Println("failed to get repo and knot", err)
···
1159
1169
}
1160
1170
1161
1171
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
1162
-
user := s.auth.GetUser(r)
1172
+
user := s.oauth.GetUser(r)
1163
1173
f, err := s.fullyResolvedRepo(r)
1164
1174
if err != nil {
1165
1175
log.Println("failed to get repo and knot", err)
···
1195
1205
1196
1206
closed := tangled.RepoIssueStateClosed
1197
1207
1198
-
client, _ := s.auth.AuthorizedClient(r)
1199
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1208
+
client, err := s.oauth.AuthorizedClient(r)
1209
+
if err != nil {
1210
+
log.Println("failed to get authorized client", err)
1211
+
return
1212
+
}
1213
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1200
1214
Collection: tangled.RepoIssueStateNSID,
1201
1215
Repo: user.Did,
1202
1216
Rkey: appview.TID(),
···
1214
1228
return
1215
1229
}
1216
1230
1217
-
err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
1231
+
err = db.CloseIssue(s.db, f.RepoAt, issueIdInt)
1218
1232
if err != nil {
1219
1233
log.Println("failed to close issue", err)
1220
1234
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
···
1231
1245
}
1232
1246
1233
1247
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
1234
-
user := s.auth.GetUser(r)
1248
+
user := s.oauth.GetUser(r)
1235
1249
f, err := s.fullyResolvedRepo(r)
1236
1250
if err != nil {
1237
1251
log.Println("failed to get repo and knot", err)
···
1279
1293
}
1280
1294
1281
1295
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
1282
-
user := s.auth.GetUser(r)
1296
+
user := s.oauth.GetUser(r)
1283
1297
f, err := s.fullyResolvedRepo(r)
1284
1298
if err != nil {
1285
1299
log.Println("failed to get repo and knot", err)
···
1330
1344
}
1331
1345
1332
1346
atUri := f.RepoAt.String()
1333
-
client, _ := s.auth.AuthorizedClient(r)
1334
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1347
+
client, err := s.oauth.AuthorizedClient(r)
1348
+
if err != nil {
1349
+
log.Println("failed to get authorized client", err)
1350
+
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1351
+
return
1352
+
}
1353
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1335
1354
Collection: tangled.RepoIssueCommentNSID,
1336
1355
Repo: user.Did,
1337
1356
Rkey: rkey,
···
1358
1377
}
1359
1378
1360
1379
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
1361
-
user := s.auth.GetUser(r)
1380
+
user := s.oauth.GetUser(r)
1362
1381
f, err := s.fullyResolvedRepo(r)
1363
1382
if err != nil {
1364
1383
log.Println("failed to get repo and knot", err)
···
1417
1436
}
1418
1437
1419
1438
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
1420
-
user := s.auth.GetUser(r)
1439
+
user := s.oauth.GetUser(r)
1421
1440
f, err := s.fullyResolvedRepo(r)
1422
1441
if err != nil {
1423
1442
log.Println("failed to get repo and knot", err)
···
1469
1488
case http.MethodPost:
1470
1489
// extract form value
1471
1490
newBody := r.FormValue("body")
1472
-
client, _ := s.auth.AuthorizedClient(r)
1491
+
client, err := s.oauth.AuthorizedClient(r)
1492
+
if err != nil {
1493
+
log.Println("failed to get authorized client", err)
1494
+
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1495
+
return
1496
+
}
1473
1497
rkey := comment.Rkey
1474
1498
1475
1499
// optimistic update
···
1484
1508
// rkey is optional, it was introduced later
1485
1509
if comment.Rkey != "" {
1486
1510
// update the record on pds
1487
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey)
1511
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey)
1488
1512
if err != nil {
1489
1513
// failed to get record
1490
1514
log.Println(err, rkey)
···
1499
1523
createdAt := record["createdAt"].(string)
1500
1524
commentIdInt64 := int64(commentIdInt)
1501
1525
1502
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1526
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1503
1527
Collection: tangled.RepoIssueCommentNSID,
1504
1528
Repo: user.Did,
1505
1529
Rkey: rkey,
···
1542
1566
}
1543
1567
1544
1568
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
1545
-
user := s.auth.GetUser(r)
1569
+
user := s.oauth.GetUser(r)
1546
1570
f, err := s.fullyResolvedRepo(r)
1547
1571
if err != nil {
1548
1572
log.Println("failed to get repo and knot", err)
···
1599
1623
1600
1624
// delete from pds
1601
1625
if comment.Rkey != "" {
1602
-
client, _ := s.auth.AuthorizedClient(r)
1603
-
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
1626
+
client, err := s.oauth.AuthorizedClient(r)
1627
+
if err != nil {
1628
+
log.Println("failed to get authorized client", err)
1629
+
s.pages.Notice(w, "issue-comment", "Failed to delete comment.")
1630
+
return
1631
+
}
1632
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
1604
1633
Collection: tangled.GraphFollowNSID,
1605
1634
Repo: user.Did,
1606
1635
Rkey: comment.Rkey,
···
1647
1676
page = pagination.FirstPage()
1648
1677
}
1649
1678
1650
-
user := s.auth.GetUser(r)
1679
+
user := s.oauth.GetUser(r)
1651
1680
f, err := s.fullyResolvedRepo(r)
1652
1681
if err != nil {
1653
1682
log.Println("failed to get repo and knot", err)
···
1676
1705
}
1677
1706
1678
1707
s.pages.RepoIssues(w, pages.RepoIssuesParams{
1679
-
LoggedInUser: s.auth.GetUser(r),
1708
+
LoggedInUser: s.oauth.GetUser(r),
1680
1709
RepoInfo: f.RepoInfo(s, user),
1681
1710
Issues: issues,
1682
1711
DidHandleMap: didHandleMap,
···
1687
1716
}
1688
1717
1689
1718
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
1690
-
user := s.auth.GetUser(r)
1719
+
user := s.oauth.GetUser(r)
1691
1720
1692
1721
f, err := s.fullyResolvedRepo(r)
1693
1722
if err != nil {
···
1735
1764
return
1736
1765
}
1737
1766
1738
-
client, _ := s.auth.AuthorizedClient(r)
1767
+
client, err := s.oauth.AuthorizedClient(r)
1768
+
if err != nil {
1769
+
log.Println("failed to get authorized client", err)
1770
+
s.pages.Notice(w, "issues", "Failed to create issue.")
1771
+
return
1772
+
}
1739
1773
atUri := f.RepoAt.String()
1740
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1774
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1741
1775
Collection: tangled.RepoIssueNSID,
1742
1776
Repo: user.Did,
1743
1777
Rkey: appview.TID(),
···
1770
1804
}
1771
1805
1772
1806
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
1773
-
user := s.auth.GetUser(r)
1807
+
user := s.oauth.GetUser(r)
1774
1808
f, err := s.fullyResolvedRepo(r)
1775
1809
if err != nil {
1776
1810
log.Printf("failed to resolve source repo: %v", err)
···
1779
1813
1780
1814
switch r.Method {
1781
1815
case http.MethodGet:
1782
-
user := s.auth.GetUser(r)
1816
+
user := s.oauth.GetUser(r)
1783
1817
knots, err := s.enforcer.GetDomainsForUser(user.Did)
1784
1818
if err != nil {
1785
1819
s.pages.Notice(w, "repo", "Invalid user account.")
···
1829
1863
return
1830
1864
}
1831
1865
1832
-
client, err := NewSignedClient(knot, secret, s.config.Dev)
1866
+
client, err := knotclient.NewSignedClient(knot, secret, s.config.Core.Dev)
1833
1867
if err != nil {
1834
1868
s.pages.Notice(w, "repo", "Failed to reach knot server.")
1835
1869
return
1836
1870
}
1837
1871
1838
1872
var uri string
1839
-
if s.config.Dev {
1873
+
if s.config.Core.Dev {
1840
1874
uri = "http"
1841
1875
} else {
1842
1876
uri = "https"
···
1883
1917
// continue
1884
1918
}
1885
1919
1886
-
xrpcClient, _ := s.auth.AuthorizedClient(r)
1920
+
xrpcClient, err := s.oauth.AuthorizedClient(r)
1921
+
if err != nil {
1922
+
log.Println("failed to get authorized client", err)
1923
+
s.pages.Notice(w, "repo", "Failed to create repository.")
1924
+
return
1925
+
}
1887
1926
1888
1927
createdAt := time.Now().Format(time.RFC3339)
1889
-
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
1928
+
atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1890
1929
Collection: tangled.RepoNSID,
1891
1930
Repo: user.Did,
1892
1931
Rkey: rkey,
+4
-3
appview/state/repo_util.go
+4
-3
appview/state/repo_util.go
···
12
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
13
"github.com/go-chi/chi/v5"
14
14
"github.com/go-git/go-git/v5/plumbing/object"
15
-
"tangled.sh/tangled.sh/core/appview/auth"
16
15
"tangled.sh/tangled.sh/core/appview/db"
16
+
"tangled.sh/tangled.sh/core/appview/knotclient"
17
+
"tangled.sh/tangled.sh/core/appview/oauth"
17
18
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
18
19
)
19
20
···
45
46
ref := chi.URLParam(r, "ref")
46
47
47
48
if ref == "" {
48
-
us, err := NewUnsignedClient(knot, s.config.Dev)
49
+
us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
49
50
if err != nil {
50
51
return nil, err
51
52
}
···
73
74
}, nil
74
75
}
75
76
76
-
func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo {
77
+
func RolesInRepo(s *State, u *oauth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo {
77
78
if u != nil {
78
79
r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
79
80
return repoinfo.RolesInRepo{r}
+42
-20
appview/state/router.go
+42
-20
appview/state/router.go
···
5
5
"strings"
6
6
7
7
"github.com/go-chi/chi/v5"
8
+
"github.com/gorilla/sessions"
8
9
"tangled.sh/tangled.sh/core/appview/middleware"
10
+
oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler"
9
11
"tangled.sh/tangled.sh/core/appview/settings"
10
12
"tangled.sh/tangled.sh/core/appview/state/userutil"
11
13
)
···
53
55
r.Use(StripLeadingAt)
54
56
55
57
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
56
-
r.Get("/", s.ProfilePage)
58
+
r.Get("/", s.Profile)
59
+
57
60
r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) {
61
+
r.Use(GoImport(s))
62
+
58
63
r.Get("/", s.RepoIndex)
59
64
r.Get("/commits/{ref}", s.RepoLog)
60
65
r.Route("/tree/{ref}", func(r chi.Router) {
···
66
71
r.Route("/tags", func(r chi.Router) {
67
72
r.Get("/", s.RepoTags)
68
73
r.Route("/{tag}", func(r chi.Router) {
69
-
r.Use(middleware.AuthMiddleware(s.auth))
74
+
r.Use(middleware.AuthMiddleware(s.oauth))
70
75
// require auth to download for now
71
76
r.Get("/download/{file}", s.DownloadArtifact)
72
77
···
89
94
r.Get("/{issue}", s.RepoSingleIssue)
90
95
91
96
r.Group(func(r chi.Router) {
92
-
r.Use(middleware.AuthMiddleware(s.auth))
97
+
r.Use(middleware.AuthMiddleware(s.oauth))
93
98
r.Get("/new", s.NewIssue)
94
99
r.Post("/new", s.NewIssue)
95
100
r.Post("/{issue}/comment", s.NewIssueComment)
···
105
110
})
106
111
107
112
r.Route("/fork", func(r chi.Router) {
108
-
r.Use(middleware.AuthMiddleware(s.auth))
113
+
r.Use(middleware.AuthMiddleware(s.oauth))
109
114
r.Get("/", s.ForkRepo)
110
115
r.Post("/", s.ForkRepo)
111
116
})
112
117
113
118
r.Route("/pulls", func(r chi.Router) {
114
119
r.Get("/", s.RepoPulls)
115
-
r.With(middleware.AuthMiddleware(s.auth)).Route("/new", func(r chi.Router) {
120
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) {
116
121
r.Get("/", s.NewPull)
117
122
r.Get("/patch-upload", s.PatchUploadFragment)
118
123
r.Post("/validate-patch", s.ValidatePatch)
···
130
135
r.Get("/", s.RepoPullPatch)
131
136
r.Get("/interdiff", s.RepoPullInterdiff)
132
137
r.Get("/actions", s.PullActions)
133
-
r.With(middleware.AuthMiddleware(s.auth)).Route("/comment", func(r chi.Router) {
138
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) {
134
139
r.Get("/", s.PullComment)
135
140
r.Post("/", s.PullComment)
136
141
})
···
141
146
})
142
147
143
148
r.Group(func(r chi.Router) {
144
-
r.Use(middleware.AuthMiddleware(s.auth))
149
+
r.Use(middleware.AuthMiddleware(s.oauth))
145
150
r.Route("/resubmit", func(r chi.Router) {
146
151
r.Get("/", s.ResubmitPull)
147
152
r.Post("/", s.ResubmitPull)
···
164
169
165
170
// settings routes, needs auth
166
171
r.Group(func(r chi.Router) {
167
-
r.Use(middleware.AuthMiddleware(s.auth))
172
+
r.Use(middleware.AuthMiddleware(s.oauth))
168
173
// repo description can only be edited by owner
169
174
r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
170
175
r.Put("/", s.RepoDescription)
···
195
200
196
201
r.Get("/", s.Timeline)
197
202
198
-
r.With(middleware.AuthMiddleware(s.auth)).Post("/logout", s.Logout)
199
-
200
-
r.Route("/login", func(r chi.Router) {
201
-
r.Get("/", s.Login)
202
-
r.Post("/", s.Login)
203
-
})
203
+
r.With(middleware.AuthMiddleware(s.oauth)).Post("/logout", s.Logout)
204
204
205
205
r.Route("/knots", func(r chi.Router) {
206
-
r.Use(middleware.AuthMiddleware(s.auth))
206
+
r.Use(middleware.AuthMiddleware(s.oauth))
207
207
r.Get("/", s.Knots)
208
208
r.Post("/key", s.RegistrationKey)
209
209
···
221
221
222
222
r.Route("/repo", func(r chi.Router) {
223
223
r.Route("/new", func(r chi.Router) {
224
-
r.Use(middleware.AuthMiddleware(s.auth))
224
+
r.Use(middleware.AuthMiddleware(s.oauth))
225
225
r.Get("/", s.NewRepo)
226
226
r.Post("/", s.NewRepo)
227
227
})
228
228
// r.Post("/import", s.ImportRepo)
229
229
})
230
230
231
-
r.With(middleware.AuthMiddleware(s.auth)).Route("/follow", func(r chi.Router) {
231
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) {
232
232
r.Post("/", s.Follow)
233
233
r.Delete("/", s.Follow)
234
234
})
235
235
236
-
r.With(middleware.AuthMiddleware(s.auth)).Route("/star", func(r chi.Router) {
236
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) {
237
237
r.Post("/", s.Star)
238
238
r.Delete("/", s.Star)
239
239
})
240
240
241
-
r.Mount("/settings", s.SettingsRouter())
241
+
r.Route("/profile", func(r chi.Router) {
242
+
r.Use(middleware.AuthMiddleware(s.oauth))
243
+
r.Get("/edit-bio", s.EditBioFragment)
244
+
r.Get("/edit-pins", s.EditPinsFragment)
245
+
r.Post("/bio", s.UpdateProfileBio)
246
+
r.Post("/pins", s.UpdateProfilePins)
247
+
})
242
248
249
+
r.Mount("/settings", s.SettingsRouter())
250
+
r.Mount("/", s.OAuthRouter())
243
251
r.Get("/keys/{user}", s.Keys)
244
252
245
253
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
···
248
256
return r
249
257
}
250
258
259
+
func (s *State) OAuthRouter() http.Handler {
260
+
oauth := &oauthhandler.OAuthHandler{
261
+
Config: s.config,
262
+
Pages: s.pages,
263
+
Resolver: s.resolver,
264
+
Db: s.db,
265
+
Store: sessions.NewCookieStore([]byte(s.config.Core.CookieSecret)),
266
+
OAuth: s.oauth,
267
+
Enforcer: s.enforcer,
268
+
}
269
+
270
+
return oauth.Router()
271
+
}
272
+
251
273
func (s *State) SettingsRouter() http.Handler {
252
274
settings := &settings.Settings{
253
275
Db: s.db,
254
-
Auth: s.auth,
276
+
OAuth: s.oauth,
255
277
Pages: s.pages,
256
278
Config: s.config,
257
279
}
-489
appview/state/signer.go
-489
appview/state/signer.go
···
1
-
package state
2
-
3
-
import (
4
-
"bytes"
5
-
"crypto/hmac"
6
-
"crypto/sha256"
7
-
"encoding/hex"
8
-
"encoding/json"
9
-
"fmt"
10
-
"io"
11
-
"log"
12
-
"net/http"
13
-
"net/url"
14
-
"strconv"
15
-
"time"
16
-
17
-
"tangled.sh/tangled.sh/core/types"
18
-
)
19
-
20
-
type SignerTransport struct {
21
-
Secret string
22
-
}
23
-
24
-
func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
25
-
timestamp := time.Now().Format(time.RFC3339)
26
-
mac := hmac.New(sha256.New, []byte(s.Secret))
27
-
message := req.Method + req.URL.Path + timestamp
28
-
mac.Write([]byte(message))
29
-
signature := hex.EncodeToString(mac.Sum(nil))
30
-
req.Header.Set("X-Signature", signature)
31
-
req.Header.Set("X-Timestamp", timestamp)
32
-
return http.DefaultTransport.RoundTrip(req)
33
-
}
34
-
35
-
type SignedClient struct {
36
-
Secret string
37
-
Url *url.URL
38
-
client *http.Client
39
-
}
40
-
41
-
func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) {
42
-
client := &http.Client{
43
-
Timeout: 5 * time.Second,
44
-
Transport: SignerTransport{
45
-
Secret: secret,
46
-
},
47
-
}
48
-
49
-
scheme := "https"
50
-
if dev {
51
-
scheme = "http"
52
-
}
53
-
url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
54
-
if err != nil {
55
-
return nil, err
56
-
}
57
-
58
-
signedClient := &SignedClient{
59
-
Secret: secret,
60
-
client: client,
61
-
Url: url,
62
-
}
63
-
64
-
return signedClient, nil
65
-
}
66
-
67
-
func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
68
-
return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
69
-
}
70
-
71
-
func (s *SignedClient) Init(did string) (*http.Response, error) {
72
-
const (
73
-
Method = "POST"
74
-
Endpoint = "/init"
75
-
)
76
-
77
-
body, _ := json.Marshal(map[string]any{
78
-
"did": did,
79
-
})
80
-
81
-
req, err := s.newRequest(Method, Endpoint, body)
82
-
if err != nil {
83
-
return nil, err
84
-
}
85
-
86
-
return s.client.Do(req)
87
-
}
88
-
89
-
func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) {
90
-
const (
91
-
Method = "PUT"
92
-
Endpoint = "/repo/new"
93
-
)
94
-
95
-
body, _ := json.Marshal(map[string]any{
96
-
"did": did,
97
-
"name": repoName,
98
-
"default_branch": defaultBranch,
99
-
})
100
-
101
-
req, err := s.newRequest(Method, Endpoint, body)
102
-
if err != nil {
103
-
return nil, err
104
-
}
105
-
106
-
return s.client.Do(req)
107
-
}
108
-
109
-
func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
110
-
const (
111
-
Method = "POST"
112
-
Endpoint = "/repo/fork"
113
-
)
114
-
115
-
body, _ := json.Marshal(map[string]any{
116
-
"did": ownerDid,
117
-
"source": source,
118
-
"name": name,
119
-
})
120
-
121
-
req, err := s.newRequest(Method, Endpoint, body)
122
-
if err != nil {
123
-
return nil, err
124
-
}
125
-
126
-
return s.client.Do(req)
127
-
}
128
-
129
-
func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
130
-
const (
131
-
Method = "DELETE"
132
-
Endpoint = "/repo"
133
-
)
134
-
135
-
body, _ := json.Marshal(map[string]any{
136
-
"did": did,
137
-
"name": repoName,
138
-
})
139
-
140
-
req, err := s.newRequest(Method, Endpoint, body)
141
-
if err != nil {
142
-
return nil, err
143
-
}
144
-
145
-
return s.client.Do(req)
146
-
}
147
-
148
-
func (s *SignedClient) AddMember(did string) (*http.Response, error) {
149
-
const (
150
-
Method = "PUT"
151
-
Endpoint = "/member/add"
152
-
)
153
-
154
-
body, _ := json.Marshal(map[string]any{
155
-
"did": did,
156
-
})
157
-
158
-
req, err := s.newRequest(Method, Endpoint, body)
159
-
if err != nil {
160
-
return nil, err
161
-
}
162
-
163
-
return s.client.Do(req)
164
-
}
165
-
166
-
func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) {
167
-
const (
168
-
Method = "PUT"
169
-
)
170
-
endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
171
-
172
-
body, _ := json.Marshal(map[string]any{
173
-
"branch": branch,
174
-
})
175
-
176
-
req, err := s.newRequest(Method, endpoint, body)
177
-
if err != nil {
178
-
return nil, err
179
-
}
180
-
181
-
return s.client.Do(req)
182
-
}
183
-
184
-
func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
185
-
const (
186
-
Method = "POST"
187
-
)
188
-
endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
189
-
190
-
body, _ := json.Marshal(map[string]any{
191
-
"did": memberDid,
192
-
})
193
-
194
-
req, err := s.newRequest(Method, endpoint, body)
195
-
if err != nil {
196
-
return nil, err
197
-
}
198
-
199
-
return s.client.Do(req)
200
-
}
201
-
202
-
func (s *SignedClient) Merge(
203
-
patch []byte,
204
-
ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
205
-
) (*http.Response, error) {
206
-
const (
207
-
Method = "POST"
208
-
)
209
-
endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
210
-
211
-
mr := types.MergeRequest{
212
-
Branch: branch,
213
-
CommitMessage: commitMessage,
214
-
CommitBody: commitBody,
215
-
AuthorName: authorName,
216
-
AuthorEmail: authorEmail,
217
-
Patch: string(patch),
218
-
}
219
-
220
-
body, _ := json.Marshal(mr)
221
-
222
-
req, err := s.newRequest(Method, endpoint, body)
223
-
if err != nil {
224
-
return nil, err
225
-
}
226
-
227
-
return s.client.Do(req)
228
-
}
229
-
230
-
func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
231
-
const (
232
-
Method = "POST"
233
-
)
234
-
endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
235
-
236
-
body, _ := json.Marshal(map[string]any{
237
-
"patch": string(patch),
238
-
"branch": branch,
239
-
})
240
-
241
-
req, err := s.newRequest(Method, endpoint, body)
242
-
if err != nil {
243
-
return nil, err
244
-
}
245
-
246
-
return s.client.Do(req)
247
-
}
248
-
249
-
func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) {
250
-
const (
251
-
Method = "POST"
252
-
)
253
-
endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch))
254
-
255
-
req, err := s.newRequest(Method, endpoint, nil)
256
-
if err != nil {
257
-
return nil, err
258
-
}
259
-
260
-
return s.client.Do(req)
261
-
}
262
-
263
-
type UnsignedClient struct {
264
-
Url *url.URL
265
-
client *http.Client
266
-
}
267
-
268
-
func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) {
269
-
client := &http.Client{
270
-
Timeout: 5 * time.Second,
271
-
}
272
-
273
-
scheme := "https"
274
-
if dev {
275
-
scheme = "http"
276
-
}
277
-
url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
278
-
if err != nil {
279
-
return nil, err
280
-
}
281
-
282
-
unsignedClient := &UnsignedClient{
283
-
client: client,
284
-
Url: url,
285
-
}
286
-
287
-
return unsignedClient, nil
288
-
}
289
-
290
-
func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) {
291
-
reqUrl := us.Url.JoinPath(endpoint)
292
-
293
-
// add query parameters
294
-
if query != nil {
295
-
reqUrl.RawQuery = query.Encode()
296
-
}
297
-
298
-
return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body))
299
-
}
300
-
301
-
func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) {
302
-
const (
303
-
Method = "GET"
304
-
)
305
-
306
-
endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref)
307
-
if ref == "" {
308
-
endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName)
309
-
}
310
-
311
-
req, err := us.newRequest(Method, endpoint, nil, nil)
312
-
if err != nil {
313
-
return nil, err
314
-
}
315
-
316
-
return us.client.Do(req)
317
-
}
318
-
319
-
func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*http.Response, error) {
320
-
const (
321
-
Method = "GET"
322
-
)
323
-
324
-
endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref))
325
-
326
-
query := url.Values{}
327
-
query.Add("page", strconv.Itoa(page))
328
-
query.Add("per_page", strconv.Itoa(60))
329
-
330
-
req, err := us.newRequest(Method, endpoint, query, nil)
331
-
if err != nil {
332
-
return nil, err
333
-
}
334
-
335
-
return us.client.Do(req)
336
-
}
337
-
338
-
func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) {
339
-
const (
340
-
Method = "GET"
341
-
)
342
-
343
-
endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName)
344
-
345
-
req, err := us.newRequest(Method, endpoint, nil, nil)
346
-
if err != nil {
347
-
return nil, err
348
-
}
349
-
350
-
return us.client.Do(req)
351
-
}
352
-
353
-
func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) {
354
-
const (
355
-
Method = "GET"
356
-
)
357
-
358
-
endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName)
359
-
360
-
req, err := us.newRequest(Method, endpoint, nil, nil)
361
-
if err != nil {
362
-
return nil, err
363
-
}
364
-
365
-
resp, err := us.client.Do(req)
366
-
if err != nil {
367
-
return nil, err
368
-
}
369
-
370
-
body, err := io.ReadAll(resp.Body)
371
-
if err != nil {
372
-
return nil, err
373
-
}
374
-
375
-
var result types.RepoTagsResponse
376
-
err = json.Unmarshal(body, &result)
377
-
if err != nil {
378
-
return nil, err
379
-
}
380
-
381
-
return &result, nil
382
-
}
383
-
384
-
func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
385
-
const (
386
-
Method = "GET"
387
-
)
388
-
389
-
endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch))
390
-
391
-
req, err := us.newRequest(Method, endpoint, nil, nil)
392
-
if err != nil {
393
-
return nil, err
394
-
}
395
-
396
-
return us.client.Do(req)
397
-
}
398
-
399
-
func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) {
400
-
const (
401
-
Method = "GET"
402
-
)
403
-
404
-
endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
405
-
406
-
req, err := us.newRequest(Method, endpoint, nil, nil)
407
-
if err != nil {
408
-
return nil, err
409
-
}
410
-
411
-
resp, err := us.client.Do(req)
412
-
if err != nil {
413
-
return nil, err
414
-
}
415
-
defer resp.Body.Close()
416
-
417
-
var defaultBranch types.RepoDefaultBranchResponse
418
-
if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil {
419
-
return nil, err
420
-
}
421
-
422
-
return &defaultBranch, nil
423
-
}
424
-
425
-
func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) {
426
-
const (
427
-
Method = "GET"
428
-
Endpoint = "/capabilities"
429
-
)
430
-
431
-
req, err := us.newRequest(Method, Endpoint, nil, nil)
432
-
if err != nil {
433
-
return nil, err
434
-
}
435
-
436
-
resp, err := us.client.Do(req)
437
-
if err != nil {
438
-
return nil, err
439
-
}
440
-
defer resp.Body.Close()
441
-
442
-
var capabilities types.Capabilities
443
-
if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil {
444
-
return nil, err
445
-
}
446
-
447
-
return &capabilities, nil
448
-
}
449
-
450
-
func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) {
451
-
const (
452
-
Method = "GET"
453
-
)
454
-
455
-
endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2))
456
-
457
-
req, err := us.newRequest(Method, endpoint, nil, nil)
458
-
if err != nil {
459
-
return nil, fmt.Errorf("Failed to create request.")
460
-
}
461
-
462
-
compareResp, err := us.client.Do(req)
463
-
if err != nil {
464
-
return nil, fmt.Errorf("Failed to create request.")
465
-
}
466
-
defer compareResp.Body.Close()
467
-
468
-
switch compareResp.StatusCode {
469
-
case 404:
470
-
case 400:
471
-
return nil, fmt.Errorf("Branch comparisons not supported on this knot.")
472
-
}
473
-
474
-
respBody, err := io.ReadAll(compareResp.Body)
475
-
if err != nil {
476
-
log.Println("failed to compare across branches")
477
-
return nil, fmt.Errorf("Failed to compare branches.")
478
-
}
479
-
defer compareResp.Body.Close()
480
-
481
-
var formatPatchResponse types.RepoFormatPatchResponse
482
-
err = json.Unmarshal(respBody, &formatPatchResponse)
483
-
if err != nil {
484
-
log.Println("failed to unmarshal format-patch response", err)
485
-
return nil, fmt.Errorf("failed to compare branches.")
486
-
}
487
-
488
-
return &formatPatchResponse, nil
489
-
}
+9
-4
appview/state/star.go
+9
-4
appview/state/star.go
···
15
15
)
16
16
17
17
func (s *State) Star(w http.ResponseWriter, r *http.Request) {
18
-
currentUser := s.auth.GetUser(r)
18
+
currentUser := s.oauth.GetUser(r)
19
19
20
20
subject := r.URL.Query().Get("subject")
21
21
if subject == "" {
···
29
29
return
30
30
}
31
31
32
-
client, _ := s.auth.AuthorizedClient(r)
32
+
client, err := s.oauth.AuthorizedClient(r)
33
+
if err != nil {
34
+
log.Println("failed to authorize client", err)
35
+
return
36
+
}
33
37
34
38
switch r.Method {
35
39
case http.MethodPost:
36
40
createdAt := time.Now().Format(time.RFC3339)
37
41
rkey := appview.TID()
38
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
42
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
39
43
Collection: tangled.FeedStarNSID,
40
44
Repo: currentUser.Did,
41
45
Rkey: rkey,
···
80
84
return
81
85
}
82
86
83
-
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
87
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
84
88
Collection: tangled.FeedStarNSID,
85
89
Repo: currentUser.Did,
86
90
Rkey: star.Rkey,
···
100
104
starCount, err := db.GetStarCount(s.db, subjectUri)
101
105
if err != nil {
102
106
log.Println("failed to get star count for ", subjectUri)
107
+
return
103
108
}
104
109
105
110
s.pages.RepoActionsFragment(w, pages.RepoActionsFragmentParams{
+141
-104
appview/state/state.go
+141
-104
appview/state/state.go
···
19
19
"github.com/go-chi/chi/v5"
20
20
"tangled.sh/tangled.sh/core/api/tangled"
21
21
"tangled.sh/tangled.sh/core/appview"
22
-
"tangled.sh/tangled.sh/core/appview/auth"
23
22
"tangled.sh/tangled.sh/core/appview/db"
23
+
"tangled.sh/tangled.sh/core/appview/knotclient"
24
+
"tangled.sh/tangled.sh/core/appview/oauth"
24
25
"tangled.sh/tangled.sh/core/appview/pages"
25
26
"tangled.sh/tangled.sh/core/jetstream"
26
27
"tangled.sh/tangled.sh/core/rbac"
···
28
29
29
30
type State struct {
30
31
db *db.DB
31
-
auth *auth.Auth
32
+
oauth *oauth.OAuth
32
33
enforcer *rbac.Enforcer
33
-
tidClock *syntax.TIDClock
34
+
tidClock syntax.TIDClock
34
35
pages *pages.Pages
35
36
resolver *appview.Resolver
36
37
jc *jetstream.JetstreamClient
···
38
39
}
39
40
40
41
func Make(config *appview.Config) (*State, error) {
41
-
d, err := db.Make(config.DbPath)
42
-
if err != nil {
43
-
return nil, err
44
-
}
45
-
46
-
auth, err := auth.Make(config.CookieSecret)
42
+
d, err := db.Make(config.Core.DbPath)
47
43
if err != nil {
48
44
return nil, err
49
45
}
50
46
51
-
enforcer, err := rbac.NewEnforcer(config.DbPath)
47
+
enforcer, err := rbac.NewEnforcer(config.Core.DbPath)
52
48
if err != nil {
53
49
return nil, err
54
50
}
···
59
55
60
56
resolver := appview.NewResolver()
61
57
58
+
oauth := oauth.NewOAuth(d, config)
59
+
62
60
wrapper := db.DbWrapper{d}
63
61
jc, err := jetstream.NewJetstreamClient(
64
-
config.JetstreamEndpoint,
62
+
config.Jetstream.Endpoint,
65
63
"appview",
66
-
[]string{tangled.GraphFollowNSID, tangled.FeedStarNSID, tangled.PublicKeyNSID, tangled.RepoArtifactNSID},
64
+
[]string{
65
+
tangled.GraphFollowNSID,
66
+
tangled.FeedStarNSID,
67
+
tangled.PublicKeyNSID,
68
+
tangled.RepoArtifactNSID,
69
+
tangled.ActorProfileNSID,
70
+
},
67
71
nil,
68
72
slog.Default(),
69
73
wrapper,
···
72
76
if err != nil {
73
77
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
74
78
}
75
-
err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper))
79
+
err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper, enforcer))
76
80
if err != nil {
77
81
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
78
82
}
79
83
80
84
state := &State{
81
85
d,
82
-
auth,
86
+
oauth,
83
87
enforcer,
84
88
clock,
85
89
pgs,
···
95
99
return c.Next().String()
96
100
}
97
101
98
-
func (s *State) Login(w http.ResponseWriter, r *http.Request) {
99
-
ctx := r.Context()
102
+
// func (s *State) Login(w http.ResponseWriter, r *http.Request) {
103
+
// ctx := r.Context()
100
104
101
-
switch r.Method {
102
-
case http.MethodGet:
103
-
err := s.pages.Login(w, pages.LoginParams{})
104
-
if err != nil {
105
-
log.Printf("rendering login page: %s", err)
106
-
}
105
+
// switch r.Method {
106
+
// case http.MethodGet:
107
+
// err := s.pages.Login(w, pages.LoginParams{})
108
+
// if err != nil {
109
+
// log.Printf("rendering login page: %s", err)
110
+
// }
107
111
108
-
return
109
-
case http.MethodPost:
110
-
handle := strings.TrimPrefix(r.FormValue("handle"), "@")
111
-
appPassword := r.FormValue("app_password")
112
+
// return
113
+
// case http.MethodPost:
114
+
// handle := strings.TrimPrefix(r.FormValue("handle"), "@")
115
+
// appPassword := r.FormValue("app_password")
112
116
113
-
resolved, err := s.resolver.ResolveIdent(ctx, handle)
114
-
if err != nil {
115
-
log.Println("failed to resolve handle:", err)
116
-
s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
117
-
return
118
-
}
117
+
// resolved, err := s.resolver.ResolveIdent(ctx, handle)
118
+
// if err != nil {
119
+
// log.Println("failed to resolve handle:", err)
120
+
// s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
121
+
// return
122
+
// }
119
123
120
-
atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword)
121
-
if err != nil {
122
-
s.pages.Notice(w, "login-msg", "Invalid handle or password.")
123
-
return
124
-
}
125
-
sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
124
+
// atSession, err := s.oauth.CreateInitialSession(ctx, resolved, appPassword)
125
+
// if err != nil {
126
+
// s.pages.Notice(w, "login-msg", "Invalid handle or password.")
127
+
// return
128
+
// }
129
+
// sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
126
130
127
-
err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
128
-
if err != nil {
129
-
s.pages.Notice(w, "login-msg", "Failed to login, try again later.")
130
-
return
131
-
}
131
+
// err = s.oauth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
132
+
// if err != nil {
133
+
// s.pages.Notice(w, "login-msg", "Failed to login, try again later.")
134
+
// return
135
+
// }
132
136
133
-
log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
137
+
// log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
134
138
135
-
did := resolved.DID.String()
136
-
defaultKnot := "knot1.tangled.sh"
139
+
// did := resolved.DID.String()
140
+
// defaultKnot := "knot1.tangled.sh"
137
141
138
-
go func() {
139
-
log.Printf("adding %s to default knot", did)
140
-
err = s.enforcer.AddMember(defaultKnot, did)
141
-
if err != nil {
142
-
log.Println("failed to add user to knot1.tangled.sh: ", err)
143
-
return
144
-
}
145
-
err = s.enforcer.E.SavePolicy()
146
-
if err != nil {
147
-
log.Println("failed to add user to knot1.tangled.sh: ", err)
148
-
return
149
-
}
142
+
// go func() {
143
+
// log.Printf("adding %s to default knot", did)
144
+
// err = s.enforcer.AddMember(defaultKnot, did)
145
+
// if err != nil {
146
+
// log.Println("failed to add user to knot1.tangled.sh: ", err)
147
+
// return
148
+
// }
149
+
// err = s.enforcer.E.SavePolicy()
150
+
// if err != nil {
151
+
// log.Println("failed to add user to knot1.tangled.sh: ", err)
152
+
// return
153
+
// }
150
154
151
-
secret, err := db.GetRegistrationKey(s.db, defaultKnot)
152
-
if err != nil {
153
-
log.Println("failed to get registration key for knot1.tangled.sh")
154
-
return
155
-
}
156
-
signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Dev)
157
-
resp, err := signedClient.AddMember(did)
158
-
if err != nil {
159
-
log.Println("failed to add user to knot1.tangled.sh: ", err)
160
-
return
161
-
}
155
+
// secret, err := db.GetRegistrationKey(s.db, defaultKnot)
156
+
// if err != nil {
157
+
// log.Println("failed to get registration key for knot1.tangled.sh")
158
+
// return
159
+
// }
160
+
// signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Core.Dev)
161
+
// resp, err := signedClient.AddMember(did)
162
+
// if err != nil {
163
+
// log.Println("failed to add user to knot1.tangled.sh: ", err)
164
+
// return
165
+
// }
162
166
163
-
if resp.StatusCode != http.StatusNoContent {
164
-
log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)
165
-
return
166
-
}
167
-
}()
167
+
// if resp.StatusCode != http.StatusNoContent {
168
+
// log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)
169
+
// return
170
+
// }
171
+
// }()
168
172
169
-
s.pages.HxRedirect(w, "/")
170
-
return
171
-
}
172
-
}
173
+
// s.pages.HxRedirect(w, "/")
174
+
// return
175
+
// }
176
+
// }
173
177
174
178
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
175
-
s.auth.ClearSession(r, w)
179
+
s.oauth.ClearSession(r, w)
176
180
w.Header().Set("HX-Redirect", "/login")
177
181
w.WriteHeader(http.StatusSeeOther)
178
182
}
179
183
180
184
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
181
-
user := s.auth.GetUser(r)
185
+
user := s.oauth.GetUser(r)
182
186
183
187
timeline, err := db.MakeTimeline(s.db)
184
188
if err != nil {
···
229
233
230
234
return
231
235
case http.MethodPost:
232
-
session, err := s.auth.Store.Get(r, appview.SessionName)
236
+
session, err := s.oauth.Store.Get(r, appview.SessionName)
233
237
if err != nil || session.IsNew {
234
238
log.Println("unauthorized attempt to generate registration key")
235
239
http.Error(w, "Forbidden", http.StatusUnauthorized)
···
291
295
292
296
// create a signed request and check if a node responds to that
293
297
func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
294
-
user := s.auth.GetUser(r)
298
+
user := s.oauth.GetUser(r)
295
299
296
300
domain := chi.URLParam(r, "domain")
297
301
if domain == "" {
···
306
310
return
307
311
}
308
312
309
-
client, err := NewSignedClient(domain, secret, s.config.Dev)
313
+
client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
310
314
if err != nil {
311
315
log.Println("failed to create client to ", domain)
312
316
}
···
415
419
return
416
420
}
417
421
418
-
user := s.auth.GetUser(r)
422
+
user := s.oauth.GetUser(r)
419
423
reg, err := db.RegistrationByDomain(s.db, domain)
420
424
if err != nil {
421
425
w.Write([]byte("failed to pull up registration info"))
···
463
467
// get knots registered by this user
464
468
func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
465
469
// for now, this is just pubkeys
466
-
user := s.auth.GetUser(r)
470
+
user := s.oauth.GetUser(r)
467
471
registrations, err := db.RegistrationsByDid(s.db, user.Did)
468
472
if err != nil {
469
473
log.Println(err)
···
516
520
log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain)
517
521
518
522
// announce this relation into the firehose, store into owners' pds
519
-
client, _ := s.auth.AuthorizedClient(r)
520
-
currentUser := s.auth.GetUser(r)
523
+
client, err := s.oauth.AuthorizedClient(r)
524
+
if err != nil {
525
+
http.Error(w, "failed to authorize client", http.StatusInternalServerError)
526
+
return
527
+
}
528
+
currentUser := s.oauth.GetUser(r)
521
529
createdAt := time.Now().Format(time.RFC3339)
522
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
530
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
523
531
Collection: tangled.KnotMemberNSID,
524
532
Repo: currentUser.Did,
525
533
Rkey: appview.TID(),
···
544
552
return
545
553
}
546
554
547
-
ksClient, err := NewSignedClient(domain, secret, s.config.Dev)
555
+
ksClient, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
548
556
if err != nil {
549
557
log.Println("failed to create client to ", domain)
550
558
return
···
573
581
func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
574
582
}
575
583
584
+
func validateRepoName(name string) error {
585
+
// check for path traversal attempts
586
+
if name == "." || name == ".." ||
587
+
strings.Contains(name, "/") || strings.Contains(name, "\\") {
588
+
return fmt.Errorf("Repository name contains invalid path characters")
589
+
}
590
+
591
+
// check for sequences that could be used for traversal when normalized
592
+
if strings.Contains(name, "./") || strings.Contains(name, "../") ||
593
+
strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
594
+
return fmt.Errorf("Repository name contains invalid path sequence")
595
+
}
596
+
597
+
// then continue with character validation
598
+
for _, char := range name {
599
+
if !((char >= 'a' && char <= 'z') ||
600
+
(char >= 'A' && char <= 'Z') ||
601
+
(char >= '0' && char <= '9') ||
602
+
char == '-' || char == '_' || char == '.') {
603
+
return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
604
+
}
605
+
}
606
+
607
+
// additional check to prevent multiple sequential dots
608
+
if strings.Contains(name, "..") {
609
+
return fmt.Errorf("Repository name cannot contain sequential dots")
610
+
}
611
+
612
+
// if all checks pass
613
+
return nil
614
+
}
615
+
576
616
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
577
617
switch r.Method {
578
618
case http.MethodGet:
579
-
user := s.auth.GetUser(r)
619
+
user := s.oauth.GetUser(r)
580
620
knots, err := s.enforcer.GetDomainsForUser(user.Did)
581
621
if err != nil {
582
622
s.pages.Notice(w, "repo", "Invalid user account.")
···
589
629
})
590
630
591
631
case http.MethodPost:
592
-
user := s.auth.GetUser(r)
632
+
user := s.oauth.GetUser(r)
593
633
594
634
domain := r.FormValue("domain")
595
635
if domain == "" {
···
603
643
return
604
644
}
605
645
606
-
// Check for valid repository name (GitHub-like rules)
607
-
// No spaces, only alphanumeric characters, dashes, and underscores
608
-
for _, char := range repoName {
609
-
if !((char >= 'a' && char <= 'z') ||
610
-
(char >= 'A' && char <= 'Z') ||
611
-
(char >= '0' && char <= '9') ||
612
-
char == '-' || char == '_' || char == '.') {
613
-
s.pages.Notice(w, "repo", "Repository name can only contain alphanumeric characters, periods, hyphens, and underscores.")
614
-
return
615
-
}
646
+
if err := validateRepoName(repoName); err != nil {
647
+
s.pages.Notice(w, "repo", err.Error())
648
+
return
616
649
}
617
650
618
651
defaultBranch := r.FormValue("branch")
···
640
673
return
641
674
}
642
675
643
-
client, err := NewSignedClient(domain, secret, s.config.Dev)
676
+
client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
644
677
if err != nil {
645
678
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
646
679
return
···
655
688
Description: description,
656
689
}
657
690
658
-
xrpcClient, _ := s.auth.AuthorizedClient(r)
691
+
xrpcClient, err := s.oauth.AuthorizedClient(r)
692
+
if err != nil {
693
+
s.pages.Notice(w, "repo", "Failed to write record to PDS.")
694
+
return
695
+
}
659
696
660
697
createdAt := time.Now().Format(time.RFC3339)
661
-
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
698
+
atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
662
699
Collection: tangled.RepoNSID,
663
700
Repo: user.Did,
664
701
Rkey: rkey,
+1
-1
appview/tid.go
+1
-1
appview/tid.go
+80
appview/xrpcclient/xrpc.go
+80
appview/xrpcclient/xrpc.go
···
1
+
package xrpcclient
2
+
3
+
import (
4
+
"bytes"
5
+
"context"
6
+
"io"
7
+
8
+
"github.com/bluesky-social/indigo/api/atproto"
9
+
"github.com/bluesky-social/indigo/xrpc"
10
+
oauth "github.com/haileyok/atproto-oauth-golang"
11
+
)
12
+
13
+
type Client struct {
14
+
*oauth.XrpcClient
15
+
authArgs *oauth.XrpcAuthedRequestArgs
16
+
}
17
+
18
+
func NewClient(client *oauth.XrpcClient, authArgs *oauth.XrpcAuthedRequestArgs) *Client {
19
+
return &Client{
20
+
XrpcClient: client,
21
+
authArgs: authArgs,
22
+
}
23
+
}
24
+
25
+
func (c *Client) RepoPutRecord(ctx context.Context, input *atproto.RepoPutRecord_Input) (*atproto.RepoPutRecord_Output, error) {
26
+
var out atproto.RepoPutRecord_Output
27
+
if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil {
28
+
return nil, err
29
+
}
30
+
31
+
return &out, nil
32
+
}
33
+
34
+
func (c *Client) RepoGetRecord(ctx context.Context, cid string, collection string, repo string, rkey string) (*atproto.RepoGetRecord_Output, error) {
35
+
var out atproto.RepoGetRecord_Output
36
+
37
+
params := map[string]interface{}{
38
+
"cid": cid,
39
+
"collection": collection,
40
+
"repo": repo,
41
+
"rkey": rkey,
42
+
}
43
+
if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.repo.getRecord", params, nil, &out); err != nil {
44
+
return nil, err
45
+
}
46
+
47
+
return &out, nil
48
+
}
49
+
50
+
func (c *Client) RepoUploadBlob(ctx context.Context, input io.Reader) (*atproto.RepoUploadBlob_Output, error) {
51
+
var out atproto.RepoUploadBlob_Output
52
+
if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "*/*", "com.atproto.repo.uploadBlob", nil, input, &out); err != nil {
53
+
return nil, err
54
+
}
55
+
56
+
return &out, nil
57
+
}
58
+
59
+
func (c *Client) SyncGetBlob(ctx context.Context, cid string, did string) ([]byte, error) {
60
+
buf := new(bytes.Buffer)
61
+
62
+
params := map[string]interface{}{
63
+
"cid": cid,
64
+
"did": did,
65
+
}
66
+
if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.sync.getBlob", params, nil, buf); err != nil {
67
+
return nil, err
68
+
}
69
+
70
+
return buf.Bytes(), nil
71
+
}
72
+
73
+
func (c *Client) RepoDeleteRecord(ctx context.Context, input *atproto.RepoDeleteRecord_Input) (*atproto.RepoDeleteRecord_Output, error) {
74
+
var out atproto.RepoDeleteRecord_Output
75
+
if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.deleteRecord", nil, input, &out); err != nil {
76
+
return nil, err
77
+
}
78
+
79
+
return &out, nil
80
+
}
+2
-2
cmd/appview/main.go
+2
-2
cmd/appview/main.go
+1
cmd/gen.go
+1
cmd/gen.go
+39
cmd/genjwks/main.go
+39
cmd/genjwks/main.go
···
1
+
// adapted from https://github.com/haileyok/atproto-oauth-golang
2
+
3
+
package main
4
+
5
+
import (
6
+
"crypto/ecdsa"
7
+
"crypto/elliptic"
8
+
"crypto/rand"
9
+
"encoding/json"
10
+
"fmt"
11
+
"time"
12
+
13
+
"github.com/lestrrat-go/jwx/v2/jwk"
14
+
)
15
+
16
+
func main() {
17
+
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
18
+
if err != nil {
19
+
panic(err)
20
+
}
21
+
22
+
key, err := jwk.FromRaw(privKey)
23
+
if err != nil {
24
+
panic(err)
25
+
}
26
+
27
+
kid := fmt.Sprintf("%d", time.Now().Unix())
28
+
29
+
if err := key.Set(jwk.KeyIDKey, kid); err != nil {
30
+
panic(err)
31
+
}
32
+
33
+
b, err := json.Marshal(key)
34
+
if err != nil {
35
+
panic(err)
36
+
}
37
+
38
+
fmt.Println(string(b))
39
+
}
+2
-4
docker/Dockerfile
+2
-4
docker/Dockerfile
···
42
42
COPY docker/rootfs/ .
43
43
44
44
RUN chown root:root /usr/local/libexec/tangled-keyfetch && \
45
-
chmod 755 /usr/local/libexec/tangled-keyfetch && \
46
-
chown git:git /home/git/repoguard && \
47
-
chown git:git /app && chown git:git /home/git/repositories
45
+
chmod 755 /usr/local/libexec/tangled-keyfetch
48
46
49
47
EXPOSE 22
50
48
EXPOSE 5555
51
49
52
-
ENTRYPOINT ["/init"]
50
+
ENTRYPOINT ["/bin/sh", "-c", "chown git:git /home/git/repoguard && chown git:git /app && chown git:git /home/git/repositories && /init"]
+17
-1
docker/docker-compose.yml
+17
-1
docker/docker-compose.yml
···
13
13
- "./repositories:/home/git/repositories"
14
14
- "./server:/app"
15
15
ports:
16
-
- "5555:5555"
17
16
- "2222:22"
17
+
frontend:
18
+
image: caddy:2-alpine
19
+
command: >
20
+
caddy
21
+
reverse-proxy
22
+
--from ${KNOT_SERVER_HOSTNAME}
23
+
--to knot:5555
24
+
depends_on:
25
+
- knot
26
+
ports:
27
+
- "443:443"
28
+
- "443:443/udp"
29
+
volumes:
30
+
- caddy_data:/data
31
+
restart: always
32
+
volumes:
33
+
caddy_data:
+72
docs/hacking.md
+72
docs/hacking.md
···
1
+
# hacking on tangled
2
+
3
+
We highly recommend [installing
4
+
nix](https://nixos.org/download/) (the package manager)
5
+
before working on the codebase. The nix flake provides a lot
6
+
of helpers to get started and most importantly, builds and
7
+
dev shells are entirely deterministic.
8
+
9
+
To set up your dev environment:
10
+
11
+
```bash
12
+
nix develop
13
+
```
14
+
15
+
Non-nix users can look at the `devShell` attribute in the
16
+
`flake.nix` file to determine necessary dependencies.
17
+
18
+
## running the appview
19
+
20
+
The nix flake also exposes a few `app` attributes (run `nix
21
+
flake show` to see a full list of what the flake provides),
22
+
one of the apps runs the appview with the `air`
23
+
live-reloader:
24
+
25
+
```bash
26
+
TANGLED_DEV=true nix run .#watch-appview
27
+
28
+
# TANGLED_DB_PATH might be of interest to point to
29
+
# different sqlite DBs
30
+
31
+
# in a separate shell, you can live-reload tailwind
32
+
nix run .#watch-tailwind
33
+
```
34
+
35
+
## running a knotserver
36
+
37
+
An end-to-end knotserver setup requires setting up a machine
38
+
with `sshd`, `repoguard`, `keyfetch`, a git user, which is
39
+
quite cumbersome and so the nix flake provides a
40
+
`nixosConfiguration` to do so.
41
+
42
+
To begin, head to `http://localhost:3000` in the browser and
43
+
generate a knotserver secret. Replace the existing secret in
44
+
`flake.nix` with the newly generated secret.
45
+
46
+
You can now start a lightweight NixOS VM using
47
+
`nixos-shell` like so:
48
+
49
+
```bash
50
+
QEMU_NET_OPTS="hostfwd=tcp::6000-:6000,hostfwd=tcp::2222-:22" nixos-shell --flake .#knotVM
51
+
52
+
# hit Ctrl-a + c + q to exit the VM
53
+
```
54
+
55
+
This starts a knotserver on port 6000 with `ssh` exposed on
56
+
port 2222. You can push repositories to this VM with this
57
+
ssh config block on your main machine:
58
+
59
+
```bash
60
+
Host nixos-shell
61
+
Hostname localhost
62
+
Port 2222
63
+
User git
64
+
IdentityFile ~/.ssh/my_tangled_key
65
+
```
66
+
67
+
Set up a remote called `local-dev` on a git repo:
68
+
69
+
```bash
70
+
git remote add local-dev git@nixos-shell:user/repo
71
+
git push local-dev main
72
+
```
+5
-5
flake.lock
+5
-5
flake.lock
···
64
64
"inter-fonts-src": {
65
65
"flake": false,
66
66
"locked": {
67
-
"lastModified": 1731680160,
67
+
"lastModified": 1731687360,
68
68
"narHash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=",
69
69
"type": "tarball",
70
70
"url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip"
···
89
89
},
90
90
"nixpkgs": {
91
91
"locked": {
92
-
"lastModified": 1746055187,
93
-
"narHash": "sha256-3dqArYSMP9hM7Qpy5YWhnSjiqniSaT2uc5h2Po7tmg0=",
92
+
"lastModified": 1746904237,
93
+
"narHash": "sha256-3e+AVBczosP5dCLQmMoMEogM57gmZ2qrVSrmq9aResQ=",
94
94
"owner": "nixos",
95
95
"repo": "nixpkgs",
96
-
"rev": "3e362ce63e16b9572d8c2297c04f7c19ab6725a5",
96
+
"rev": "d89fc19e405cb2d55ce7cc114356846a0ee5e956",
97
97
"type": "github"
98
98
},
99
99
"original": {
100
100
"owner": "nixos",
101
-
"ref": "nixos-24.11",
101
+
"ref": "nixos-unstable",
102
102
"repo": "nixpkgs",
103
103
"type": "github"
104
104
}
+12
-9
flake.nix
+12
-9
flake.nix
···
2
2
description = "atproto github";
3
3
4
4
inputs = {
5
-
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
5
+
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
6
6
indigo = {
7
7
url = "github:oppiliappan/indigo";
8
8
flake = false;
···
49
49
inherit (gitignore.lib) gitignoreSource;
50
50
in {
51
51
overlays.default = final: prev: let
52
-
goModHash = "sha256-CmBuvv3duQQoc8iTW4244w1rYLGeqMQS+qQ3wwReZZg=";
52
+
goModHash = "sha256-zcfTNo7QsiihzLa4qHEX8uGGtbcmBn8TlSm0YHBRNw8=";
53
53
buildCmdPackage = name:
54
54
final.buildGoModule {
55
55
pname = name;
···
57
57
src = gitignoreSource ./.;
58
58
subPackages = ["cmd/${name}"];
59
59
vendorHash = goModHash;
60
-
CGO_ENABLED = 0;
60
+
env.CGO_ENABLED = 0;
61
61
};
62
62
in {
63
63
indigo-lexgen = final.buildGoModule {
···
88
88
doCheck = false;
89
89
subPackages = ["cmd/appview"];
90
90
vendorHash = goModHash;
91
-
CGO_ENABLED = 1;
91
+
env.CGO_ENABLED = 1;
92
92
stdenv = pkgsStatic.stdenv;
93
93
};
94
94
···
111
111
112
112
runHook postInstall
113
113
'';
114
-
CGO_ENABLED = 1;
114
+
env.CGO_ENABLED = 1;
115
115
};
116
116
knotserver-unwrapped = final.pkgsStatic.buildGoModule {
117
117
pname = "knotserver";
···
119
119
src = gitignoreSource ./.;
120
120
subPackages = ["cmd/knotserver"];
121
121
vendorHash = goModHash;
122
-
CGO_ENABLED = 1;
122
+
env.CGO_ENABLED = 1;
123
123
};
124
124
repoguard = buildCmdPackage "repoguard";
125
125
keyfetch = buildCmdPackage "keyfetch";
126
+
genjwks = buildCmdPackage "genjwks";
126
127
};
127
128
packages = forAllSystems (system: {
128
129
inherit
···
133
134
knotserver-unwrapped
134
135
repoguard
135
136
keyfetch
137
+
genjwks
136
138
;
137
139
});
138
140
defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview);
···
162
164
cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/
163
165
cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/
164
166
cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/
167
+
export TANGLED_OAUTH_JWKS="$(${pkgs.genjwks}/bin/genjwks)"
165
168
'';
169
+
env.CGO_ENABLED = 1;
166
170
};
167
171
});
168
172
apps = forAllSystems (system: let
···
170
174
air-watcher = name:
171
175
pkgs.writeShellScriptBin "run"
172
176
''
173
-
TANGLED_DEV=true ${pkgs.air}/bin/air -c /dev/null \
174
-
-build.cmd "${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o ./appview/pages/static/tw.css && ${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \
177
+
${pkgs.air}/bin/air -c /dev/null \
178
+
-build.cmd "${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \
175
179
-build.bin "./out/${name}.out" \
176
180
-build.stop_on_error "true" \
177
181
-build.include_ext "go"
···
446
450
};
447
451
};
448
452
}
449
-
+18
-12
go.mod
+18
-12
go.mod
···
1
1
module tangled.sh/tangled.sh/core
2
2
3
-
go 1.23.0
3
+
go 1.24.0
4
4
5
-
toolchain go1.23.6
5
+
toolchain go1.24.3
6
6
7
7
require (
8
8
github.com/Blank-Xu/sql-adapter v1.1.1
9
9
github.com/alecthomas/chroma/v2 v2.15.0
10
10
github.com/bluekeyes/go-gitdiff v0.8.1
11
-
github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20
11
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188
12
12
github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1
13
13
github.com/casbin/casbin/v2 v2.103.0
14
14
github.com/cyphar/filepath-securejoin v0.4.1
···
19
19
github.com/go-git/go-git/v5 v5.14.0
20
20
github.com/google/uuid v1.6.0
21
21
github.com/gorilla/sessions v1.4.0
22
+
github.com/haileyok/atproto-oauth-golang v0.0.2
22
23
github.com/ipfs/go-cid v0.5.0
24
+
github.com/lestrrat-go/jwx/v2 v2.0.12
23
25
github.com/mattn/go-sqlite3 v1.14.24
24
26
github.com/microcosm-cc/bluemonday v1.0.27
25
27
github.com/resend/resend-go/v2 v2.15.0
···
41
43
github.com/casbin/govaluate v1.3.0 // indirect
42
44
github.com/cespare/xxhash/v2 v2.3.0 // indirect
43
45
github.com/cloudflare/circl v1.6.0 // indirect
44
-
github.com/davecgh/go-spew v1.1.1 // indirect
46
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
45
47
github.com/dlclark/regexp2 v1.11.5 // indirect
46
48
github.com/emirpasic/gods v1.18.1 // indirect
47
49
github.com/felixge/httpsnoop v1.0.4 // indirect
48
50
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
49
51
github.com/go-git/go-billy/v5 v5.6.2 // indirect
50
-
github.com/go-logr/logr v1.4.1 // indirect
52
+
github.com/go-logr/logr v1.4.2 // indirect
51
53
github.com/go-logr/stdr v1.2.2 // indirect
52
54
github.com/goccy/go-json v0.10.2 // indirect
53
55
github.com/gogo/protobuf v1.3.2 // indirect
56
+
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
54
57
github.com/gorilla/css v1.0.1 // indirect
55
58
github.com/gorilla/securecookie v1.1.2 // indirect
56
59
github.com/gorilla/websocket v1.5.1 // indirect
···
75
78
github.com/kevinburke/ssh_config v1.2.0 // indirect
76
79
github.com/klauspost/compress v1.17.9 // indirect
77
80
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
81
+
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
82
+
github.com/lestrrat-go/httpcc v1.0.1 // indirect
83
+
github.com/lestrrat-go/httprc v1.0.4 // indirect
84
+
github.com/lestrrat-go/iter v1.0.2 // indirect
85
+
github.com/lestrrat-go/option v1.0.1 // indirect
78
86
github.com/mattn/go-isatty v0.0.20 // indirect
79
87
github.com/minio/sha256-simd v1.0.1 // indirect
80
88
github.com/mr-tron/base58 v1.2.0 // indirect
···
86
94
github.com/opentracing/opentracing-go v1.2.0 // indirect
87
95
github.com/pjbgf/sha1cd v0.3.2 // indirect
88
96
github.com/pkg/errors v0.9.1 // indirect
89
-
github.com/pmezard/go-difflib v1.0.0 // indirect
90
97
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
91
98
github.com/prometheus/client_golang v1.19.1 // indirect
92
99
github.com/prometheus/client_model v0.6.1 // indirect
93
100
github.com/prometheus/common v0.54.0 // indirect
94
101
github.com/prometheus/procfs v0.15.1 // indirect
102
+
github.com/segmentio/asm v1.2.0 // indirect
95
103
github.com/sergi/go-diff v1.3.1 // indirect
96
104
github.com/skeema/knownhosts v1.3.1 // indirect
97
105
github.com/spaolacci/murmur3 v1.1.0 // indirect
98
-
github.com/stretchr/testify v1.10.0 // indirect
99
106
github.com/xanzy/ssh-agent v0.3.3 // indirect
100
107
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
101
108
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
102
109
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
103
-
go.opentelemetry.io/otel v1.21.0 // indirect
104
-
go.opentelemetry.io/otel/metric v1.21.0 // indirect
105
-
go.opentelemetry.io/otel/trace v1.21.0 // indirect
110
+
go.opentelemetry.io/otel v1.29.0 // indirect
111
+
go.opentelemetry.io/otel/metric v1.29.0 // indirect
112
+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
106
113
go.uber.org/atomic v1.11.0 // indirect
107
114
go.uber.org/multierr v1.11.0 // indirect
108
115
go.uber.org/zap v1.26.0 // indirect
109
116
golang.org/x/crypto v0.37.0 // indirect
110
117
golang.org/x/net v0.39.0 // indirect
111
118
golang.org/x/sys v0.32.0 // indirect
112
-
golang.org/x/time v0.5.0 // indirect
119
+
golang.org/x/time v0.8.0 // indirect
113
120
google.golang.org/protobuf v1.34.2 // indirect
114
121
gopkg.in/warnings.v0 v0.1.2 // indirect
115
-
gopkg.in/yaml.v3 v3.0.1 // indirect
116
122
lukechampine.com/blake3 v1.2.1 // indirect
117
123
)
118
124
+61
-16
go.sum
+61
-16
go.sum
···
26
26
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
27
27
github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI=
28
28
github.com/bluekeyes/go-gitdiff v0.8.1/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE=
29
-
github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20 h1:yHusfYYi8odoCcsI6AurU+dRWb7itHAQNwt3/Rl9Vfs=
30
-
github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20/go.mod h1:Qp4YqWf+AQ3TwQCxV5Ls8O2tXE55zVTGVs3zTmn7BOg=
29
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk=
30
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA=
31
31
github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA=
32
32
github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4=
33
33
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
···
52
52
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
53
53
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
54
54
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
-
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
56
55
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
56
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
57
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
58
+
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
59
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
60
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
57
61
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
58
62
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
59
63
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
···
82
86
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
83
87
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
84
88
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
85
-
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
86
-
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
89
+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
90
+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
87
91
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
88
92
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
89
93
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
···
91
95
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
92
96
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
93
97
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
98
+
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
99
+
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
94
100
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
95
101
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
96
102
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
···
111
117
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
112
118
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
113
119
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
120
+
github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8=
121
+
github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8=
114
122
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
115
123
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
116
124
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
159
167
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
160
168
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
161
169
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
170
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
171
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
162
172
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
163
173
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
164
174
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
···
177
187
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
178
188
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
179
189
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
190
+
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
191
+
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
192
+
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
193
+
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
194
+
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
195
+
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
196
+
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
197
+
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
198
+
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
199
+
github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA=
200
+
github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ=
201
+
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
202
+
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
203
+
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
180
204
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
181
205
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
182
206
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
···
212
236
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
213
237
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
214
238
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
215
-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
216
239
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
240
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
241
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
217
242
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
218
243
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
219
244
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
···
227
252
github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE=
228
253
github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
229
254
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
230
-
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
231
-
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
255
+
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
256
+
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
232
257
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
258
+
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
259
+
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
233
260
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
234
261
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
235
262
github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog=
···
246
273
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
247
274
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
248
275
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
276
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
277
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
249
278
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
250
279
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
251
280
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
281
+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
252
282
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
283
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
284
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
285
+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
253
286
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
254
287
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
255
288
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
···
270
303
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
271
304
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
272
305
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
273
-
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
274
-
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
275
-
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
276
-
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
277
-
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
278
-
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
306
+
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
307
+
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
308
+
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
309
+
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
310
+
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
311
+
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
279
312
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
280
313
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
281
314
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
···
303
336
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
304
337
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
305
338
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
339
+
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
306
340
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
307
341
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
308
342
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
···
314
348
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
315
349
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
316
350
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
351
+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
317
352
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
318
353
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
319
354
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
···
327
362
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
328
363
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
329
364
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
365
+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
330
366
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
331
367
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
332
368
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
···
334
370
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
335
371
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
336
372
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
373
+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
337
374
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
338
375
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
339
376
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
···
348
385
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
349
386
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
350
387
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
388
+
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
351
389
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
352
390
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
353
391
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
···
357
395
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
358
396
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
359
397
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
398
+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
399
+
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
360
400
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
361
401
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
362
402
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
···
364
404
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
365
405
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
366
406
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
407
+
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
408
+
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
367
409
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
368
410
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
369
411
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
···
372
414
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
373
415
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
374
416
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
417
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
418
+
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
375
419
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
376
420
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
377
-
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
378
-
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
421
+
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
422
+
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
379
423
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
380
424
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
381
425
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
···
389
433
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
390
434
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
391
435
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
436
+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
392
437
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
393
438
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
394
439
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+38
knotserver/routes.go
+38
knotserver/routes.go
···
600
600
name := data.Name
601
601
defaultBranch := data.DefaultBranch
602
602
603
+
if err := validateRepoName(name); err != nil {
604
+
l.Error("creating repo", "error", err.Error())
605
+
writeError(w, err.Error(), http.StatusBadRequest)
606
+
return
607
+
}
608
+
603
609
relativeRepoPath := filepath.Join(did, name)
604
610
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
605
611
err := git.InitBare(repoPath, defaultBranch)
···
1078
1084
func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1079
1085
w.Write([]byte("ok"))
1080
1086
}
1087
+
1088
+
func validateRepoName(name string) error {
1089
+
// check for path traversal attempts
1090
+
if name == "." || name == ".." ||
1091
+
strings.Contains(name, "/") || strings.Contains(name, "\\") {
1092
+
return fmt.Errorf("Repository name contains invalid path characters")
1093
+
}
1094
+
1095
+
// check for sequences that could be used for traversal when normalized
1096
+
if strings.Contains(name, "./") || strings.Contains(name, "../") ||
1097
+
strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
1098
+
return fmt.Errorf("Repository name contains invalid path sequence")
1099
+
}
1100
+
1101
+
// then continue with character validation
1102
+
for _, char := range name {
1103
+
if !((char >= 'a' && char <= 'z') ||
1104
+
(char >= 'A' && char <= 'Z') ||
1105
+
(char >= '0' && char <= '9') ||
1106
+
char == '-' || char == '_' || char == '.') {
1107
+
return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
1108
+
}
1109
+
}
1110
+
1111
+
// additional check to prevent multiple sequential dots
1112
+
if strings.Contains(name, "..") {
1113
+
return fmt.Errorf("Repository name cannot contain sequential dots")
1114
+
}
1115
+
1116
+
// if all checks pass
1117
+
return nil
1118
+
}
+72
lexicons/actor/profile.json
+72
lexicons/actor/profile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "sh.tangled.actor.profile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A declaration of a Tangled account profile.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"required": [
12
+
"bluesky"
13
+
],
14
+
"properties": {
15
+
"description": {
16
+
"type": "string",
17
+
"description": "Free-form profile description text.",
18
+
"maxGraphemes": 256,
19
+
"maxLength": 2560
20
+
},
21
+
"links": {
22
+
"type": "array",
23
+
"minLength": 0,
24
+
"maxLength": 5,
25
+
"items": {
26
+
"type": "string",
27
+
"description": "Any URI, intended for social profiles or websites, can be used to link DIDs/AT-URIs too.",
28
+
"format": "uri"
29
+
}
30
+
},
31
+
"stats": {
32
+
"type": "array",
33
+
"minLength": 0,
34
+
"maxLength": 2,
35
+
"items": {
36
+
"type": "string",
37
+
"description": "Vanity stats.",
38
+
"enum": [
39
+
"merged-pull-request-count",
40
+
"closed-pull-request-count",
41
+
"open-pull-request-count",
42
+
"open-issue-count",
43
+
"closed-issue-count",
44
+
"repository-count"
45
+
]
46
+
}
47
+
},
48
+
"bluesky": {
49
+
"type": "boolean",
50
+
"description": "Include link to this account on Bluesky."
51
+
},
52
+
"location": {
53
+
"type": "string",
54
+
"description": "Free-form location text.",
55
+
"maxGraphemes": 40,
56
+
"maxLength": 400
57
+
},
58
+
"pinnedRepositories": {
59
+
"type": "array",
60
+
"description": "Any ATURI, it is up to appviews to validate these fields.",
61
+
"minLength": 0,
62
+
"maxLength": 6,
63
+
"items": {
64
+
"type": "string",
65
+
"format": "at-uri"
66
+
}
67
+
}
68
+
}
69
+
}
70
+
}
71
+
}
72
+
}
-1
lexicons/publicKey.json
-1
lexicons/publicKey.json