forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

+1 -1
.air/appview.toml
··· 1 [build] 2 cmd = "tailwindcss -i input.css -o ./appview/pages/static/tw.css && go build -o .bin/app ./cmd/appview/main.go" 3 - bin = ".bin/app" 4 root = "." 5 6 exclude_regex = [".*_templ.go"]
··· 1 [build] 2 cmd = "tailwindcss -i input.css -o ./appview/pages/static/tw.css && go build -o .bin/app ./cmd/appview/main.go" 3 + bin = ";set -o allexport && source .env && set +o allexport; .bin/app" 4 root = "." 5 6 exclude_regex = [".*_templ.go"]
+4 -1
.gitignore
··· 9 out/ 10 ./camo/node_modules/* 11 ./avatar/node_modules/* 12 -
··· 9 out/ 10 ./camo/node_modules/* 11 ./avatar/node_modules/* 12 + patches 13 + *.qcow2 14 + .DS_Store 15 + .env
+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
··· 3389 3390 return nil 3391 }
··· 3389 3390 return nil 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
··· 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
··· 6 "github.com/sethvargo/go-envconfig" 7 ) 8 9 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"` 20 } 21 22 func LoadConfig(ctx context.Context) (*Config, error) {
··· 6 "github.com/sethvargo/go-envconfig" 7 ) 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 + 39 type Config struct { 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_"` 46 } 47 48 func LoadConfig(ctx context.Context) (*Config, error) {
+3
appview/consts.go
··· 9 SessionRefreshJwt = "refreshJwt" 10 SessionExpiry = "expiry" 11 SessionAuthenticated = "authenticated" 12 )
··· 9 SessionRefreshJwt = "refreshJwt" 10 SessionExpiry = "expiry" 11 SessionAuthenticated = "authenticated" 12 + 13 + SessionDpopPrivateJwk = "dpopPrivateJwk" 14 + SessionDpopAuthServerNonce = "dpopAuthServerNonce" 15 )
-16
appview/db/artifact.go
··· 57 return err 58 } 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 func GetArtifact(e Execer, filters ...filter) ([]Artifact, error) { 77 var artifacts []Artifact 78
··· 57 return err 58 } 59 60 func GetArtifact(e Execer, filters ...filter) ([]Artifact, error) { 61 var artifacts []Artifact 62
+99
appview/db/db.go
··· 3 import ( 4 "context" 5 "database/sql" 6 "log" 7 8 _ "github.com/mattn/go-sqlite3" ··· 231 foreign key (repo_at) references repos(at_uri) on delete cascade 232 ); 233 234 create table if not exists migrations ( 235 id integer primary key autoincrement, 236 name text unique ··· 348 349 return nil 350 }
··· 3 import ( 4 "context" 5 "database/sql" 6 + "fmt" 7 "log" 8 9 _ "github.com/mattn/go-sqlite3" ··· 232 foreign key (repo_at) references repos(at_uri) on delete cascade 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 + 317 create table if not exists migrations ( 318 id integer primary key autoincrement, 319 name text unique ··· 431 432 return nil 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
···
··· 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
··· 1 package db 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 type RepoEvent struct { ··· 162 163 return &timeline, nil 164 }
··· 1 package db 2 3 import ( 4 + "database/sql" 5 "fmt" 6 + "log" 7 + "net/url" 8 + "slices" 9 + "strings" 10 "time" 11 + 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "tangled.sh/tangled.sh/core/api/tangled" 14 ) 15 16 type RepoEvent struct { ··· 170 171 return &timeline, nil 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
··· 235 } 236 237 func NewPull(tx *sql.Tx, pull *Pull) error { 238 - defer tx.Rollback() 239 - 240 _, err := tx.Exec(` 241 insert or ignore into repo_pull_seqs (repo_at, next_pull_id) 242 values (?, 1) ··· 291 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 292 values (?, ?, ?, ?, ?) 293 `, 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 303 } 304 305 func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
··· 235 } 236 237 func NewPull(tx *sql.Tx, pull *Pull) error { 238 _, err := tx.Exec(` 239 insert or ignore into repo_pull_seqs (repo_at, next_pull_id) 240 values (?, 1) ··· 289 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 290 values (?, ?, ?, ?, ?) 291 `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev) 292 + return err 293 } 294 295 func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
+12
appview/db/repos.go
··· 2 3 import ( 4 "database/sql" 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 ) 9 10 type Repo struct { ··· 21 22 // optional 23 Source string 24 } 25 26 func GetAllRepos(e Execer, limit int) ([]Repo, error) {
··· 2 3 import ( 4 "database/sql" 5 + "fmt" 6 "time" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 + securejoin "github.com/cyphar/filepath-securejoin" 10 + "tangled.sh/tangled.sh/core/api/tangled" 11 ) 12 13 type Repo struct { ··· 24 25 // optional 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 36 } 37 38 func GetAllRepos(e Execer, limit int) ([]Repo, error) {
+1 -1
appview/db/star.go
··· 71 72 // Remove a star 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) 75 return err 76 } 77
··· 71 72 // Remove a star 73 func DeleteStarByRkey(e Execer, starredByDid string, rkey string) error { 74 + _, err := e.Exec(`delete from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey) 75 return err 76 } 77
+104 -5
appview/ingester.go
··· 13 "github.com/ipfs/go-cid" 14 "tangled.sh/tangled.sh/core/api/tangled" 15 "tangled.sh/tangled.sh/core/appview/db" 16 ) 17 18 type Ingester func(ctx context.Context, e *models.Event) error 19 20 - func Ingest(d db.DbWrapper) Ingester { 21 return func(ctx context.Context, e *models.Event) error { 22 var err error 23 defer func() { ··· 40 case tangled.PublicKeyNSID: 41 ingestPublicKey(&d, e) 42 case tangled.RepoArtifactNSID: 43 - ingestArtifact(&d, e) 44 } 45 46 return err ··· 137 return nil 138 } 139 140 - func ingestArtifact(d *db.DbWrapper, e *models.Event) error { 141 did := e.Did 142 var err error 143 144 switch e.Commit.Operation { 145 case models.CommitOperationCreate, models.CommitOperationUpdate: 146 - log.Println("processing add of artifact") 147 raw := json.RawMessage(e.Commit.Record) 148 record := tangled.RepoArtifact{} 149 err = json.Unmarshal(raw, &record) ··· 157 return err 158 } 159 160 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 161 if err != nil { 162 createdAt = time.Now() ··· 176 177 err = db.AddArtifact(d, artifact) 178 case models.CommitOperationDelete: 179 - log.Println("processing delete of artifact") 180 err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey)) 181 } 182 ··· 186 187 return nil 188 }
··· 13 "github.com/ipfs/go-cid" 14 "tangled.sh/tangled.sh/core/api/tangled" 15 "tangled.sh/tangled.sh/core/appview/db" 16 + "tangled.sh/tangled.sh/core/rbac" 17 ) 18 19 type Ingester func(ctx context.Context, e *models.Event) error 20 21 + func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer) Ingester { 22 return func(ctx context.Context, e *models.Event) error { 23 var err error 24 defer func() { ··· 41 case tangled.PublicKeyNSID: 42 ingestPublicKey(&d, e) 43 case tangled.RepoArtifactNSID: 44 + ingestArtifact(&d, e, enforcer) 45 + case tangled.ActorProfileNSID: 46 + ingestProfile(&d, e) 47 } 48 49 return err ··· 140 return nil 141 } 142 143 + func ingestArtifact(d *db.DbWrapper, e *models.Event, enforcer *rbac.Enforcer) error { 144 did := e.Did 145 var err error 146 147 switch e.Commit.Operation { 148 case models.CommitOperationCreate, models.CommitOperationUpdate: 149 raw := json.RawMessage(e.Commit.Record) 150 record := tangled.RepoArtifact{} 151 err = json.Unmarshal(raw, &record) ··· 159 return err 160 } 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 + 172 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 173 if err != nil { 174 createdAt = time.Now() ··· 188 189 err = db.AddArtifact(d, artifact) 190 case models.CommitOperationDelete: 191 err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey)) 192 } 193 ··· 197 198 return nil 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
···
··· 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 "log" 6 "net/http" 7 "strconv" 8 - "time" 9 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" 14 "tangled.sh/tangled.sh/core/appview/pagination" 15 ) 16 17 type Middleware func(http.Handler) http.Handler 18 19 - func AuthMiddleware(a *auth.Auth) Middleware { 20 return func(next http.Handler) http.Handler { 21 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 redirectFunc := func(w http.ResponseWriter, r *http.Request) { ··· 29 } 30 } 31 32 - session, err := a.GetSession(r) 33 - if session.IsNew || err != nil { 34 log.Printf("not logged in, redirecting") 35 redirectFunc(w, r) 36 return 37 } 38 39 - authorized, ok := session.Values[appview.SessionAuthenticated].(bool) 40 - if !ok || !authorized { 41 log.Printf("not logged in, redirecting") 42 redirectFunc(w, r) 43 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 } 93 94 next.ServeHTTP(w, r)
··· 5 "log" 6 "net/http" 7 "strconv" 8 9 + "tangled.sh/tangled.sh/core/appview/oauth" 10 "tangled.sh/tangled.sh/core/appview/pagination" 11 ) 12 13 type Middleware func(http.Handler) http.Handler 14 15 + func AuthMiddleware(a *oauth.OAuth) Middleware { 16 return func(next http.Handler) http.Handler { 17 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 redirectFunc := func(w http.ResponseWriter, r *http.Request) { ··· 25 } 26 } 27 28 + _, auth, err := a.GetSession(r) 29 + if err != nil { 30 log.Printf("not logged in, redirecting") 31 redirectFunc(w, r) 32 return 33 } 34 35 + if !auth { 36 log.Printf("not logged in, redirecting") 37 redirectFunc(w, r) 38 return 39 } 40 41 next.ServeHTTP(w, r)
+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
···
··· 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
···
··· 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
··· 13 "time" 14 15 "github.com/dustin/go-humanize" 16 "tangled.sh/tangled.sh/core/appview/filetree" 17 "tangled.sh/tangled.sh/core/appview/pages/markup" 18 ) ··· 144 }, 145 "markdown": func(text string) template.HTML { 146 rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault} 147 - return template.HTML(rctx.RenderMarkdown(text)) 148 }, 149 "isNil": func(t any) bool { 150 // returns false for other "zero" values
··· 13 "time" 14 15 "github.com/dustin/go-humanize" 16 + "github.com/microcosm-cc/bluemonday" 17 "tangled.sh/tangled.sh/core/appview/filetree" 18 "tangled.sh/tangled.sh/core/appview/pages/markup" 19 ) ··· 145 }, 146 "markdown": func(text string) template.HTML { 147 rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault} 148 + return template.HTML(bluemonday.UGCPolicy().Sanitize(rctx.RenderMarkdown(text))) 149 }, 150 "isNil": func(t any) bool { 151 // returns false for other "zero" values
+2
appview/pages/markup/markdown.go
··· 10 "github.com/yuin/goldmark/ast" 11 "github.com/yuin/goldmark/extension" 12 "github.com/yuin/goldmark/parser" 13 "github.com/yuin/goldmark/text" 14 "github.com/yuin/goldmark/util" 15 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" ··· 41 goldmark.WithParserOptions( 42 parser.WithAutoHeadingID(), 43 ), 44 ) 45 46 if rctx != nil {
··· 10 "github.com/yuin/goldmark/ast" 11 "github.com/yuin/goldmark/extension" 12 "github.com/yuin/goldmark/parser" 13 + "github.com/yuin/goldmark/renderer/html" 14 "github.com/yuin/goldmark/text" 15 "github.com/yuin/goldmark/util" 16 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" ··· 42 goldmark.WithParserOptions( 43 parser.WithAutoHeadingID(), 44 ), 45 + goldmark.WithRendererOptions(html.WithUnsafe()), 46 ) 47 48 if rctx != nil {
+82 -43
appview/pages/pages.go
··· 16 "strings" 17 18 "tangled.sh/tangled.sh/core/appview" 19 - "tangled.sh/tangled.sh/core/appview/auth" 20 "tangled.sh/tangled.sh/core/appview/db" 21 "tangled.sh/tangled.sh/core/appview/pages/markup" 22 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 23 "tangled.sh/tangled.sh/core/appview/pagination" ··· 48 func NewPages(config *appview.Config) *Pages { 49 // initialized with safe defaults, can be overriden per use 50 rctx := &markup.RenderContext{ 51 - IsDev: config.Dev, 52 - CamoUrl: config.CamoHost, 53 - CamoSecret: config.CamoSharedSecret, 54 } 55 56 p := &Pages{ 57 t: make(map[string]*template.Template), 58 - dev: config.Dev, 59 embedFS: Files, 60 rctx: rctx, 61 templateDir: "appview/pages", ··· 250 } 251 252 type TimelineParams struct { 253 - LoggedInUser *auth.User 254 Timeline []db.TimelineEvent 255 DidHandleMap map[string]string 256 } ··· 260 } 261 262 type SettingsParams struct { 263 - LoggedInUser *auth.User 264 PubKeys []db.PublicKey 265 Emails []db.Email 266 } ··· 270 } 271 272 type KnotsParams struct { 273 - LoggedInUser *auth.User 274 Registrations []db.Registration 275 } 276 ··· 279 } 280 281 type KnotParams struct { 282 - LoggedInUser *auth.User 283 DidHandleMap map[string]string 284 Registration *db.Registration 285 Members []string ··· 291 } 292 293 type NewRepoParams struct { 294 - LoggedInUser *auth.User 295 Knots []string 296 } 297 ··· 300 } 301 302 type ForkRepoParams struct { 303 - LoggedInUser *auth.User 304 Knots []string 305 RepoInfo repoinfo.RepoInfo 306 } ··· 310 } 311 312 type ProfilePageParams struct { 313 - LoggedInUser *auth.User 314 - UserDid string 315 - UserHandle string 316 Repos []db.Repo 317 CollaboratingRepos []db.Repo 318 - ProfileStats ProfileStats 319 - FollowStatus db.FollowStatus 320 - AvatarUri string 321 ProfileTimeline *db.ProfileTimeline 322 323 DidHandleMap map[string]string 324 } 325 326 - type ProfileStats struct { 327 - Followers int 328 - Following int 329 } 330 331 func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 332 return p.execute("user/profile", w, params) 333 } 334 335 type FollowFragmentParams struct { 336 UserDid string 337 FollowStatus db.FollowStatus ··· 341 return p.executePlain("user/fragments/follow", w, params) 342 } 343 344 type RepoActionsFragmentParams struct { 345 IsStarred bool 346 RepoAt syntax.ATURI ··· 364 } 365 366 type RepoIndexParams struct { 367 - LoggedInUser *auth.User 368 RepoInfo repoinfo.RepoInfo 369 Active string 370 TagMap map[string][]string ··· 405 } 406 407 type RepoLogParams struct { 408 - LoggedInUser *auth.User 409 RepoInfo repoinfo.RepoInfo 410 TagMap map[string][]string 411 types.RepoLogResponse ··· 419 } 420 421 type RepoCommitParams struct { 422 - LoggedInUser *auth.User 423 RepoInfo repoinfo.RepoInfo 424 Active string 425 EmailToDidOrHandle map[string]string ··· 433 } 434 435 type RepoTreeParams struct { 436 - LoggedInUser *auth.User 437 RepoInfo repoinfo.RepoInfo 438 Active string 439 BreadCrumbs [][]string ··· 469 } 470 471 type RepoBranchesParams struct { 472 - LoggedInUser *auth.User 473 RepoInfo repoinfo.RepoInfo 474 Active string 475 types.RepoBranchesResponse ··· 481 } 482 483 type RepoTagsParams struct { 484 - LoggedInUser *auth.User 485 RepoInfo repoinfo.RepoInfo 486 Active string 487 types.RepoTagsResponse ··· 495 } 496 497 type RepoArtifactParams struct { 498 - LoggedInUser *auth.User 499 RepoInfo repoinfo.RepoInfo 500 Artifact db.Artifact 501 } ··· 505 } 506 507 type RepoBlobParams struct { 508 - LoggedInUser *auth.User 509 RepoInfo repoinfo.RepoInfo 510 Active string 511 BreadCrumbs [][]string ··· 523 case markup.FormatMarkdown: 524 p.rctx.RepoInfo = params.RepoInfo 525 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 526 - params.RenderedContents = template.HTML(p.rctx.RenderMarkdown(params.Contents)) 527 } 528 } 529 ··· 567 } 568 569 type RepoSettingsParams struct { 570 - LoggedInUser *auth.User 571 RepoInfo repoinfo.RepoInfo 572 Collaborators []Collaborator 573 Active string ··· 583 } 584 585 type RepoIssuesParams struct { 586 - LoggedInUser *auth.User 587 RepoInfo repoinfo.RepoInfo 588 Active string 589 Issues []db.Issue ··· 598 } 599 600 type RepoSingleIssueParams struct { 601 - LoggedInUser *auth.User 602 RepoInfo repoinfo.RepoInfo 603 Active string 604 Issue db.Issue ··· 620 } 621 622 type RepoNewIssueParams struct { 623 - LoggedInUser *auth.User 624 RepoInfo repoinfo.RepoInfo 625 Active string 626 } ··· 631 } 632 633 type EditIssueCommentParams struct { 634 - LoggedInUser *auth.User 635 RepoInfo repoinfo.RepoInfo 636 Issue *db.Issue 637 Comment *db.Comment ··· 642 } 643 644 type SingleIssueCommentParams struct { 645 - LoggedInUser *auth.User 646 DidHandleMap map[string]string 647 RepoInfo repoinfo.RepoInfo 648 Issue *db.Issue ··· 654 } 655 656 type RepoNewPullParams struct { 657 - LoggedInUser *auth.User 658 RepoInfo repoinfo.RepoInfo 659 Branches []types.Branch 660 Active string ··· 666 } 667 668 type RepoPullsParams struct { 669 - LoggedInUser *auth.User 670 RepoInfo repoinfo.RepoInfo 671 Pulls []*db.Pull 672 Active string ··· 698 } 699 700 type RepoSinglePullParams struct { 701 - LoggedInUser *auth.User 702 RepoInfo repoinfo.RepoInfo 703 Active string 704 DidHandleMap map[string]string ··· 713 } 714 715 type RepoPullPatchParams struct { 716 - LoggedInUser *auth.User 717 DidHandleMap map[string]string 718 RepoInfo repoinfo.RepoInfo 719 Pull *db.Pull ··· 728 } 729 730 type RepoPullInterdiffParams struct { 731 - LoggedInUser *auth.User 732 DidHandleMap map[string]string 733 RepoInfo repoinfo.RepoInfo 734 Pull *db.Pull ··· 778 } 779 780 type PullResubmitParams struct { 781 - LoggedInUser *auth.User 782 RepoInfo repoinfo.RepoInfo 783 Pull *db.Pull 784 SubmissionId int ··· 789 } 790 791 type PullActionsParams struct { 792 - LoggedInUser *auth.User 793 RepoInfo repoinfo.RepoInfo 794 Pull *db.Pull 795 RoundNumber int ··· 802 } 803 804 type PullNewCommentParams struct { 805 - LoggedInUser *auth.User 806 RepoInfo repoinfo.RepoInfo 807 Pull *db.Pull 808 RoundNumber int
··· 16 "strings" 17 18 "tangled.sh/tangled.sh/core/appview" 19 "tangled.sh/tangled.sh/core/appview/db" 20 + "tangled.sh/tangled.sh/core/appview/oauth" 21 "tangled.sh/tangled.sh/core/appview/pages/markup" 22 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 23 "tangled.sh/tangled.sh/core/appview/pagination" ··· 48 func NewPages(config *appview.Config) *Pages { 49 // initialized with safe defaults, can be overriden per use 50 rctx := &markup.RenderContext{ 51 + IsDev: config.Core.Dev, 52 + CamoUrl: config.Camo.Host, 53 + CamoSecret: config.Camo.SharedSecret, 54 } 55 56 p := &Pages{ 57 t: make(map[string]*template.Template), 58 + dev: config.Core.Dev, 59 embedFS: Files, 60 rctx: rctx, 61 templateDir: "appview/pages", ··· 250 } 251 252 type TimelineParams struct { 253 + LoggedInUser *oauth.User 254 Timeline []db.TimelineEvent 255 DidHandleMap map[string]string 256 } ··· 260 } 261 262 type SettingsParams struct { 263 + LoggedInUser *oauth.User 264 PubKeys []db.PublicKey 265 Emails []db.Email 266 } ··· 270 } 271 272 type KnotsParams struct { 273 + LoggedInUser *oauth.User 274 Registrations []db.Registration 275 } 276 ··· 279 } 280 281 type KnotParams struct { 282 + LoggedInUser *oauth.User 283 DidHandleMap map[string]string 284 Registration *db.Registration 285 Members []string ··· 291 } 292 293 type NewRepoParams struct { 294 + LoggedInUser *oauth.User 295 Knots []string 296 } 297 ··· 300 } 301 302 type ForkRepoParams struct { 303 + LoggedInUser *oauth.User 304 Knots []string 305 RepoInfo repoinfo.RepoInfo 306 } ··· 310 } 311 312 type ProfilePageParams struct { 313 + LoggedInUser *oauth.User 314 Repos []db.Repo 315 CollaboratingRepos []db.Repo 316 ProfileTimeline *db.ProfileTimeline 317 + Card ProfileCard 318 319 DidHandleMap map[string]string 320 } 321 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 331 } 332 333 func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 334 return p.execute("user/profile", w, params) 335 } 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 + 349 type FollowFragmentParams struct { 350 UserDid string 351 FollowStatus db.FollowStatus ··· 355 return p.executePlain("user/fragments/follow", w, params) 356 } 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 + 383 type RepoActionsFragmentParams struct { 384 IsStarred bool 385 RepoAt syntax.ATURI ··· 403 } 404 405 type RepoIndexParams struct { 406 + LoggedInUser *oauth.User 407 RepoInfo repoinfo.RepoInfo 408 Active string 409 TagMap map[string][]string ··· 444 } 445 446 type RepoLogParams struct { 447 + LoggedInUser *oauth.User 448 RepoInfo repoinfo.RepoInfo 449 TagMap map[string][]string 450 types.RepoLogResponse ··· 458 } 459 460 type RepoCommitParams struct { 461 + LoggedInUser *oauth.User 462 RepoInfo repoinfo.RepoInfo 463 Active string 464 EmailToDidOrHandle map[string]string ··· 472 } 473 474 type RepoTreeParams struct { 475 + LoggedInUser *oauth.User 476 RepoInfo repoinfo.RepoInfo 477 Active string 478 BreadCrumbs [][]string ··· 508 } 509 510 type RepoBranchesParams struct { 511 + LoggedInUser *oauth.User 512 RepoInfo repoinfo.RepoInfo 513 Active string 514 types.RepoBranchesResponse ··· 520 } 521 522 type RepoTagsParams struct { 523 + LoggedInUser *oauth.User 524 RepoInfo repoinfo.RepoInfo 525 Active string 526 types.RepoTagsResponse ··· 534 } 535 536 type RepoArtifactParams struct { 537 + LoggedInUser *oauth.User 538 RepoInfo repoinfo.RepoInfo 539 Artifact db.Artifact 540 } ··· 544 } 545 546 type RepoBlobParams struct { 547 + LoggedInUser *oauth.User 548 RepoInfo repoinfo.RepoInfo 549 Active string 550 BreadCrumbs [][]string ··· 562 case markup.FormatMarkdown: 563 p.rctx.RepoInfo = params.RepoInfo 564 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 565 + params.RenderedContents = template.HTML(bluemonday.UGCPolicy().Sanitize(p.rctx.RenderMarkdown(params.Contents))) 566 } 567 } 568 ··· 606 } 607 608 type RepoSettingsParams struct { 609 + LoggedInUser *oauth.User 610 RepoInfo repoinfo.RepoInfo 611 Collaborators []Collaborator 612 Active string ··· 622 } 623 624 type RepoIssuesParams struct { 625 + LoggedInUser *oauth.User 626 RepoInfo repoinfo.RepoInfo 627 Active string 628 Issues []db.Issue ··· 637 } 638 639 type RepoSingleIssueParams struct { 640 + LoggedInUser *oauth.User 641 RepoInfo repoinfo.RepoInfo 642 Active string 643 Issue db.Issue ··· 659 } 660 661 type RepoNewIssueParams struct { 662 + LoggedInUser *oauth.User 663 RepoInfo repoinfo.RepoInfo 664 Active string 665 } ··· 670 } 671 672 type EditIssueCommentParams struct { 673 + LoggedInUser *oauth.User 674 RepoInfo repoinfo.RepoInfo 675 Issue *db.Issue 676 Comment *db.Comment ··· 681 } 682 683 type SingleIssueCommentParams struct { 684 + LoggedInUser *oauth.User 685 DidHandleMap map[string]string 686 RepoInfo repoinfo.RepoInfo 687 Issue *db.Issue ··· 693 } 694 695 type RepoNewPullParams struct { 696 + LoggedInUser *oauth.User 697 RepoInfo repoinfo.RepoInfo 698 Branches []types.Branch 699 Active string ··· 705 } 706 707 type RepoPullsParams struct { 708 + LoggedInUser *oauth.User 709 RepoInfo repoinfo.RepoInfo 710 Pulls []*db.Pull 711 Active string ··· 737 } 738 739 type RepoSinglePullParams struct { 740 + LoggedInUser *oauth.User 741 RepoInfo repoinfo.RepoInfo 742 Active string 743 DidHandleMap map[string]string ··· 752 } 753 754 type RepoPullPatchParams struct { 755 + LoggedInUser *oauth.User 756 DidHandleMap map[string]string 757 RepoInfo repoinfo.RepoInfo 758 Pull *db.Pull ··· 767 } 768 769 type RepoPullInterdiffParams struct { 770 + LoggedInUser *oauth.User 771 DidHandleMap map[string]string 772 RepoInfo repoinfo.RepoInfo 773 Pull *db.Pull ··· 817 } 818 819 type PullResubmitParams struct { 820 + LoggedInUser *oauth.User 821 RepoInfo repoinfo.RepoInfo 822 Pull *db.Pull 823 SubmissionId int ··· 828 } 829 830 type PullActionsParams struct { 831 + LoggedInUser *oauth.User 832 RepoInfo repoinfo.RepoInfo 833 Pull *db.Pull 834 RoundNumber int ··· 841 } 842 843 type PullNewCommentParams struct { 844 + LoggedInUser *oauth.User 845 RepoInfo repoinfo.RepoInfo 846 Pull *db.Pull 847 RoundNumber int
+1
appview/pages/templates/layouts/base.html
··· 7 name="viewport" 8 content="width=device-width, initial-scale=1.0" 9 /> 10 <script src="/static/htmx.min.js"></script> 11 <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 12 <title>{{ block "title" . }}{{ end }} ยท tangled</title>
··· 7 name="viewport" 8 content="width=device-width, initial-scale=1.0" 9 /> 10 + <meta name="htmx-config" content='{"includeIndicatorStyles": false}'> 11 <script src="/static/htmx.min.js"></script> 12 <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 13 <title>{{ block "title" . }}{{ end }} ยท tangled</title>
+1 -1
appview/pages/templates/layouts/footer.html
··· 1 {{ define "layouts/footer" }} 2 - <div class="w-full p-4 bg-white dark:bg-gray-800 rounded-t"> 3 <div class="container mx-auto text-center text-gray-600 dark:text-gray-400 text-sm"> 4 <span class="font-semibold italic">tangled</span> &mdash; made by <a href="/@oppi.li">@oppi.li</a> and <a href="/@icyphox.sh">@icyphox.sh</a> 5 </div>
··· 1 {{ define "layouts/footer" }} 2 + <div class="w-full p-4 bg-white dark:bg-gray-800 rounded-t drop-shadow-sm"> 3 <div class="container mx-auto text-center text-gray-600 dark:text-gray-400 text-sm"> 4 <span class="font-semibold italic">tangled</span> &mdash; made by <a href="/@oppi.li">@oppi.li</a> and <a href="/@icyphox.sh">@icyphox.sh</a> 5 </div>
+1 -1
appview/pages/templates/repo/fragments/cloneInstructions.html
··· 4 {{ $knot = "tangled.sh" }} 5 {{ end }} 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" 8 > 9 <div class="flex flex-col gap-2"> 10 <strong>push</strong>
··· 4 {{ $knot = "tangled.sh" }} 5 {{ end }} 6 <section 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 > 9 <div class="flex flex-col gap-2"> 10 <strong>push</strong>
+6 -6
appview/pages/templates/repo/fragments/filetree.html
··· 2 {{ if and .Name .IsDirectory }} 3 <details open> 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> 8 </span> 9 </summary> 10 <div class="ml-1 pl-4 border-l border-gray-200 dark:border-gray-700"> ··· 14 </div> 15 </details> 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> 20 </div> 21 {{ else }} 22 {{ range $child := .Children }}
··· 2 {{ if and .Name .IsDirectory }} 3 <details open> 4 <summary class="cursor-pointer list-none pt-1"> 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 </span> 9 </summary> 10 <div class="ml-1 pl-4 border-l border-gray-200 dark:border-gray-700"> ··· 14 </div> 15 </details> 16 {{ else if .Name }} 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 </div> 21 {{ else }} 22 {{ range $child := .Children }}
+13 -12
appview/pages/templates/repo/fragments/repoActions.html
··· 2 <div class="flex items-center gap-2 z-auto"> 3 <button 4 id="starBtn" 5 - class="btn disabled:opacity-50 disabled:cursor-not-allowed" 6 {{ if .IsStarred }} 7 hx-delete="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}" 8 {{ else }} ··· 14 hx-swap="outerHTML" 15 hx-disabled-elt="#starBtn" 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> 27 </button> 28 {{ if .DisableFork }} 29 <button ··· 36 </button> 37 {{ else }} 38 <a 39 - class="btn text-sm no-underline hover:no-underline flex items-center gap-2" 40 href="/{{ .FullName }}/fork" 41 > 42 {{ i "git-fork" "w-4 h-4" }} 43 fork 44 </a> 45 {{ end }} 46 </div>
··· 2 <div class="flex items-center gap-2 z-auto"> 3 <button 4 id="starBtn" 5 + class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group" 6 {{ if .IsStarred }} 7 hx-delete="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}" 8 {{ else }} ··· 14 hx-swap="outerHTML" 15 hx-disabled-elt="#starBtn" 16 > 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" }} 26 </button> 27 {{ if .DisableFork }} 28 <button ··· 35 </button> 36 {{ else }} 37 <a 38 + class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 39 + hx-boost="true" 40 href="/{{ .FullName }}/fork" 41 > 42 {{ i "git-fork" "w-4 h-4" }} 43 fork 44 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 45 </a> 46 {{ end }} 47 </div>
+3 -3
appview/pages/templates/repo/index.html
··· 103 class="{{ $linkstyle }}" 104 > 105 <div class="flex items-center gap-2"> 106 - {{ i "folder" "w-3 h-3 fill-current" }} 107 {{ .Name }} 108 </div> 109 </a> ··· 125 class="{{ $linkstyle }}" 126 > 127 <div class="flex items-center gap-2"> 128 - {{ i "file" "w-3 h-3" }}{{ .Name }} 129 </div> 130 </a> 131 ··· 320 {{ define "repoAfter" }} 321 {{- if .HTMLReadme }} 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 }} 324 prose dark:prose-invert dark:[&_pre]:bg-gray-900 325 dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 326 dark:[&_pre]:border dark:[&_pre]:border-gray-700
··· 103 class="{{ $linkstyle }}" 104 > 105 <div class="flex items-center gap-2"> 106 + {{ i "folder" "size-4 fill-current" }} 107 {{ .Name }} 108 </div> 109 </a> ··· 125 class="{{ $linkstyle }}" 126 > 127 <div class="flex items-center gap-2"> 128 + {{ i "file" "size-4" }}{{ .Name }} 129 </div> 130 </a> 131 ··· 320 {{ define "repoAfter" }} 321 {{- if .HTMLReadme }} 322 <section 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 prose dark:prose-invert dark:[&_pre]:bg-gray-900 325 dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 326 dark:[&_pre]:border dark:[&_pre]:border-gray-700
+26 -17
appview/pages/templates/repo/issues/issues.html
··· 1 {{ define "title" }}issues &middot; {{ .RepoInfo.FullName }}{{ end }} 2 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> 21 {{ end }} 22 23 {{ define "repoAfter" }}
··· 1 {{ define "title" }}issues &middot; {{ .RepoInfo.FullName }}{{ end }} 2 3 {{ define "repoContent" }} 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> 30 {{ end }} 31 32 {{ define "repoAfter" }}
+7 -2
appview/pages/templates/repo/new.html
··· 5 <p class="text-xl font-bold dark:text-white">Create a new repository</p> 6 </div> 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"> 9 <div class="space-y-2"> 10 <label for="name" class="-mb-1 dark:text-white">Repository name</label> 11 <input ··· 60 </fieldset> 61 62 <div class="space-y-2"> 63 - <button type="submit" class="btn">create repo</button> 64 <div id="repo" class="error"></div> 65 </div> 66 </form>
··· 5 <p class="text-xl font-bold dark:text-white">Create a new repository</p> 6 </div> 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" hx-indicator="#spinner"> 9 <div class="space-y-2"> 10 <label for="name" class="-mb-1 dark:text-white">Repository name</label> 11 <input ··· 60 </fieldset> 61 62 <div class="space-y-2"> 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> 69 <div id="repo" class="error"></div> 70 </div> 71 </form>
+14 -9
appview/pages/templates/repo/pulls/fragments/pullActions.html
··· 17 hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 18 hx-target="#actions-{{$roundNumber}}" 19 hx-swap="outerHtml" 20 - class="btn p-2 flex items-center gap-2 no-underline hover:no-underline"> 21 {{ i "message-square-plus" "w-4 h-4" }} 22 <span>comment</span> 23 </button> 24 {{ if and $isPushAllowed $isOpen $isLastRound }} 25 {{ $disabled := "" }} ··· 30 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 31 hx-swap="none" 32 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 {{ i "git-merge" "w-4 h-4" }} 35 - <span>merge</span> 36 </button> 37 {{ end }} 38 ··· 51 {{ end }} 52 53 hx-disabled-elt="#resubmitBtn" 54 - class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }} 55 56 {{ if $disabled }} 57 title="Update this branch to resubmit this pull request" ··· 59 title="Resubmit this pull request" 60 {{ end }} 61 > 62 - {{ i "rotate-ccw" "w-4 h-4" }} 63 <span>resubmit</span> 64 </button> 65 {{ end }} 66 ··· 68 <button 69 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 70 hx-swap="none" 71 - class="btn p-2 flex items-center gap-2"> 72 - {{ i "ban" "w-4 h-4" }} 73 <span>close</span> 74 </button> 75 {{ end }} 76 ··· 78 <button 79 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 80 hx-swap="none" 81 - class="btn p-2 flex items-center gap-2"> 82 - {{ i "refresh-ccw-dot" "w-4 h-4" }} 83 <span>reopen</span> 84 </button> 85 {{ end }} 86 </div>
··· 17 hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 18 hx-target="#actions-{{$roundNumber}}" 19 hx-swap="outerHtml" 20 + class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group"> 21 {{ i "message-square-plus" "w-4 h-4" }} 22 <span>comment</span> 23 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 24 </button> 25 {{ if and $isPushAllowed $isOpen $isLastRound }} 26 {{ $disabled := "" }} ··· 31 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 32 hx-swap="none" 33 hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 34 + class="btn p-2 flex items-center gap-2 group" {{ $disabled }}> 35 {{ i "git-merge" "w-4 h-4" }} 36 + <span>merge</span> 37 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 38 </button> 39 {{ end }} 40 ··· 53 {{ end }} 54 55 hx-disabled-elt="#resubmitBtn" 56 + class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }} 57 58 {{ if $disabled }} 59 title="Update this branch to resubmit this pull request" ··· 61 title="Resubmit this pull request" 62 {{ end }} 63 > 64 + {{ i "rotate-ccw" "w-4 h-4" }} 65 <span>resubmit</span> 66 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 67 </button> 68 {{ end }} 69 ··· 71 <button 72 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 73 hx-swap="none" 74 + class="btn p-2 flex items-center gap-2 group"> 75 + {{ i "ban" "w-4 h-4" }} 76 <span>close</span> 77 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 78 </button> 79 {{ end }} 80 ··· 82 <button 83 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 84 hx-swap="none" 85 + class="btn p-2 flex items-center gap-2 group"> 86 + {{ i "refresh-ccw-dot" "w-4 h-4" }} 87 <span>reopen</span> 88 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 89 </button> 90 {{ end }} 91 </div>
+9 -11
appview/pages/templates/repo/pulls/fragments/pullHeader.html
··· 42 </span> 43 </span> 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 - 54 <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 }} 56 </span> 57 - </span> 58 {{ end }} 59 </span> 60 </div>
··· 42 </span> 43 </span> 44 {{ if not .Pull.IsPatchBased }} 45 + from 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"> 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 -}} 55 </span> 56 {{ end }} 57 </span> 58 </div>
+1 -1
appview/pages/templates/repo/pulls/new.html
··· 18 > 19 <option disabled selected>target branch</option> 20 {{ range .Branches }} 21 - <option value="{{ .Reference.Name }}" class="py-1"> 22 {{ .Reference.Name }} 23 </option> 24 {{ end }}
··· 18 > 19 <option disabled selected>target branch</option> 20 {{ range .Branches }} 21 + <option value="{{ .Reference.Name }}" class="py-1" {{if .IsDefault}}selected{{end}}> 22 {{ .Reference.Name }} 23 </option> 24 {{ end }}
+8 -4
appview/pages/templates/repo/pulls/pull.html
··· 51 </span> 52 </div> 53 54 - <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2" 55 hx-boost="true" 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> 58 </a> 59 {{ if not (eq .RoundNumber 0) }} 60 - <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2" 61 hx-boost="true" 62 href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff"> 63 - {{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">interdiff</span> 64 </a> 65 <span id="interdiff-error-{{.RoundNumber}}"></span> 66 {{ end }}
··· 51 </span> 52 </div> 53 54 + <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 55 hx-boost="true" 56 href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}"> 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" }} 60 </a> 61 {{ if not (eq .RoundNumber 0) }} 62 + <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 63 hx-boost="true" 64 href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff"> 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" }} 68 </a> 69 <span id="interdiff-error-{{.RoundNumber}}"></span> 70 {{ end }}
+32 -29
appview/pages/templates/repo/pulls/pulls.html
··· 2 3 {{ define "repoContent" }} 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" 10 > 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> 23 <a 24 href="/{{ .RepoInfo.FullName }}/pulls/new" 25 class="btn text-sm flex items-center gap-2 no-underline hover:no-underline" ··· 79 </span> 80 </span> 81 {{ 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> 94 </span> 95 {{ end }} 96 <span class="before:content-['ยท']">
··· 2 3 {{ define "repoContent" }} 4 <div class="flex justify-between items-center"> 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 }}" 23 > 24 + {{ i "ban" "w-4 h-4" }} 25 + <span>{{ .RepoInfo.Stats.PullCount.Closed }} closed</span> 26 + </a> 27 + </div> 28 <a 29 href="/{{ .RepoInfo.FullName }}/pulls/new" 30 class="btn text-sm flex items-center gap-2 no-underline hover:no-underline" ··· 84 </span> 85 </span> 86 {{ if not .IsPatchBased }} 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 -}} 97 </span> 98 {{ end }} 99 <span class="before:content-['ยท']">
+2 -2
appview/pages/templates/repo/tree.html
··· 54 <div class="flex justify-between items-center"> 55 <a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 56 <div class="flex items-center gap-2"> 57 - {{ i "folder" "w-3 h-3 fill-current" }}{{ .Name }} 58 </div> 59 </a> 60 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time> ··· 69 <div class="flex justify-between items-center"> 70 <a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 71 <div class="flex items-center gap-2"> 72 - {{ i "file" "w-3 h-3" }}{{ .Name }} 73 </div> 74 </a> 75 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
··· 54 <div class="flex justify-between items-center"> 55 <a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 56 <div class="flex items-center gap-2"> 57 + {{ i "folder" "size-4 fill-current" }}{{ .Name }} 58 </div> 59 </a> 60 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time> ··· 69 <div class="flex justify-between items-center"> 70 <a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 71 <div class="flex items-center gap-2"> 72 + {{ i "file" "size-4" }}{{ .Name }} 73 </div> 74 </a> 75 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
+1 -1
appview/pages/templates/timeline.html
··· 23 </div> 24 <div class="italic text-lg"> 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>. 27 Read an introduction to Tangled <a href="https://blog.tangled.sh/intro">here</a>.</p> 28 </div> 29 </div>
··· 23 </div> 24 <div class="italic text-lg"> 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>. 27 Read an introduction to Tangled <a href="https://blog.tangled.sh/intro">here</a>.</p> 28 </div> 29 </div>
+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
···
··· 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
···
··· 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
···
··· 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
··· 8 content="width=device-width, initial-scale=1.0" 9 /> 10 <script src="/static/htmx.min.js"></script> 11 - <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 12 <title>login</title> 13 </head> 14 <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"> 17 tangled 18 </h1> 19 <h2 class="text-center text-xl italic dark:text-white"> 20 tightly-knit social coding. 21 </h2> 22 <form 23 - class="w-full mt-4" 24 hx-post="/login" 25 hx-swap="none" 26 - hx-disabled-elt="this" 27 > 28 <div class="flex flex-col"> 29 <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 <input 47 - type="password" 48 - id="app_password" 49 - name="app_password" 50 - tabindex="2" 51 required 52 /> 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 - >. 60 </span> 61 </div> 62 ··· 70 </button> 71 </form> 72 <p class="text-sm text-gray-500"> 73 - Join our <a href="https://chat.tangled.sh">Discord</a> or IRC channel: 74 <a href="https://web.libera.chat/#tangled" 75 ><code>#tangled</code> on Libera Chat</a 76 >.
··· 8 content="width=device-width, initial-scale=1.0" 9 /> 10 <script src="/static/htmx.min.js"></script> 11 + <link 12 + rel="stylesheet" 13 + href="/static/tw.css?{{ cssContentHash }}" 14 + type="text/css" 15 + /> 16 <title>login</title> 17 </head> 18 <body class="flex items-center justify-center min-h-screen"> 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 + > 23 tangled 24 </h1> 25 <h2 class="text-center text-xl italic dark:text-white"> 26 tightly-knit social coding. 27 </h2> 28 <form 29 + class="mt-4 max-w-sm mx-auto" 30 hx-post="/login" 31 hx-swap="none" 32 + hx-disabled-elt="#login-button" 33 > 34 <div class="flex flex-col"> 35 <label for="handle">handle</label> 36 <input 37 + type="text" 38 + id="handle" 39 + name="handle" 40 + tabindex="1" 41 required 42 /> 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. 48 </span> 49 </div> 50 ··· 58 </button> 59 </form> 60 <p class="text-sm text-gray-500"> 61 + Join our <a href="https://chat.tangled.sh">Discord</a> or 62 + IRC channel: 63 <a href="https://web.libera.chat/#tangled" 64 ><code>#tangled</code> on Libera Chat</a 65 >.
+80 -91
appview/pages/templates/user/profile.html
··· 1 - {{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }} 2 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 }} 7 </div> 8 - <div class="md:col-span-2 order-2 md:order-2"> 9 {{ block "ownRepos" . }}{{ end }} 10 {{ block "collaboratingRepos" . }}{{ end }} 11 </div> 12 - <div class="md:col-span-2 order-3 md:order-3"> 13 {{ block "profileTimeline" . }}{{ end }} 14 </div> 15 </div> 16 {{ end }} 17 18 {{ define "profileTimeline" }} 19 - <p class="text-sm font-bold py-2 dark:text-white px-6">ACTIVITY</p> 20 <div class="flex flex-col gap-6 relative"> 21 {{ with .ProfileTimeline }} 22 {{ range $idx, $byMonth := .ByMonth }} ··· 225 {{ end }} 226 {{ end }} 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 }} 235 </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> 253 </div> 254 {{ end }} 255 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> 285 </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> 302 </div> 303 - {{ if .Description }} 304 - <div class="text-gray-600 dark:text-gray-300 text-sm"> 305 - {{ .Description }} 306 </div> 307 {{ 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 </div> 318 - {{ else }} 319 - <p class="px-6 dark:text-white">This user is not collaborating.</p> 320 - {{ end }} 321 </div> 322 {{ end }}
··· 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ 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-3 order-2 md:order-2"> 9 {{ block "ownRepos" . }}{{ end }} 10 {{ block "collaboratingRepos" . }}{{ end }} 11 </div> 12 + <div class="md:col-span-3 order-3 md:order-3"> 13 {{ block "profileTimeline" . }}{{ end }} 14 </div> 15 </div> 16 {{ end }} 17 18 {{ define "profileTimeline" }} 19 + <p class="text-sm font-bold p-2 dark:text-white">ACTIVITY</p> 20 <div class="flex flex-col gap-6 relative"> 21 {{ with .ProfileTimeline }} 22 {{ range $idx, $byMonth := .ByMonth }} ··· 225 {{ end }} 226 {{ end }} 227 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> 271 </div> 272 + {{ else }} 273 + <p class="px-6 dark:text-white">This user does not have any repos yet.</p> 274 + {{ end }} 275 </div> 276 {{ end }} 277 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> 290 </div> 291 + {{ if .Description }} 292 + <div class="text-gray-600 dark:text-gray-300 text-sm"> 293 + {{ .Description }} 294 </div> 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> 302 </div> 303 {{ end }} 304 </div> 305 + </div> 306 + {{ else }} 307 + <p class="px-6 dark:text-white">This user is not collaborating.</p> 308 + {{ end }} 309 </div> 310 + {{ end }} 311 {{ end }}
+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
··· 13 "github.com/go-chi/chi/v5" 14 "tangled.sh/tangled.sh/core/api/tangled" 15 "tangled.sh/tangled.sh/core/appview" 16 - "tangled.sh/tangled.sh/core/appview/auth" 17 "tangled.sh/tangled.sh/core/appview/db" 18 "tangled.sh/tangled.sh/core/appview/email" 19 "tangled.sh/tangled.sh/core/appview/middleware" 20 "tangled.sh/tangled.sh/core/appview/pages" 21 22 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 27 28 type Settings struct { 29 Db *db.DB 30 - Auth *auth.Auth 31 Pages *pages.Pages 32 Config *appview.Config 33 } ··· 35 func (s *Settings) Router() http.Handler { 36 r := chi.NewRouter() 37 38 - r.Use(middleware.AuthMiddleware(s.Auth)) 39 40 r.Get("/", s.settings) 41 ··· 56 } 57 58 func (s *Settings) settings(w http.ResponseWriter, r *http.Request) { 59 - user := s.Auth.GetUser(r) 60 pubKeys, err := db.GetPublicKeys(s.Db, user.Did) 61 if err != nil { 62 log.Println(err) ··· 79 verifyURL := s.verifyUrl(did, emailAddr, code) 80 81 return email.Email{ 82 - APIKey: s.Config.ResendApiKey, 83 From: "noreply@notifs.tangled.sh", 84 To: emailAddr, 85 Subject: "Verify your Tangled email", ··· 111 log.Println("unimplemented") 112 return 113 case http.MethodPut: 114 - did := s.Auth.GetDid(r) 115 emAddr := r.FormValue("email") 116 emAddr = strings.TrimSpace(emAddr) 117 ··· 174 s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.") 175 return 176 case http.MethodDelete: 177 - did := s.Auth.GetDid(r) 178 emailAddr := r.FormValue("email") 179 emailAddr = strings.TrimSpace(emailAddr) 180 ··· 207 208 func (s *Settings) verifyUrl(did string, email string, code string) string { 209 var appUrl string 210 - if s.Config.Dev { 211 - appUrl = "http://" + s.Config.ListenAddr 212 } else { 213 appUrl = "https://tangled.sh" 214 } ··· 252 return 253 } 254 255 - did := s.Auth.GetDid(r) 256 emAddr := r.FormValue("email") 257 emAddr = strings.TrimSpace(emAddr) 258 ··· 323 } 324 325 func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) { 326 - did := s.Auth.GetDid(r) 327 emailAddr := r.FormValue("email") 328 emailAddr = strings.TrimSpace(emailAddr) 329 ··· 348 log.Println("unimplemented") 349 return 350 case http.MethodPut: 351 - did := s.Auth.GetDid(r) 352 key := r.FormValue("key") 353 key = strings.TrimSpace(key) 354 name := r.FormValue("name") 355 - client, _ := s.Auth.AuthorizedClient(r) 356 357 - _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 358 if err != nil { 359 log.Printf("parsing public key: %s", err) 360 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 } 379 380 // store in pds too 381 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 382 Collection: tangled.PublicKeyNSID, 383 Repo: did, 384 Rkey: rkey, ··· 409 return 410 411 case http.MethodDelete: 412 - did := s.Auth.GetDid(r) 413 q := r.URL.Query() 414 415 name := q.Get("name") ··· 420 log.Println(rkey) 421 log.Println(key) 422 423 - client, _ := s.Auth.AuthorizedClient(r) 424 425 if err := db.DeletePublicKey(s.Db, did, name, key); err != nil { 426 log.Printf("removing public key: %s", err) ··· 430 431 if rkey != "" { 432 // remove from pds too 433 - _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 434 Collection: tangled.PublicKeyNSID, 435 Repo: did, 436 Rkey: rkey,
··· 13 "github.com/go-chi/chi/v5" 14 "tangled.sh/tangled.sh/core/api/tangled" 15 "tangled.sh/tangled.sh/core/appview" 16 "tangled.sh/tangled.sh/core/appview/db" 17 "tangled.sh/tangled.sh/core/appview/email" 18 "tangled.sh/tangled.sh/core/appview/middleware" 19 + "tangled.sh/tangled.sh/core/appview/oauth" 20 "tangled.sh/tangled.sh/core/appview/pages" 21 22 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 27 28 type Settings struct { 29 Db *db.DB 30 + OAuth *oauth.OAuth 31 Pages *pages.Pages 32 Config *appview.Config 33 } ··· 35 func (s *Settings) Router() http.Handler { 36 r := chi.NewRouter() 37 38 + r.Use(middleware.AuthMiddleware(s.OAuth)) 39 40 r.Get("/", s.settings) 41 ··· 56 } 57 58 func (s *Settings) settings(w http.ResponseWriter, r *http.Request) { 59 + user := s.OAuth.GetUser(r) 60 pubKeys, err := db.GetPublicKeys(s.Db, user.Did) 61 if err != nil { 62 log.Println(err) ··· 79 verifyURL := s.verifyUrl(did, emailAddr, code) 80 81 return email.Email{ 82 + APIKey: s.Config.Resend.ApiKey, 83 From: "noreply@notifs.tangled.sh", 84 To: emailAddr, 85 Subject: "Verify your Tangled email", ··· 111 log.Println("unimplemented") 112 return 113 case http.MethodPut: 114 + did := s.OAuth.GetDid(r) 115 emAddr := r.FormValue("email") 116 emAddr = strings.TrimSpace(emAddr) 117 ··· 174 s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.") 175 return 176 case http.MethodDelete: 177 + did := s.OAuth.GetDid(r) 178 emailAddr := r.FormValue("email") 179 emailAddr = strings.TrimSpace(emailAddr) 180 ··· 207 208 func (s *Settings) verifyUrl(did string, email string, code string) string { 209 var appUrl string 210 + if s.Config.Core.Dev { 211 + appUrl = "http://" + s.Config.Core.ListenAddr 212 } else { 213 appUrl = "https://tangled.sh" 214 } ··· 252 return 253 } 254 255 + did := s.OAuth.GetDid(r) 256 emAddr := r.FormValue("email") 257 emAddr = strings.TrimSpace(emAddr) 258 ··· 323 } 324 325 func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) { 326 + did := s.OAuth.GetDid(r) 327 emailAddr := r.FormValue("email") 328 emailAddr = strings.TrimSpace(emailAddr) 329 ··· 348 log.Println("unimplemented") 349 return 350 case http.MethodPut: 351 + did := s.OAuth.GetDid(r) 352 key := r.FormValue("key") 353 key = strings.TrimSpace(key) 354 name := r.FormValue("name") 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 + } 360 361 + _, _, _, _, err = ssh.ParseAuthorizedKey([]byte(key)) 362 if err != nil { 363 log.Printf("parsing public key: %s", err) 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.") ··· 382 } 383 384 // store in pds too 385 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 386 Collection: tangled.PublicKeyNSID, 387 Repo: did, 388 Rkey: rkey, ··· 413 return 414 415 case http.MethodDelete: 416 + did := s.OAuth.GetDid(r) 417 q := r.URL.Query() 418 419 name := q.Get("name") ··· 424 log.Println(rkey) 425 log.Println(key) 426 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 + } 433 434 if err := db.DeletePublicKey(s.Db, did, name, key); err != nil { 435 log.Printf("removing public key: %s", err) ··· 439 440 if rkey != "" { 441 // remove from pds too 442 + _, err := client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 443 Collection: tangled.PublicKeyNSID, 444 Repo: did, 445 Rkey: rkey,
+20 -10
appview/state/artifact.go
··· 16 "tangled.sh/tangled.sh/core/api/tangled" 17 "tangled.sh/tangled.sh/core/appview" 18 "tangled.sh/tangled.sh/core/appview/db" 19 "tangled.sh/tangled.sh/core/appview/pages" 20 "tangled.sh/tangled.sh/core/types" 21 ) 22 23 // TODO: proper statuses here on early exit 24 func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) { 25 - user := s.auth.GetUser(r) 26 tagParam := chi.URLParam(r, "tag") 27 f, err := s.fullyResolvedRepo(r) 28 if err != nil { ··· 46 } 47 defer file.Close() 48 49 - client, _ := s.auth.AuthorizedClient(r) 50 51 - uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) 52 if err != nil { 53 log.Println("failed to upload blob", err) 54 s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.") ··· 60 rkey := appview.TID() 61 createdAt := time.Now() 62 63 - putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 64 Collection: tangled.RepoArtifactNSID, 65 Repo: user.Did, 66 Rkey: rkey, ··· 140 return 141 } 142 143 - client, _ := s.auth.AuthorizedClient(r) 144 145 artifacts, err := db.GetArtifact( 146 s.db, ··· 159 160 artifact := artifacts[0] 161 162 - getBlobResp, err := comatproto.SyncGetBlob(r.Context(), client, artifact.BlobCid.String(), artifact.Did) 163 if err != nil { 164 log.Println("failed to get blob from pds", err) 165 return ··· 171 172 // TODO: proper statuses here on early exit 173 func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) { 174 - user := s.auth.GetUser(r) 175 tagParam := chi.URLParam(r, "tag") 176 filename := chi.URLParam(r, "file") 177 f, err := s.fullyResolvedRepo(r) ··· 180 return 181 } 182 183 - client, _ := s.auth.AuthorizedClient(r) 184 185 tag := plumbing.NewHash(tagParam) 186 ··· 208 return 209 } 210 211 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 212 Collection: tangled.RepoArtifactNSID, 213 Repo: user.Did, 214 Rkey: artifact.Rkey, ··· 254 return nil, err 255 } 256 257 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 258 if err != nil { 259 return nil, err 260 }
··· 16 "tangled.sh/tangled.sh/core/api/tangled" 17 "tangled.sh/tangled.sh/core/appview" 18 "tangled.sh/tangled.sh/core/appview/db" 19 + "tangled.sh/tangled.sh/core/appview/knotclient" 20 "tangled.sh/tangled.sh/core/appview/pages" 21 "tangled.sh/tangled.sh/core/types" 22 ) 23 24 // TODO: proper statuses here on early exit 25 func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) { 26 + user := s.oauth.GetUser(r) 27 tagParam := chi.URLParam(r, "tag") 28 f, err := s.fullyResolvedRepo(r) 29 if err != nil { ··· 47 } 48 defer file.Close() 49 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 + } 56 57 + uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file) 58 if err != nil { 59 log.Println("failed to upload blob", err) 60 s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.") ··· 66 rkey := appview.TID() 67 createdAt := time.Now() 68 69 + putRecordResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 70 Collection: tangled.RepoArtifactNSID, 71 Repo: user.Did, 72 Rkey: rkey, ··· 146 return 147 } 148 149 + client, err := s.oauth.AuthorizedClient(r) 150 + if err != nil { 151 + log.Println("failed to get authorized client", err) 152 + return 153 + } 154 155 artifacts, err := db.GetArtifact( 156 s.db, ··· 169 170 artifact := artifacts[0] 171 172 + getBlobResp, err := client.SyncGetBlob(r.Context(), artifact.BlobCid.String(), artifact.Did) 173 if err != nil { 174 log.Println("failed to get blob from pds", err) 175 return ··· 181 182 // TODO: proper statuses here on early exit 183 func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) { 184 + user := s.oauth.GetUser(r) 185 tagParam := chi.URLParam(r, "tag") 186 filename := chi.URLParam(r, "file") 187 f, err := s.fullyResolvedRepo(r) ··· 190 return 191 } 192 193 + client, _ := s.oauth.AuthorizedClient(r) 194 195 tag := plumbing.NewHash(tagParam) 196 ··· 218 return 219 } 220 221 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 222 Collection: tangled.RepoArtifactNSID, 223 Repo: user.Did, 224 Rkey: artifact.Rkey, ··· 264 return nil, err 265 } 266 267 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 268 if err != nil { 269 return nil, err 270 }
+8 -4
appview/state/follow.go
··· 14 ) 15 16 func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 17 - currentUser := s.auth.GetUser(r) 18 19 subject := r.URL.Query().Get("subject") 20 if subject == "" { ··· 32 return 33 } 34 35 - client, _ := s.auth.AuthorizedClient(r) 36 37 switch r.Method { 38 case http.MethodPost: 39 createdAt := time.Now().Format(time.RFC3339) 40 rkey := appview.TID() 41 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 42 Collection: tangled.GraphFollowNSID, 43 Repo: currentUser.Did, 44 Rkey: rkey, ··· 75 return 76 } 77 78 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 79 Collection: tangled.GraphFollowNSID, 80 Repo: currentUser.Did, 81 Rkey: follow.Rkey,
··· 14 ) 15 16 func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 17 + currentUser := s.oauth.GetUser(r) 18 19 subject := r.URL.Query().Get("subject") 20 if subject == "" { ··· 32 return 33 } 34 35 + client, err := s.oauth.AuthorizedClient(r) 36 + if err != nil { 37 + log.Println("failed to authorize client") 38 + return 39 + } 40 41 switch r.Method { 42 case http.MethodPost: 43 createdAt := time.Now().Format(time.RFC3339) 44 rkey := appview.TID() 45 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 46 Collection: tangled.GraphFollowNSID, 47 Repo: currentUser.Did, 48 Rkey: rkey, ··· 79 return 80 } 81 82 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 83 Collection: tangled.GraphFollowNSID, 84 Repo: currentUser.Did, 85 Rkey: follow.Rkey,
+2 -2
appview/state/git_http.go
··· 15 repo := chi.URLParam(r, "repo") 16 17 scheme := "https" 18 - if s.config.Dev { 19 scheme = "http" 20 } 21 targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) ··· 52 repo := chi.URLParam(r, "repo") 53 54 scheme := "https" 55 - if s.config.Dev { 56 scheme = "http" 57 } 58 targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery)
··· 15 repo := chi.URLParam(r, "repo") 16 17 scheme := "https" 18 + if s.config.Core.Dev { 19 scheme = "http" 20 } 21 targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) ··· 52 repo := chi.URLParam(r, "repo") 53 54 scheme := "https" 55 + if s.config.Core.Dev { 56 scheme = "http" 57 } 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
··· 2 3 import ( 4 "context" 5 "log" 6 "net/http" 7 "strconv" ··· 20 return func(next http.Handler) http.Handler { 21 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 // requires auth also 23 - actor := s.auth.GetUser(r) 24 if actor == nil { 25 // we need a logged in user 26 log.Printf("not logged in, redirecting") ··· 54 return func(next http.Handler) http.Handler { 55 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 // requires auth also 57 - actor := s.auth.GetUser(r) 58 if actor == nil { 59 // we need a logged in user 60 log.Printf("not logged in, redirecting") ··· 131 if err != nil { 132 // invalid did or handle 133 log.Println("failed to resolve repo") 134 - w.WriteHeader(http.StatusNotFound) 135 return 136 } 137 ··· 175 }) 176 } 177 }
··· 2 3 import ( 4 "context" 5 + "fmt" 6 "log" 7 "net/http" 8 "strconv" ··· 21 return func(next http.Handler) http.Handler { 22 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 // requires auth also 24 + actor := s.oauth.GetUser(r) 25 if actor == nil { 26 // we need a logged in user 27 log.Printf("not logged in, redirecting") ··· 55 return func(next http.Handler) http.Handler { 56 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 57 // requires auth also 58 + actor := s.oauth.GetUser(r) 59 if actor == nil { 60 // we need a logged in user 61 log.Printf("not logged in, redirecting") ··· 132 if err != nil { 133 // invalid did or handle 134 log.Println("failed to resolve repo") 135 + s.pages.Error404(w) 136 return 137 } 138 ··· 176 }) 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
··· 7 "fmt" 8 "log" 9 "net/http" 10 11 "github.com/bluesky-social/indigo/atproto/identity" 12 "github.com/go-chi/chi/v5" 13 "tangled.sh/tangled.sh/core/appview/db" 14 "tangled.sh/tangled.sh/core/appview/pages" 15 ) 16 17 - func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 18 didOrHandle := chi.URLParam(r, "user") 19 if didOrHandle == "" { 20 http.Error(w, "Bad request", http.StatusBadRequest) ··· 27 return 28 } 29 30 repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 31 if err != nil { 32 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 33 } 34 35 collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 36 if err != nil { 37 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 38 } 39 40 timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String()) 41 if err != nil { 42 log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) ··· 76 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 77 } 78 79 - loggedInUser := s.auth.GetUser(r) 80 followStatus := db.IsNotFollowing 81 if loggedInUser != nil { 82 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) ··· 85 profileAvatarUri := s.GetAvatarUri(ident.Handle.String()) 86 s.pages.ProfilePage(w, pages.ProfilePageParams{ 87 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, 95 }, 96 - FollowStatus: db.FollowStatus(followStatus), 97 - DidHandleMap: didHandleMap, 98 - AvatarUri: profileAvatarUri, 99 ProfileTimeline: timeline, 100 }) 101 } 102 103 func (s *State) GetAvatarUri(handle string) string { 104 - secret := s.config.AvatarSharedSecret 105 h := hmac.New(sha256.New, []byte(secret)) 106 h.Write([]byte(handle)) 107 signature := hex.EncodeToString(h.Sum(nil)) 108 - return fmt.Sprintf("%s/%s/%s", s.config.AvatarHost, signature, handle) 109 }
··· 7 "fmt" 8 "log" 9 "net/http" 10 + "slices" 11 + "strings" 12 13 + comatproto "github.com/bluesky-social/indigo/api/atproto" 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" 17 "github.com/go-chi/chi/v5" 18 + "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/db" 20 "tangled.sh/tangled.sh/core/appview/pages" 21 ) 22 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) { 34 didOrHandle := chi.URLParam(r, "user") 35 if didOrHandle == "" { 36 http.Error(w, "Bad request", http.StatusBadRequest) ··· 43 return 44 } 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 + 51 repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 52 if err != nil { 53 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 54 } 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 + 70 collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 71 if err != nil { 72 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 73 } 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 + 83 timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String()) 84 if err != nil { 85 log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) ··· 119 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 120 } 121 122 + loggedInUser := s.oauth.GetUser(r) 123 followStatus := db.IsNotFollowing 124 if loggedInUser != nil { 125 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) ··· 128 profileAvatarUri := s.GetAvatarUri(ident.Handle.String()) 129 s.pages.ProfilePage(w, pages.ProfilePageParams{ 130 LoggedInUser: loggedInUser, 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, 142 }, 143 ProfileTimeline: timeline, 144 }) 145 } 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 + 192 func (s *State) GetAvatarUri(handle string) string { 193 + secret := s.config.Avatar.SharedSecret 194 h := hmac.New(sha256.New, []byte(secret)) 195 h.Write([]byte(handle)) 196 signature := hex.EncodeToString(h.Sum(nil)) 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 + }) 423 }
+84 -53
appview/state/pull.go
··· 13 14 "tangled.sh/tangled.sh/core/api/tangled" 15 "tangled.sh/tangled.sh/core/appview" 16 - "tangled.sh/tangled.sh/core/appview/auth" 17 "tangled.sh/tangled.sh/core/appview/db" 18 "tangled.sh/tangled.sh/core/appview/pages" 19 "tangled.sh/tangled.sh/core/patchutil" 20 "tangled.sh/tangled.sh/core/types" ··· 29 func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { 30 switch r.Method { 31 case http.MethodGet: 32 - user := s.auth.GetUser(r) 33 f, err := s.fullyResolvedRepo(r) 34 if err != nil { 35 log.Println("failed to get repo and knot", err) ··· 73 } 74 75 func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 76 - user := s.auth.GetUser(r) 77 f, err := s.fullyResolvedRepo(r) 78 if err != nil { 79 log.Println("failed to get repo and knot", err) ··· 143 } 144 } 145 146 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 147 if err != nil { 148 log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 149 return types.MergeCheckResponse{ ··· 215 repoName = f.RepoName 216 } 217 218 - us, err := NewUnsignedClient(knot, s.config.Dev) 219 if err != nil { 220 log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 221 return pages.Unknown ··· 250 } 251 252 func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 253 - user := s.auth.GetUser(r) 254 f, err := s.fullyResolvedRepo(r) 255 if err != nil { 256 log.Println("failed to get repo and knot", err) ··· 298 } 299 300 func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 301 - user := s.auth.GetUser(r) 302 303 f, err := s.fullyResolvedRepo(r) 304 if err != nil { ··· 355 interdiff := patchutil.Interdiff(previousPatch, currentPatch) 356 357 s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 358 - LoggedInUser: s.auth.GetUser(r), 359 RepoInfo: f.RepoInfo(s, user), 360 Pull: pull, 361 Round: roundIdInt, ··· 397 } 398 399 func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 400 - user := s.auth.GetUser(r) 401 params := r.URL.Query() 402 403 state := db.PullOpen ··· 451 } 452 453 s.pages.RepoPulls(w, pages.RepoPullsParams{ 454 - LoggedInUser: s.auth.GetUser(r), 455 RepoInfo: f.RepoInfo(s, user), 456 Pulls: pulls, 457 DidHandleMap: didHandleMap, ··· 461 } 462 463 func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 464 - user := s.auth.GetUser(r) 465 f, err := s.fullyResolvedRepo(r) 466 if err != nil { 467 log.Println("failed to get repo and knot", err) ··· 519 } 520 521 atUri := f.RepoAt.String() 522 - client, _ := s.auth.AuthorizedClient(r) 523 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 524 Collection: tangled.RepoPullCommentNSID, 525 Repo: user.Did, 526 Rkey: appview.TID(), ··· 568 } 569 570 func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 571 - user := s.auth.GetUser(r) 572 f, err := s.fullyResolvedRepo(r) 573 if err != nil { 574 log.Println("failed to get repo and knot", err) ··· 577 578 switch r.Method { 579 case http.MethodGet: 580 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 581 if err != nil { 582 log.Printf("failed to create unsigned client for %s", f.Knot) 583 s.pages.Error503(w) ··· 646 return 647 } 648 649 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 650 if err != nil { 651 log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 652 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") ··· 689 } 690 } 691 692 - func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) { 693 pullSource := &db.PullSource{ 694 Branch: sourceBranch, 695 } ··· 698 } 699 700 // Generate a patch using /compare 701 - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 702 if err != nil { 703 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 704 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 723 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 724 } 725 726 - func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) { 727 if !patchutil.IsPatchValid(patch) { 728 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 729 return ··· 732 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil) 733 } 734 735 - func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 736 fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 737 if errors.Is(err, sql.ErrNoRows) { 738 s.pages.Notice(w, "pull", "No such fork.") ··· 750 return 751 } 752 753 - sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) 754 if err != nil { 755 log.Println("failed to create signed client:", err) 756 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 757 return 758 } 759 760 - us, err := NewUnsignedClient(fork.Knot, s.config.Dev) 761 if err != nil { 762 log.Println("failed to create unsigned client:", err) 763 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 816 w http.ResponseWriter, 817 r *http.Request, 818 f *FullyResolvedRepo, 819 - user *auth.User, 820 title, body, targetBranch string, 821 patch string, 822 sourceRev string, ··· 870 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 871 return 872 } 873 - client, _ := s.auth.AuthorizedClient(r) 874 - pullId, err := db.NextPullId(s.db, f.RepoAt) 875 if err != nil { 876 log.Println("failed to get pull id", err) 877 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 878 return 879 } 880 881 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 882 Collection: tangled.RepoPullNSID, 883 Repo: user.Did, 884 Rkey: rkey, ··· 893 }, 894 }, 895 }) 896 897 - if err != nil { 898 log.Println("failed to create pull request", err) 899 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 900 return ··· 929 } 930 931 func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 932 - user := s.auth.GetUser(r) 933 f, err := s.fullyResolvedRepo(r) 934 if err != nil { 935 log.Println("failed to get repo and knot", err) ··· 942 } 943 944 func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 945 - user := s.auth.GetUser(r) 946 f, err := s.fullyResolvedRepo(r) 947 if err != nil { 948 log.Println("failed to get repo and knot", err) 949 return 950 } 951 952 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 953 if err != nil { 954 log.Printf("failed to create unsigned client for %s", f.Knot) 955 s.pages.Error503(w) ··· 982 } 983 984 func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 985 - user := s.auth.GetUser(r) 986 f, err := s.fullyResolvedRepo(r) 987 if err != nil { 988 log.Println("failed to get repo and knot", err) ··· 1002 } 1003 1004 func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1005 - user := s.auth.GetUser(r) 1006 1007 f, err := s.fullyResolvedRepo(r) 1008 if err != nil { ··· 1019 return 1020 } 1021 1022 - sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev) 1023 if err != nil { 1024 log.Printf("failed to create unsigned client for %s", repo.Knot) 1025 s.pages.Error503(w) ··· 1046 return 1047 } 1048 1049 - targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1050 if err != nil { 1051 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1052 s.pages.Error503(w) ··· 1081 } 1082 1083 func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1084 - user := s.auth.GetUser(r) 1085 f, err := s.fullyResolvedRepo(r) 1086 if err != nil { 1087 log.Println("failed to get repo and knot", err) ··· 1117 } 1118 1119 func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1120 - user := s.auth.GetUser(r) 1121 1122 pull, ok := r.Context().Value("pull").(*db.Pull) 1123 if !ok { ··· 1159 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") 1160 return 1161 } 1162 - client, _ := s.auth.AuthorizedClient(r) 1163 1164 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1165 if err != nil { 1166 // failed to get record 1167 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1168 return 1169 } 1170 1171 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1172 Collection: tangled.RepoPullNSID, 1173 Repo: user.Did, 1174 Rkey: pull.Rkey, ··· 1200 } 1201 1202 func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1203 - user := s.auth.GetUser(r) 1204 1205 pull, ok := r.Context().Value("pull").(*db.Pull) 1206 if !ok { ··· 1227 return 1228 } 1229 1230 - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1231 if err != nil { 1232 log.Printf("failed to create client for %s: %s", f.Knot, err) 1233 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1268 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1269 return 1270 } 1271 - client, _ := s.auth.AuthorizedClient(r) 1272 1273 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1274 if err != nil { 1275 // failed to get record 1276 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1280 recordPullSource := &tangled.RepoPull_Source{ 1281 Branch: pull.PullSource.Branch, 1282 } 1283 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1284 Collection: tangled.RepoPullNSID, 1285 Repo: user.Did, 1286 Rkey: pull.Rkey, ··· 1313 } 1314 1315 func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1316 - user := s.auth.GetUser(r) 1317 1318 pull, ok := r.Context().Value("pull").(*db.Pull) 1319 if !ok { ··· 1342 } 1343 1344 // extract patch by performing compare 1345 - ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev) 1346 if err != nil { 1347 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1348 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1357 } 1358 1359 // update the hidden tracking branch to latest 1360 - signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev) 1361 if err != nil { 1362 log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1363 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1406 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1407 return 1408 } 1409 - client, _ := s.auth.AuthorizedClient(r) 1410 1411 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1412 if err != nil { 1413 // failed to get record 1414 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1420 Branch: pull.PullSource.Branch, 1421 Repo: &repoAt, 1422 } 1423 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1424 Collection: tangled.RepoPullNSID, 1425 Repo: user.Did, 1426 Rkey: pull.Rkey, ··· 1503 log.Printf("failed to get primary email: %s", err) 1504 } 1505 1506 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1507 if err != nil { 1508 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1509 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1533 } 1534 1535 func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1536 - user := s.auth.GetUser(r) 1537 1538 f, err := s.fullyResolvedRepo(r) 1539 if err != nil { ··· 1587 } 1588 1589 func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1590 - user := s.auth.GetUser(r) 1591 1592 f, err := s.fullyResolvedRepo(r) 1593 if err != nil {
··· 13 14 "tangled.sh/tangled.sh/core/api/tangled" 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/oauth" 19 "tangled.sh/tangled.sh/core/appview/pages" 20 "tangled.sh/tangled.sh/core/patchutil" 21 "tangled.sh/tangled.sh/core/types" ··· 30 func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { 31 switch r.Method { 32 case http.MethodGet: 33 + user := s.oauth.GetUser(r) 34 f, err := s.fullyResolvedRepo(r) 35 if err != nil { 36 log.Println("failed to get repo and knot", err) ··· 74 } 75 76 func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 77 + user := s.oauth.GetUser(r) 78 f, err := s.fullyResolvedRepo(r) 79 if err != nil { 80 log.Println("failed to get repo and knot", err) ··· 144 } 145 } 146 147 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 148 if err != nil { 149 log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 150 return types.MergeCheckResponse{ ··· 216 repoName = f.RepoName 217 } 218 219 + us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 220 if err != nil { 221 log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 222 return pages.Unknown ··· 251 } 252 253 func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 254 + user := s.oauth.GetUser(r) 255 f, err := s.fullyResolvedRepo(r) 256 if err != nil { 257 log.Println("failed to get repo and knot", err) ··· 299 } 300 301 func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 302 + user := s.oauth.GetUser(r) 303 304 f, err := s.fullyResolvedRepo(r) 305 if err != nil { ··· 356 interdiff := patchutil.Interdiff(previousPatch, currentPatch) 357 358 s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 359 + LoggedInUser: s.oauth.GetUser(r), 360 RepoInfo: f.RepoInfo(s, user), 361 Pull: pull, 362 Round: roundIdInt, ··· 398 } 399 400 func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 401 + user := s.oauth.GetUser(r) 402 params := r.URL.Query() 403 404 state := db.PullOpen ··· 452 } 453 454 s.pages.RepoPulls(w, pages.RepoPullsParams{ 455 + LoggedInUser: s.oauth.GetUser(r), 456 RepoInfo: f.RepoInfo(s, user), 457 Pulls: pulls, 458 DidHandleMap: didHandleMap, ··· 462 } 463 464 func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 465 + user := s.oauth.GetUser(r) 466 f, err := s.fullyResolvedRepo(r) 467 if err != nil { 468 log.Println("failed to get repo and knot", err) ··· 520 } 521 522 atUri := f.RepoAt.String() 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{ 530 Collection: tangled.RepoPullCommentNSID, 531 Repo: user.Did, 532 Rkey: appview.TID(), ··· 574 } 575 576 func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 577 + user := s.oauth.GetUser(r) 578 f, err := s.fullyResolvedRepo(r) 579 if err != nil { 580 log.Println("failed to get repo and knot", err) ··· 583 584 switch r.Method { 585 case http.MethodGet: 586 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 587 if err != nil { 588 log.Printf("failed to create unsigned client for %s", f.Knot) 589 s.pages.Error503(w) ··· 652 return 653 } 654 655 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 656 if err != nil { 657 log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 658 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") ··· 695 } 696 } 697 698 + func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, sourceBranch string) { 699 pullSource := &db.PullSource{ 700 Branch: sourceBranch, 701 } ··· 704 } 705 706 // Generate a patch using /compare 707 + ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 708 if err != nil { 709 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 710 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 729 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 730 } 731 732 + func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, patch string) { 733 if !patchutil.IsPatchValid(patch) { 734 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 735 return ··· 738 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil) 739 } 740 741 + func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 742 fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 743 if errors.Is(err, sql.ErrNoRows) { 744 s.pages.Notice(w, "pull", "No such fork.") ··· 756 return 757 } 758 759 + sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev) 760 if err != nil { 761 log.Println("failed to create signed client:", err) 762 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 763 return 764 } 765 766 + us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev) 767 if err != nil { 768 log.Println("failed to create unsigned client:", err) 769 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 822 w http.ResponseWriter, 823 r *http.Request, 824 f *FullyResolvedRepo, 825 + user *oauth.User, 826 title, body, targetBranch string, 827 patch string, 828 sourceRev string, ··· 876 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 877 return 878 } 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) 886 if err != nil { 887 log.Println("failed to get pull id", err) 888 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 889 return 890 } 891 892 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 893 Collection: tangled.RepoPullNSID, 894 Repo: user.Did, 895 Rkey: rkey, ··· 904 }, 905 }, 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 + } 912 913 + if err = tx.Commit(); err != nil { 914 log.Println("failed to create pull request", err) 915 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 916 return ··· 945 } 946 947 func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 948 + user := s.oauth.GetUser(r) 949 f, err := s.fullyResolvedRepo(r) 950 if err != nil { 951 log.Println("failed to get repo and knot", err) ··· 958 } 959 960 func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 961 + user := s.oauth.GetUser(r) 962 f, err := s.fullyResolvedRepo(r) 963 if err != nil { 964 log.Println("failed to get repo and knot", err) 965 return 966 } 967 968 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 969 if err != nil { 970 log.Printf("failed to create unsigned client for %s", f.Knot) 971 s.pages.Error503(w) ··· 998 } 999 1000 func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1001 + user := s.oauth.GetUser(r) 1002 f, err := s.fullyResolvedRepo(r) 1003 if err != nil { 1004 log.Println("failed to get repo and knot", err) ··· 1018 } 1019 1020 func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1021 + user := s.oauth.GetUser(r) 1022 1023 f, err := s.fullyResolvedRepo(r) 1024 if err != nil { ··· 1035 return 1036 } 1037 1038 + sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev) 1039 if err != nil { 1040 log.Printf("failed to create unsigned client for %s", repo.Knot) 1041 s.pages.Error503(w) ··· 1062 return 1063 } 1064 1065 + targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1066 if err != nil { 1067 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1068 s.pages.Error503(w) ··· 1097 } 1098 1099 func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1100 + user := s.oauth.GetUser(r) 1101 f, err := s.fullyResolvedRepo(r) 1102 if err != nil { 1103 log.Println("failed to get repo and knot", err) ··· 1133 } 1134 1135 func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1136 + user := s.oauth.GetUser(r) 1137 1138 pull, ok := r.Context().Value("pull").(*db.Pull) 1139 if !ok { ··· 1175 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") 1176 return 1177 } 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 + } 1184 1185 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1186 if err != nil { 1187 // failed to get record 1188 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1189 return 1190 } 1191 1192 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1193 Collection: tangled.RepoPullNSID, 1194 Repo: user.Did, 1195 Rkey: pull.Rkey, ··· 1221 } 1222 1223 func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1224 + user := s.oauth.GetUser(r) 1225 1226 pull, ok := r.Context().Value("pull").(*db.Pull) 1227 if !ok { ··· 1248 return 1249 } 1250 1251 + ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1252 if err != nil { 1253 log.Printf("failed to create client for %s: %s", f.Knot, err) 1254 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1289 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1290 return 1291 } 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 + } 1298 1299 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1300 if err != nil { 1301 // failed to get record 1302 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1306 recordPullSource := &tangled.RepoPull_Source{ 1307 Branch: pull.PullSource.Branch, 1308 } 1309 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1310 Collection: tangled.RepoPullNSID, 1311 Repo: user.Did, 1312 Rkey: pull.Rkey, ··· 1339 } 1340 1341 func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1342 + user := s.oauth.GetUser(r) 1343 1344 pull, ok := r.Context().Value("pull").(*db.Pull) 1345 if !ok { ··· 1368 } 1369 1370 // extract patch by performing compare 1371 + ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev) 1372 if err != nil { 1373 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1374 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1383 } 1384 1385 // update the hidden tracking branch to latest 1386 + signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev) 1387 if err != nil { 1388 log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1389 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1432 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1433 return 1434 } 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 + } 1441 1442 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1443 if err != nil { 1444 // failed to get record 1445 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1451 Branch: pull.PullSource.Branch, 1452 Repo: &repoAt, 1453 } 1454 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1455 Collection: tangled.RepoPullNSID, 1456 Repo: user.Did, 1457 Rkey: pull.Rkey, ··· 1534 log.Printf("failed to get primary email: %s", err) 1535 } 1536 1537 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 1538 if err != nil { 1539 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1540 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1564 } 1565 1566 func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1567 + user := s.oauth.GetUser(r) 1568 1569 f, err := s.fullyResolvedRepo(r) 1570 if err != nil { ··· 1618 } 1619 1620 func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1621 + user := s.oauth.GetUser(r) 1622 1623 f, err := s.fullyResolvedRepo(r) 1624 if err != nil {
+99 -60
appview/state/repo.go
··· 18 19 "tangled.sh/tangled.sh/core/api/tangled" 20 "tangled.sh/tangled.sh/core/appview" 21 - "tangled.sh/tangled.sh/core/appview/auth" 22 "tangled.sh/tangled.sh/core/appview/db" 23 "tangled.sh/tangled.sh/core/appview/pages" 24 "tangled.sh/tangled.sh/core/appview/pages/markup" 25 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" ··· 45 return 46 } 47 48 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 49 if err != nil { 50 log.Printf("failed to create unsigned client for %s", f.Knot) 51 s.pages.Error503(w) ··· 119 120 emails := uniqueEmails(commitsTrunc) 121 122 - user := s.auth.GetUser(r) 123 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 124 LoggedInUser: user, 125 RepoInfo: f.RepoInfo(s, user), ··· 150 151 ref := chi.URLParam(r, "ref") 152 153 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 154 if err != nil { 155 log.Println("failed to create unsigned client", err) 156 return ··· 190 tagMap[hash] = append(tagMap[hash], tag.Name) 191 } 192 193 - user := s.auth.GetUser(r) 194 s.pages.RepoLog(w, pages.RepoLogParams{ 195 LoggedInUser: user, 196 TagMap: tagMap, ··· 209 return 210 } 211 212 - user := s.auth.GetUser(r) 213 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 214 RepoInfo: f.RepoInfo(s, user), 215 }) ··· 232 return 233 } 234 235 - user := s.auth.GetUser(r) 236 237 switch r.Method { 238 case http.MethodGet: ··· 241 }) 242 return 243 case http.MethodPut: 244 - user := s.auth.GetUser(r) 245 newDescription := r.FormValue("description") 246 - client, _ := s.auth.AuthorizedClient(r) 247 248 // optimistic update 249 err = db.UpdateDescription(s.db, string(repoAt), newDescription) ··· 256 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 257 // 258 // 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) 260 if err != nil { 261 // failed to get record 262 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 263 return 264 } 265 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 266 Collection: tangled.RepoNSID, 267 Repo: user.Did, 268 Rkey: rkey, ··· 303 } 304 ref := chi.URLParam(r, "ref") 305 protocol := "http" 306 - if !s.config.Dev { 307 protocol = "https" 308 } 309 ··· 331 return 332 } 333 334 - user := s.auth.GetUser(r) 335 s.pages.RepoCommit(w, pages.RepoCommitParams{ 336 LoggedInUser: user, 337 RepoInfo: f.RepoInfo(s, user), ··· 351 ref := chi.URLParam(r, "ref") 352 treePath := chi.URLParam(r, "*") 353 protocol := "http" 354 - if !s.config.Dev { 355 protocol = "https" 356 } 357 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) ··· 380 return 381 } 382 383 - user := s.auth.GetUser(r) 384 385 var breadcrumbs [][]string 386 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) ··· 411 return 412 } 413 414 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 415 if err != nil { 416 log.Println("failed to create unsigned client", err) 417 return ··· 451 } 452 } 453 454 - user := s.auth.GetUser(r) 455 s.pages.RepoTags(w, pages.RepoTagsParams{ 456 LoggedInUser: user, 457 RepoInfo: f.RepoInfo(s, user), ··· 469 return 470 } 471 472 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 473 if err != nil { 474 log.Println("failed to create unsigned client", err) 475 return ··· 511 return strings.Compare(a.Name, b.Name) * -1 512 }) 513 514 - user := s.auth.GetUser(r) 515 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 516 LoggedInUser: user, 517 RepoInfo: f.RepoInfo(s, user), ··· 530 ref := chi.URLParam(r, "ref") 531 filePath := chi.URLParam(r, "*") 532 protocol := "http" 533 - if !s.config.Dev { 534 protocol = "https" 535 } 536 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 568 showRendered = r.URL.Query().Get("code") != "true" 569 } 570 571 - user := s.auth.GetUser(r) 572 s.pages.RepoBlob(w, pages.RepoBlobParams{ 573 LoggedInUser: user, 574 RepoInfo: f.RepoInfo(s, user), ··· 591 filePath := chi.URLParam(r, "*") 592 593 protocol := "http" 594 - if !s.config.Dev { 595 protocol = "https" 596 } 597 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 652 return 653 } 654 655 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 656 if err != nil { 657 log.Println("failed to create client to ", f.Knot) 658 return ··· 714 } 715 716 func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 717 - user := s.auth.GetUser(r) 718 719 f, err := s.fullyResolvedRepo(r) 720 if err != nil { ··· 723 } 724 725 // remove record from pds 726 - xrpcClient, _ := s.auth.AuthorizedClient(r) 727 repoRkey := f.RepoAt.RecordKey().String() 728 - _, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{ 729 Collection: tangled.RepoNSID, 730 Repo: user.Did, 731 Rkey: repoRkey, ··· 743 return 744 } 745 746 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 747 if err != nil { 748 log.Println("failed to create client to ", f.Knot) 749 return ··· 838 return 839 } 840 841 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 842 if err != nil { 843 log.Println("failed to create client to ", f.Knot) 844 return ··· 868 switch r.Method { 869 case http.MethodGet: 870 // for now, this is just pubkeys 871 - user := s.auth.GetUser(r) 872 repoCollaborators, err := f.Collaborators(r.Context(), s) 873 if err != nil { 874 log.Println("failed to get collaborators", err) ··· 884 885 var branchNames []string 886 var defaultBranch string 887 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 888 if err != nil { 889 log.Println("failed to create unsigned client", err) 890 } else { ··· 1008 return collaborators, nil 1009 } 1010 1011 - func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo { 1012 isStarred := false 1013 if u != nil { 1014 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) ··· 1051 1052 knot := f.Knot 1053 var disableFork bool 1054 - us, err := NewUnsignedClient(knot, s.config.Dev) 1055 if err != nil { 1056 log.Printf("failed to create unsigned client for %s: %v", knot, err) 1057 } else { ··· 1105 } 1106 1107 func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1108 - user := s.auth.GetUser(r) 1109 f, err := s.fullyResolvedRepo(r) 1110 if err != nil { 1111 log.Println("failed to get repo and knot", err) ··· 1159 } 1160 1161 func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1162 - user := s.auth.GetUser(r) 1163 f, err := s.fullyResolvedRepo(r) 1164 if err != nil { 1165 log.Println("failed to get repo and knot", err) ··· 1195 1196 closed := tangled.RepoIssueStateClosed 1197 1198 - client, _ := s.auth.AuthorizedClient(r) 1199 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1200 Collection: tangled.RepoIssueStateNSID, 1201 Repo: user.Did, 1202 Rkey: appview.TID(), ··· 1214 return 1215 } 1216 1217 - err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1218 if err != nil { 1219 log.Println("failed to close issue", err) 1220 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 1231 } 1232 1233 func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1234 - user := s.auth.GetUser(r) 1235 f, err := s.fullyResolvedRepo(r) 1236 if err != nil { 1237 log.Println("failed to get repo and knot", err) ··· 1279 } 1280 1281 func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1282 - user := s.auth.GetUser(r) 1283 f, err := s.fullyResolvedRepo(r) 1284 if err != nil { 1285 log.Println("failed to get repo and knot", err) ··· 1330 } 1331 1332 atUri := f.RepoAt.String() 1333 - client, _ := s.auth.AuthorizedClient(r) 1334 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1335 Collection: tangled.RepoIssueCommentNSID, 1336 Repo: user.Did, 1337 Rkey: rkey, ··· 1358 } 1359 1360 func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1361 - user := s.auth.GetUser(r) 1362 f, err := s.fullyResolvedRepo(r) 1363 if err != nil { 1364 log.Println("failed to get repo and knot", err) ··· 1417 } 1418 1419 func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1420 - user := s.auth.GetUser(r) 1421 f, err := s.fullyResolvedRepo(r) 1422 if err != nil { 1423 log.Println("failed to get repo and knot", err) ··· 1469 case http.MethodPost: 1470 // extract form value 1471 newBody := r.FormValue("body") 1472 - client, _ := s.auth.AuthorizedClient(r) 1473 rkey := comment.Rkey 1474 1475 // optimistic update ··· 1484 // rkey is optional, it was introduced later 1485 if comment.Rkey != "" { 1486 // update the record on pds 1487 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey) 1488 if err != nil { 1489 // failed to get record 1490 log.Println(err, rkey) ··· 1499 createdAt := record["createdAt"].(string) 1500 commentIdInt64 := int64(commentIdInt) 1501 1502 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1503 Collection: tangled.RepoIssueCommentNSID, 1504 Repo: user.Did, 1505 Rkey: rkey, ··· 1542 } 1543 1544 func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1545 - user := s.auth.GetUser(r) 1546 f, err := s.fullyResolvedRepo(r) 1547 if err != nil { 1548 log.Println("failed to get repo and knot", err) ··· 1599 1600 // delete from pds 1601 if comment.Rkey != "" { 1602 - client, _ := s.auth.AuthorizedClient(r) 1603 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 1604 Collection: tangled.GraphFollowNSID, 1605 Repo: user.Did, 1606 Rkey: comment.Rkey, ··· 1647 page = pagination.FirstPage() 1648 } 1649 1650 - user := s.auth.GetUser(r) 1651 f, err := s.fullyResolvedRepo(r) 1652 if err != nil { 1653 log.Println("failed to get repo and knot", err) ··· 1676 } 1677 1678 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1679 - LoggedInUser: s.auth.GetUser(r), 1680 RepoInfo: f.RepoInfo(s, user), 1681 Issues: issues, 1682 DidHandleMap: didHandleMap, ··· 1687 } 1688 1689 func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1690 - user := s.auth.GetUser(r) 1691 1692 f, err := s.fullyResolvedRepo(r) 1693 if err != nil { ··· 1735 return 1736 } 1737 1738 - client, _ := s.auth.AuthorizedClient(r) 1739 atUri := f.RepoAt.String() 1740 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1741 Collection: tangled.RepoIssueNSID, 1742 Repo: user.Did, 1743 Rkey: appview.TID(), ··· 1770 } 1771 1772 func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { 1773 - user := s.auth.GetUser(r) 1774 f, err := s.fullyResolvedRepo(r) 1775 if err != nil { 1776 log.Printf("failed to resolve source repo: %v", err) ··· 1779 1780 switch r.Method { 1781 case http.MethodGet: 1782 - user := s.auth.GetUser(r) 1783 knots, err := s.enforcer.GetDomainsForUser(user.Did) 1784 if err != nil { 1785 s.pages.Notice(w, "repo", "Invalid user account.") ··· 1829 return 1830 } 1831 1832 - client, err := NewSignedClient(knot, secret, s.config.Dev) 1833 if err != nil { 1834 s.pages.Notice(w, "repo", "Failed to reach knot server.") 1835 return 1836 } 1837 1838 var uri string 1839 - if s.config.Dev { 1840 uri = "http" 1841 } else { 1842 uri = "https" ··· 1883 // continue 1884 } 1885 1886 - xrpcClient, _ := s.auth.AuthorizedClient(r) 1887 1888 createdAt := time.Now().Format(time.RFC3339) 1889 - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 1890 Collection: tangled.RepoNSID, 1891 Repo: user.Did, 1892 Rkey: rkey,
··· 18 19 "tangled.sh/tangled.sh/core/api/tangled" 20 "tangled.sh/tangled.sh/core/appview" 21 "tangled.sh/tangled.sh/core/appview/db" 22 + "tangled.sh/tangled.sh/core/appview/knotclient" 23 + "tangled.sh/tangled.sh/core/appview/oauth" 24 "tangled.sh/tangled.sh/core/appview/pages" 25 "tangled.sh/tangled.sh/core/appview/pages/markup" 26 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" ··· 46 return 47 } 48 49 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 50 if err != nil { 51 log.Printf("failed to create unsigned client for %s", f.Knot) 52 s.pages.Error503(w) ··· 120 121 emails := uniqueEmails(commitsTrunc) 122 123 + user := s.oauth.GetUser(r) 124 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 125 LoggedInUser: user, 126 RepoInfo: f.RepoInfo(s, user), ··· 151 152 ref := chi.URLParam(r, "ref") 153 154 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 155 if err != nil { 156 log.Println("failed to create unsigned client", err) 157 return ··· 191 tagMap[hash] = append(tagMap[hash], tag.Name) 192 } 193 194 + user := s.oauth.GetUser(r) 195 s.pages.RepoLog(w, pages.RepoLogParams{ 196 LoggedInUser: user, 197 TagMap: tagMap, ··· 210 return 211 } 212 213 + user := s.oauth.GetUser(r) 214 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 215 RepoInfo: f.RepoInfo(s, user), 216 }) ··· 233 return 234 } 235 236 + user := s.oauth.GetUser(r) 237 238 switch r.Method { 239 case http.MethodGet: ··· 242 }) 243 return 244 case http.MethodPut: 245 + user := s.oauth.GetUser(r) 246 newDescription := r.FormValue("description") 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 + } 253 254 // optimistic update 255 err = db.UpdateDescription(s.db, string(repoAt), newDescription) ··· 262 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 263 // 264 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests 265 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 266 if err != nil { 267 // failed to get record 268 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 269 return 270 } 271 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 272 Collection: tangled.RepoNSID, 273 Repo: user.Did, 274 Rkey: rkey, ··· 309 } 310 ref := chi.URLParam(r, "ref") 311 protocol := "http" 312 + if !s.config.Core.Dev { 313 protocol = "https" 314 } 315 ··· 337 return 338 } 339 340 + user := s.oauth.GetUser(r) 341 s.pages.RepoCommit(w, pages.RepoCommitParams{ 342 LoggedInUser: user, 343 RepoInfo: f.RepoInfo(s, user), ··· 357 ref := chi.URLParam(r, "ref") 358 treePath := chi.URLParam(r, "*") 359 protocol := "http" 360 + if !s.config.Core.Dev { 361 protocol = "https" 362 } 363 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) ··· 386 return 387 } 388 389 + user := s.oauth.GetUser(r) 390 391 var breadcrumbs [][]string 392 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) ··· 417 return 418 } 419 420 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 421 if err != nil { 422 log.Println("failed to create unsigned client", err) 423 return ··· 457 } 458 } 459 460 + user := s.oauth.GetUser(r) 461 s.pages.RepoTags(w, pages.RepoTagsParams{ 462 LoggedInUser: user, 463 RepoInfo: f.RepoInfo(s, user), ··· 475 return 476 } 477 478 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 479 if err != nil { 480 log.Println("failed to create unsigned client", err) 481 return ··· 517 return strings.Compare(a.Name, b.Name) * -1 518 }) 519 520 + user := s.oauth.GetUser(r) 521 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 522 LoggedInUser: user, 523 RepoInfo: f.RepoInfo(s, user), ··· 536 ref := chi.URLParam(r, "ref") 537 filePath := chi.URLParam(r, "*") 538 protocol := "http" 539 + if !s.config.Core.Dev { 540 protocol = "https" 541 } 542 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 574 showRendered = r.URL.Query().Get("code") != "true" 575 } 576 577 + user := s.oauth.GetUser(r) 578 s.pages.RepoBlob(w, pages.RepoBlobParams{ 579 LoggedInUser: user, 580 RepoInfo: f.RepoInfo(s, user), ··· 597 filePath := chi.URLParam(r, "*") 598 599 protocol := "http" 600 + if !s.config.Core.Dev { 601 protocol = "https" 602 } 603 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 658 return 659 } 660 661 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 662 if err != nil { 663 log.Println("failed to create client to ", f.Knot) 664 return ··· 720 } 721 722 func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 723 + user := s.oauth.GetUser(r) 724 725 f, err := s.fullyResolvedRepo(r) 726 if err != nil { ··· 729 } 730 731 // remove record from pds 732 + xrpcClient, err := s.oauth.AuthorizedClient(r) 733 + if err != nil { 734 + log.Println("failed to get authorized client", err) 735 + return 736 + } 737 repoRkey := f.RepoAt.RecordKey().String() 738 + _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 739 Collection: tangled.RepoNSID, 740 Repo: user.Did, 741 Rkey: repoRkey, ··· 753 return 754 } 755 756 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 757 if err != nil { 758 log.Println("failed to create client to ", f.Knot) 759 return ··· 848 return 849 } 850 851 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 852 if err != nil { 853 log.Println("failed to create client to ", f.Knot) 854 return ··· 878 switch r.Method { 879 case http.MethodGet: 880 // for now, this is just pubkeys 881 + user := s.oauth.GetUser(r) 882 repoCollaborators, err := f.Collaborators(r.Context(), s) 883 if err != nil { 884 log.Println("failed to get collaborators", err) ··· 894 895 var branchNames []string 896 var defaultBranch string 897 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 898 if err != nil { 899 log.Println("failed to create unsigned client", err) 900 } else { ··· 1018 return collaborators, nil 1019 } 1020 1021 + func (f *FullyResolvedRepo) RepoInfo(s *State, u *oauth.User) repoinfo.RepoInfo { 1022 isStarred := false 1023 if u != nil { 1024 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) ··· 1061 1062 knot := f.Knot 1063 var disableFork bool 1064 + us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 1065 if err != nil { 1066 log.Printf("failed to create unsigned client for %s: %v", knot, err) 1067 } else { ··· 1115 } 1116 1117 func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1118 + user := s.oauth.GetUser(r) 1119 f, err := s.fullyResolvedRepo(r) 1120 if err != nil { 1121 log.Println("failed to get repo and knot", err) ··· 1169 } 1170 1171 func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1172 + user := s.oauth.GetUser(r) 1173 f, err := s.fullyResolvedRepo(r) 1174 if err != nil { 1175 log.Println("failed to get repo and knot", err) ··· 1205 1206 closed := tangled.RepoIssueStateClosed 1207 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{ 1214 Collection: tangled.RepoIssueStateNSID, 1215 Repo: user.Did, 1216 Rkey: appview.TID(), ··· 1228 return 1229 } 1230 1231 + err = db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1232 if err != nil { 1233 log.Println("failed to close issue", err) 1234 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 1245 } 1246 1247 func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1248 + user := s.oauth.GetUser(r) 1249 f, err := s.fullyResolvedRepo(r) 1250 if err != nil { 1251 log.Println("failed to get repo and knot", err) ··· 1293 } 1294 1295 func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1296 + user := s.oauth.GetUser(r) 1297 f, err := s.fullyResolvedRepo(r) 1298 if err != nil { 1299 log.Println("failed to get repo and knot", err) ··· 1344 } 1345 1346 atUri := f.RepoAt.String() 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{ 1354 Collection: tangled.RepoIssueCommentNSID, 1355 Repo: user.Did, 1356 Rkey: rkey, ··· 1377 } 1378 1379 func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1380 + user := s.oauth.GetUser(r) 1381 f, err := s.fullyResolvedRepo(r) 1382 if err != nil { 1383 log.Println("failed to get repo and knot", err) ··· 1436 } 1437 1438 func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1439 + user := s.oauth.GetUser(r) 1440 f, err := s.fullyResolvedRepo(r) 1441 if err != nil { 1442 log.Println("failed to get repo and knot", err) ··· 1488 case http.MethodPost: 1489 // extract form value 1490 newBody := r.FormValue("body") 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 + } 1497 rkey := comment.Rkey 1498 1499 // optimistic update ··· 1508 // rkey is optional, it was introduced later 1509 if comment.Rkey != "" { 1510 // update the record on pds 1511 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey) 1512 if err != nil { 1513 // failed to get record 1514 log.Println(err, rkey) ··· 1523 createdAt := record["createdAt"].(string) 1524 commentIdInt64 := int64(commentIdInt) 1525 1526 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1527 Collection: tangled.RepoIssueCommentNSID, 1528 Repo: user.Did, 1529 Rkey: rkey, ··· 1566 } 1567 1568 func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1569 + user := s.oauth.GetUser(r) 1570 f, err := s.fullyResolvedRepo(r) 1571 if err != nil { 1572 log.Println("failed to get repo and knot", err) ··· 1623 1624 // delete from pds 1625 if comment.Rkey != "" { 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{ 1633 Collection: tangled.GraphFollowNSID, 1634 Repo: user.Did, 1635 Rkey: comment.Rkey, ··· 1676 page = pagination.FirstPage() 1677 } 1678 1679 + user := s.oauth.GetUser(r) 1680 f, err := s.fullyResolvedRepo(r) 1681 if err != nil { 1682 log.Println("failed to get repo and knot", err) ··· 1705 } 1706 1707 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1708 + LoggedInUser: s.oauth.GetUser(r), 1709 RepoInfo: f.RepoInfo(s, user), 1710 Issues: issues, 1711 DidHandleMap: didHandleMap, ··· 1716 } 1717 1718 func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1719 + user := s.oauth.GetUser(r) 1720 1721 f, err := s.fullyResolvedRepo(r) 1722 if err != nil { ··· 1764 return 1765 } 1766 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 + } 1773 atUri := f.RepoAt.String() 1774 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1775 Collection: tangled.RepoIssueNSID, 1776 Repo: user.Did, 1777 Rkey: appview.TID(), ··· 1804 } 1805 1806 func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { 1807 + user := s.oauth.GetUser(r) 1808 f, err := s.fullyResolvedRepo(r) 1809 if err != nil { 1810 log.Printf("failed to resolve source repo: %v", err) ··· 1813 1814 switch r.Method { 1815 case http.MethodGet: 1816 + user := s.oauth.GetUser(r) 1817 knots, err := s.enforcer.GetDomainsForUser(user.Did) 1818 if err != nil { 1819 s.pages.Notice(w, "repo", "Invalid user account.") ··· 1863 return 1864 } 1865 1866 + client, err := knotclient.NewSignedClient(knot, secret, s.config.Core.Dev) 1867 if err != nil { 1868 s.pages.Notice(w, "repo", "Failed to reach knot server.") 1869 return 1870 } 1871 1872 var uri string 1873 + if s.config.Core.Dev { 1874 uri = "http" 1875 } else { 1876 uri = "https" ··· 1917 // continue 1918 } 1919 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 + } 1926 1927 createdAt := time.Now().Format(time.RFC3339) 1928 + atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1929 Collection: tangled.RepoNSID, 1930 Repo: user.Did, 1931 Rkey: rkey,
+4 -3
appview/state/repo_util.go
··· 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "github.com/go-chi/chi/v5" 14 "github.com/go-git/go-git/v5/plumbing/object" 15 - "tangled.sh/tangled.sh/core/appview/auth" 16 "tangled.sh/tangled.sh/core/appview/db" 17 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 18 ) 19 ··· 45 ref := chi.URLParam(r, "ref") 46 47 if ref == "" { 48 - us, err := NewUnsignedClient(knot, s.config.Dev) 49 if err != nil { 50 return nil, err 51 } ··· 73 }, nil 74 } 75 76 - func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo { 77 if u != nil { 78 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) 79 return repoinfo.RolesInRepo{r}
··· 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "github.com/go-chi/chi/v5" 14 "github.com/go-git/go-git/v5/plumbing/object" 15 "tangled.sh/tangled.sh/core/appview/db" 16 + "tangled.sh/tangled.sh/core/appview/knotclient" 17 + "tangled.sh/tangled.sh/core/appview/oauth" 18 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 19 ) 20 ··· 46 ref := chi.URLParam(r, "ref") 47 48 if ref == "" { 49 + us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 50 if err != nil { 51 return nil, err 52 } ··· 74 }, nil 75 } 76 77 + func RolesInRepo(s *State, u *oauth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo { 78 if u != nil { 79 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) 80 return repoinfo.RolesInRepo{r}
+42 -20
appview/state/router.go
··· 5 "strings" 6 7 "github.com/go-chi/chi/v5" 8 "tangled.sh/tangled.sh/core/appview/middleware" 9 "tangled.sh/tangled.sh/core/appview/settings" 10 "tangled.sh/tangled.sh/core/appview/state/userutil" 11 ) ··· 53 r.Use(StripLeadingAt) 54 55 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 56 - r.Get("/", s.ProfilePage) 57 r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) { 58 r.Get("/", s.RepoIndex) 59 r.Get("/commits/{ref}", s.RepoLog) 60 r.Route("/tree/{ref}", func(r chi.Router) { ··· 66 r.Route("/tags", func(r chi.Router) { 67 r.Get("/", s.RepoTags) 68 r.Route("/{tag}", func(r chi.Router) { 69 - r.Use(middleware.AuthMiddleware(s.auth)) 70 // require auth to download for now 71 r.Get("/download/{file}", s.DownloadArtifact) 72 ··· 89 r.Get("/{issue}", s.RepoSingleIssue) 90 91 r.Group(func(r chi.Router) { 92 - r.Use(middleware.AuthMiddleware(s.auth)) 93 r.Get("/new", s.NewIssue) 94 r.Post("/new", s.NewIssue) 95 r.Post("/{issue}/comment", s.NewIssueComment) ··· 105 }) 106 107 r.Route("/fork", func(r chi.Router) { 108 - r.Use(middleware.AuthMiddleware(s.auth)) 109 r.Get("/", s.ForkRepo) 110 r.Post("/", s.ForkRepo) 111 }) 112 113 r.Route("/pulls", func(r chi.Router) { 114 r.Get("/", s.RepoPulls) 115 - r.With(middleware.AuthMiddleware(s.auth)).Route("/new", func(r chi.Router) { 116 r.Get("/", s.NewPull) 117 r.Get("/patch-upload", s.PatchUploadFragment) 118 r.Post("/validate-patch", s.ValidatePatch) ··· 130 r.Get("/", s.RepoPullPatch) 131 r.Get("/interdiff", s.RepoPullInterdiff) 132 r.Get("/actions", s.PullActions) 133 - r.With(middleware.AuthMiddleware(s.auth)).Route("/comment", func(r chi.Router) { 134 r.Get("/", s.PullComment) 135 r.Post("/", s.PullComment) 136 }) ··· 141 }) 142 143 r.Group(func(r chi.Router) { 144 - r.Use(middleware.AuthMiddleware(s.auth)) 145 r.Route("/resubmit", func(r chi.Router) { 146 r.Get("/", s.ResubmitPull) 147 r.Post("/", s.ResubmitPull) ··· 164 165 // settings routes, needs auth 166 r.Group(func(r chi.Router) { 167 - r.Use(middleware.AuthMiddleware(s.auth)) 168 // repo description can only be edited by owner 169 r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { 170 r.Put("/", s.RepoDescription) ··· 195 196 r.Get("/", s.Timeline) 197 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 - }) 204 205 r.Route("/knots", func(r chi.Router) { 206 - r.Use(middleware.AuthMiddleware(s.auth)) 207 r.Get("/", s.Knots) 208 r.Post("/key", s.RegistrationKey) 209 ··· 221 222 r.Route("/repo", func(r chi.Router) { 223 r.Route("/new", func(r chi.Router) { 224 - r.Use(middleware.AuthMiddleware(s.auth)) 225 r.Get("/", s.NewRepo) 226 r.Post("/", s.NewRepo) 227 }) 228 // r.Post("/import", s.ImportRepo) 229 }) 230 231 - r.With(middleware.AuthMiddleware(s.auth)).Route("/follow", func(r chi.Router) { 232 r.Post("/", s.Follow) 233 r.Delete("/", s.Follow) 234 }) 235 236 - r.With(middleware.AuthMiddleware(s.auth)).Route("/star", func(r chi.Router) { 237 r.Post("/", s.Star) 238 r.Delete("/", s.Star) 239 }) 240 241 - r.Mount("/settings", s.SettingsRouter()) 242 243 r.Get("/keys/{user}", s.Keys) 244 245 r.NotFound(func(w http.ResponseWriter, r *http.Request) { ··· 248 return r 249 } 250 251 func (s *State) SettingsRouter() http.Handler { 252 settings := &settings.Settings{ 253 Db: s.db, 254 - Auth: s.auth, 255 Pages: s.pages, 256 Config: s.config, 257 }
··· 5 "strings" 6 7 "github.com/go-chi/chi/v5" 8 + "github.com/gorilla/sessions" 9 "tangled.sh/tangled.sh/core/appview/middleware" 10 + oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler" 11 "tangled.sh/tangled.sh/core/appview/settings" 12 "tangled.sh/tangled.sh/core/appview/state/userutil" 13 ) ··· 55 r.Use(StripLeadingAt) 56 57 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 58 + r.Get("/", s.Profile) 59 + 60 r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) { 61 + r.Use(GoImport(s)) 62 + 63 r.Get("/", s.RepoIndex) 64 r.Get("/commits/{ref}", s.RepoLog) 65 r.Route("/tree/{ref}", func(r chi.Router) { ··· 71 r.Route("/tags", func(r chi.Router) { 72 r.Get("/", s.RepoTags) 73 r.Route("/{tag}", func(r chi.Router) { 74 + r.Use(middleware.AuthMiddleware(s.oauth)) 75 // require auth to download for now 76 r.Get("/download/{file}", s.DownloadArtifact) 77 ··· 94 r.Get("/{issue}", s.RepoSingleIssue) 95 96 r.Group(func(r chi.Router) { 97 + r.Use(middleware.AuthMiddleware(s.oauth)) 98 r.Get("/new", s.NewIssue) 99 r.Post("/new", s.NewIssue) 100 r.Post("/{issue}/comment", s.NewIssueComment) ··· 110 }) 111 112 r.Route("/fork", func(r chi.Router) { 113 + r.Use(middleware.AuthMiddleware(s.oauth)) 114 r.Get("/", s.ForkRepo) 115 r.Post("/", s.ForkRepo) 116 }) 117 118 r.Route("/pulls", func(r chi.Router) { 119 r.Get("/", s.RepoPulls) 120 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) { 121 r.Get("/", s.NewPull) 122 r.Get("/patch-upload", s.PatchUploadFragment) 123 r.Post("/validate-patch", s.ValidatePatch) ··· 135 r.Get("/", s.RepoPullPatch) 136 r.Get("/interdiff", s.RepoPullInterdiff) 137 r.Get("/actions", s.PullActions) 138 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { 139 r.Get("/", s.PullComment) 140 r.Post("/", s.PullComment) 141 }) ··· 146 }) 147 148 r.Group(func(r chi.Router) { 149 + r.Use(middleware.AuthMiddleware(s.oauth)) 150 r.Route("/resubmit", func(r chi.Router) { 151 r.Get("/", s.ResubmitPull) 152 r.Post("/", s.ResubmitPull) ··· 169 170 // settings routes, needs auth 171 r.Group(func(r chi.Router) { 172 + r.Use(middleware.AuthMiddleware(s.oauth)) 173 // repo description can only be edited by owner 174 r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { 175 r.Put("/", s.RepoDescription) ··· 200 201 r.Get("/", s.Timeline) 202 203 + r.With(middleware.AuthMiddleware(s.oauth)).Post("/logout", s.Logout) 204 205 r.Route("/knots", func(r chi.Router) { 206 + r.Use(middleware.AuthMiddleware(s.oauth)) 207 r.Get("/", s.Knots) 208 r.Post("/key", s.RegistrationKey) 209 ··· 221 222 r.Route("/repo", func(r chi.Router) { 223 r.Route("/new", func(r chi.Router) { 224 + r.Use(middleware.AuthMiddleware(s.oauth)) 225 r.Get("/", s.NewRepo) 226 r.Post("/", s.NewRepo) 227 }) 228 // r.Post("/import", s.ImportRepo) 229 }) 230 231 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) { 232 r.Post("/", s.Follow) 233 r.Delete("/", s.Follow) 234 }) 235 236 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) { 237 r.Post("/", s.Star) 238 r.Delete("/", s.Star) 239 }) 240 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 + }) 248 249 + r.Mount("/settings", s.SettingsRouter()) 250 + r.Mount("/", s.OAuthRouter()) 251 r.Get("/keys/{user}", s.Keys) 252 253 r.NotFound(func(w http.ResponseWriter, r *http.Request) { ··· 256 return r 257 } 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 + 273 func (s *State) SettingsRouter() http.Handler { 274 settings := &settings.Settings{ 275 Db: s.db, 276 + OAuth: s.oauth, 277 Pages: s.pages, 278 Config: s.config, 279 }
-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
··· 15 ) 16 17 func (s *State) Star(w http.ResponseWriter, r *http.Request) { 18 - currentUser := s.auth.GetUser(r) 19 20 subject := r.URL.Query().Get("subject") 21 if subject == "" { ··· 29 return 30 } 31 32 - client, _ := s.auth.AuthorizedClient(r) 33 34 switch r.Method { 35 case http.MethodPost: 36 createdAt := time.Now().Format(time.RFC3339) 37 rkey := appview.TID() 38 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 39 Collection: tangled.FeedStarNSID, 40 Repo: currentUser.Did, 41 Rkey: rkey, ··· 80 return 81 } 82 83 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 84 Collection: tangled.FeedStarNSID, 85 Repo: currentUser.Did, 86 Rkey: star.Rkey, ··· 100 starCount, err := db.GetStarCount(s.db, subjectUri) 101 if err != nil { 102 log.Println("failed to get star count for ", subjectUri) 103 } 104 105 s.pages.RepoActionsFragment(w, pages.RepoActionsFragmentParams{
··· 15 ) 16 17 func (s *State) Star(w http.ResponseWriter, r *http.Request) { 18 + currentUser := s.oauth.GetUser(r) 19 20 subject := r.URL.Query().Get("subject") 21 if subject == "" { ··· 29 return 30 } 31 32 + client, err := s.oauth.AuthorizedClient(r) 33 + if err != nil { 34 + log.Println("failed to authorize client", err) 35 + return 36 + } 37 38 switch r.Method { 39 case http.MethodPost: 40 createdAt := time.Now().Format(time.RFC3339) 41 rkey := appview.TID() 42 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 43 Collection: tangled.FeedStarNSID, 44 Repo: currentUser.Did, 45 Rkey: rkey, ··· 84 return 85 } 86 87 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 88 Collection: tangled.FeedStarNSID, 89 Repo: currentUser.Did, 90 Rkey: star.Rkey, ··· 104 starCount, err := db.GetStarCount(s.db, subjectUri) 105 if err != nil { 106 log.Println("failed to get star count for ", subjectUri) 107 + return 108 } 109 110 s.pages.RepoActionsFragment(w, pages.RepoActionsFragmentParams{
+141 -104
appview/state/state.go
··· 19 "github.com/go-chi/chi/v5" 20 "tangled.sh/tangled.sh/core/api/tangled" 21 "tangled.sh/tangled.sh/core/appview" 22 - "tangled.sh/tangled.sh/core/appview/auth" 23 "tangled.sh/tangled.sh/core/appview/db" 24 "tangled.sh/tangled.sh/core/appview/pages" 25 "tangled.sh/tangled.sh/core/jetstream" 26 "tangled.sh/tangled.sh/core/rbac" ··· 28 29 type State struct { 30 db *db.DB 31 - auth *auth.Auth 32 enforcer *rbac.Enforcer 33 - tidClock *syntax.TIDClock 34 pages *pages.Pages 35 resolver *appview.Resolver 36 jc *jetstream.JetstreamClient ··· 38 } 39 40 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) 47 if err != nil { 48 return nil, err 49 } 50 51 - enforcer, err := rbac.NewEnforcer(config.DbPath) 52 if err != nil { 53 return nil, err 54 } ··· 59 60 resolver := appview.NewResolver() 61 62 wrapper := db.DbWrapper{d} 63 jc, err := jetstream.NewJetstreamClient( 64 - config.JetstreamEndpoint, 65 "appview", 66 - []string{tangled.GraphFollowNSID, tangled.FeedStarNSID, tangled.PublicKeyNSID, tangled.RepoArtifactNSID}, 67 nil, 68 slog.Default(), 69 wrapper, ··· 72 if err != nil { 73 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 74 } 75 - err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper)) 76 if err != nil { 77 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 78 } 79 80 state := &State{ 81 d, 82 - auth, 83 enforcer, 84 clock, 85 pgs, ··· 95 return c.Next().String() 96 } 97 98 - func (s *State) Login(w http.ResponseWriter, r *http.Request) { 99 - ctx := r.Context() 100 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 - } 107 108 - return 109 - case http.MethodPost: 110 - handle := strings.TrimPrefix(r.FormValue("handle"), "@") 111 - appPassword := r.FormValue("app_password") 112 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 - } 119 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} 126 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 - } 132 133 - log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 134 135 - did := resolved.DID.String() 136 - defaultKnot := "knot1.tangled.sh" 137 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 - } 150 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 - } 162 163 - if resp.StatusCode != http.StatusNoContent { 164 - log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 165 - return 166 - } 167 - }() 168 169 - s.pages.HxRedirect(w, "/") 170 - return 171 - } 172 - } 173 174 func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 175 - s.auth.ClearSession(r, w) 176 w.Header().Set("HX-Redirect", "/login") 177 w.WriteHeader(http.StatusSeeOther) 178 } 179 180 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 181 - user := s.auth.GetUser(r) 182 183 timeline, err := db.MakeTimeline(s.db) 184 if err != nil { ··· 229 230 return 231 case http.MethodPost: 232 - session, err := s.auth.Store.Get(r, appview.SessionName) 233 if err != nil || session.IsNew { 234 log.Println("unauthorized attempt to generate registration key") 235 http.Error(w, "Forbidden", http.StatusUnauthorized) ··· 291 292 // create a signed request and check if a node responds to that 293 func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 294 - user := s.auth.GetUser(r) 295 296 domain := chi.URLParam(r, "domain") 297 if domain == "" { ··· 306 return 307 } 308 309 - client, err := NewSignedClient(domain, secret, s.config.Dev) 310 if err != nil { 311 log.Println("failed to create client to ", domain) 312 } ··· 415 return 416 } 417 418 - user := s.auth.GetUser(r) 419 reg, err := db.RegistrationByDomain(s.db, domain) 420 if err != nil { 421 w.Write([]byte("failed to pull up registration info")) ··· 463 // get knots registered by this user 464 func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 465 // for now, this is just pubkeys 466 - user := s.auth.GetUser(r) 467 registrations, err := db.RegistrationsByDid(s.db, user.Did) 468 if err != nil { 469 log.Println(err) ··· 516 log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain) 517 518 // announce this relation into the firehose, store into owners' pds 519 - client, _ := s.auth.AuthorizedClient(r) 520 - currentUser := s.auth.GetUser(r) 521 createdAt := time.Now().Format(time.RFC3339) 522 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 523 Collection: tangled.KnotMemberNSID, 524 Repo: currentUser.Did, 525 Rkey: appview.TID(), ··· 544 return 545 } 546 547 - ksClient, err := NewSignedClient(domain, secret, s.config.Dev) 548 if err != nil { 549 log.Println("failed to create client to ", domain) 550 return ··· 573 func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 574 } 575 576 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 577 switch r.Method { 578 case http.MethodGet: 579 - user := s.auth.GetUser(r) 580 knots, err := s.enforcer.GetDomainsForUser(user.Did) 581 if err != nil { 582 s.pages.Notice(w, "repo", "Invalid user account.") ··· 589 }) 590 591 case http.MethodPost: 592 - user := s.auth.GetUser(r) 593 594 domain := r.FormValue("domain") 595 if domain == "" { ··· 603 return 604 } 605 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 - } 616 } 617 618 defaultBranch := r.FormValue("branch") ··· 640 return 641 } 642 643 - client, err := NewSignedClient(domain, secret, s.config.Dev) 644 if err != nil { 645 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 646 return ··· 655 Description: description, 656 } 657 658 - xrpcClient, _ := s.auth.AuthorizedClient(r) 659 660 createdAt := time.Now().Format(time.RFC3339) 661 - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 662 Collection: tangled.RepoNSID, 663 Repo: user.Did, 664 Rkey: rkey,
··· 19 "github.com/go-chi/chi/v5" 20 "tangled.sh/tangled.sh/core/api/tangled" 21 "tangled.sh/tangled.sh/core/appview" 22 "tangled.sh/tangled.sh/core/appview/db" 23 + "tangled.sh/tangled.sh/core/appview/knotclient" 24 + "tangled.sh/tangled.sh/core/appview/oauth" 25 "tangled.sh/tangled.sh/core/appview/pages" 26 "tangled.sh/tangled.sh/core/jetstream" 27 "tangled.sh/tangled.sh/core/rbac" ··· 29 30 type State struct { 31 db *db.DB 32 + oauth *oauth.OAuth 33 enforcer *rbac.Enforcer 34 + tidClock syntax.TIDClock 35 pages *pages.Pages 36 resolver *appview.Resolver 37 jc *jetstream.JetstreamClient ··· 39 } 40 41 func Make(config *appview.Config) (*State, error) { 42 + d, err := db.Make(config.Core.DbPath) 43 if err != nil { 44 return nil, err 45 } 46 47 + enforcer, err := rbac.NewEnforcer(config.Core.DbPath) 48 if err != nil { 49 return nil, err 50 } ··· 55 56 resolver := appview.NewResolver() 57 58 + oauth := oauth.NewOAuth(d, config) 59 + 60 wrapper := db.DbWrapper{d} 61 jc, err := jetstream.NewJetstreamClient( 62 + config.Jetstream.Endpoint, 63 "appview", 64 + []string{ 65 + tangled.GraphFollowNSID, 66 + tangled.FeedStarNSID, 67 + tangled.PublicKeyNSID, 68 + tangled.RepoArtifactNSID, 69 + tangled.ActorProfileNSID, 70 + }, 71 nil, 72 slog.Default(), 73 wrapper, ··· 76 if err != nil { 77 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 78 } 79 + err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper, enforcer)) 80 if err != nil { 81 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 82 } 83 84 state := &State{ 85 d, 86 + oauth, 87 enforcer, 88 clock, 89 pgs, ··· 99 return c.Next().String() 100 } 101 102 + // func (s *State) Login(w http.ResponseWriter, r *http.Request) { 103 + // ctx := r.Context() 104 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 + // } 111 112 + // return 113 + // case http.MethodPost: 114 + // handle := strings.TrimPrefix(r.FormValue("handle"), "@") 115 + // appPassword := r.FormValue("app_password") 116 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 + // } 123 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} 130 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 + // } 136 137 + // log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 138 139 + // did := resolved.DID.String() 140 + // defaultKnot := "knot1.tangled.sh" 141 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 + // } 154 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 + // } 166 167 + // if resp.StatusCode != http.StatusNoContent { 168 + // log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 169 + // return 170 + // } 171 + // }() 172 173 + // s.pages.HxRedirect(w, "/") 174 + // return 175 + // } 176 + // } 177 178 func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 179 + s.oauth.ClearSession(r, w) 180 w.Header().Set("HX-Redirect", "/login") 181 w.WriteHeader(http.StatusSeeOther) 182 } 183 184 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 185 + user := s.oauth.GetUser(r) 186 187 timeline, err := db.MakeTimeline(s.db) 188 if err != nil { ··· 233 234 return 235 case http.MethodPost: 236 + session, err := s.oauth.Store.Get(r, appview.SessionName) 237 if err != nil || session.IsNew { 238 log.Println("unauthorized attempt to generate registration key") 239 http.Error(w, "Forbidden", http.StatusUnauthorized) ··· 295 296 // create a signed request and check if a node responds to that 297 func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 298 + user := s.oauth.GetUser(r) 299 300 domain := chi.URLParam(r, "domain") 301 if domain == "" { ··· 310 return 311 } 312 313 + client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) 314 if err != nil { 315 log.Println("failed to create client to ", domain) 316 } ··· 419 return 420 } 421 422 + user := s.oauth.GetUser(r) 423 reg, err := db.RegistrationByDomain(s.db, domain) 424 if err != nil { 425 w.Write([]byte("failed to pull up registration info")) ··· 467 // get knots registered by this user 468 func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 469 // for now, this is just pubkeys 470 + user := s.oauth.GetUser(r) 471 registrations, err := db.RegistrationsByDid(s.db, user.Did) 472 if err != nil { 473 log.Println(err) ··· 520 log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain) 521 522 // announce this relation into the firehose, store into owners' pds 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) 529 createdAt := time.Now().Format(time.RFC3339) 530 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 531 Collection: tangled.KnotMemberNSID, 532 Repo: currentUser.Did, 533 Rkey: appview.TID(), ··· 552 return 553 } 554 555 + ksClient, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) 556 if err != nil { 557 log.Println("failed to create client to ", domain) 558 return ··· 581 func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 582 } 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 + 616 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 617 switch r.Method { 618 case http.MethodGet: 619 + user := s.oauth.GetUser(r) 620 knots, err := s.enforcer.GetDomainsForUser(user.Did) 621 if err != nil { 622 s.pages.Notice(w, "repo", "Invalid user account.") ··· 629 }) 630 631 case http.MethodPost: 632 + user := s.oauth.GetUser(r) 633 634 domain := r.FormValue("domain") 635 if domain == "" { ··· 643 return 644 } 645 646 + if err := validateRepoName(repoName); err != nil { 647 + s.pages.Notice(w, "repo", err.Error()) 648 + return 649 } 650 651 defaultBranch := r.FormValue("branch") ··· 673 return 674 } 675 676 + client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) 677 if err != nil { 678 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 679 return ··· 688 Description: description, 689 } 690 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 + } 696 697 createdAt := time.Now().Format(time.RFC3339) 698 + atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 699 Collection: tangled.RepoNSID, 700 Repo: user.Did, 701 Rkey: rkey,
+1 -1
appview/tid.go
··· 4 "github.com/bluesky-social/indigo/atproto/syntax" 5 ) 6 7 - var c *syntax.TIDClock = syntax.NewTIDClock(0) 8 9 func TID() string { 10 return c.Next().String()
··· 4 "github.com/bluesky-social/indigo/atproto/syntax" 5 ) 6 7 + var c syntax.TIDClock = syntax.NewTIDClock(0) 8 9 func TID() string { 10 return c.Next().String()
+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
··· 26 log.Fatal(err) 27 } 28 29 - log.Println("starting server on", c.ListenAddr) 30 - log.Println(http.ListenAndServe(c.ListenAddr, state.Router())) 31 }
··· 26 log.Fatal(err) 27 } 28 29 + log.Println("starting server on", c.Core.ListenAddr) 30 + log.Println(http.ListenAndServe(c.Core.ListenAddr, state.Router())) 31 }
+1
cmd/gen.go
··· 27 tangled.RepoPullStatus{}, 28 tangled.RepoPullComment{}, 29 tangled.RepoArtifact{}, 30 ); err != nil { 31 panic(err) 32 }
··· 27 tangled.RepoPullStatus{}, 28 tangled.RepoPullComment{}, 29 tangled.RepoArtifact{}, 30 + tangled.ActorProfile{}, 31 ); err != nil { 32 panic(err) 33 }
+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
··· 42 COPY docker/rootfs/ . 43 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 48 49 EXPOSE 22 50 EXPOSE 5555 51 52 - ENTRYPOINT ["/init"]
··· 42 COPY docker/rootfs/ . 43 44 RUN chown root:root /usr/local/libexec/tangled-keyfetch && \ 45 + chmod 755 /usr/local/libexec/tangled-keyfetch 46 47 EXPOSE 22 48 EXPOSE 5555 49 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
··· 13 - "./repositories:/home/git/repositories" 14 - "./server:/app" 15 ports: 16 - - "5555:5555" 17 - "2222:22"
··· 13 - "./repositories:/home/git/repositories" 14 - "./server:/app" 15 ports: 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
···
··· 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
··· 64 "inter-fonts-src": { 65 "flake": false, 66 "locked": { 67 - "lastModified": 1731680160, 68 "narHash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=", 69 "type": "tarball", 70 "url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" ··· 89 }, 90 "nixpkgs": { 91 "locked": { 92 - "lastModified": 1746055187, 93 - "narHash": "sha256-3dqArYSMP9hM7Qpy5YWhnSjiqniSaT2uc5h2Po7tmg0=", 94 "owner": "nixos", 95 "repo": "nixpkgs", 96 - "rev": "3e362ce63e16b9572d8c2297c04f7c19ab6725a5", 97 "type": "github" 98 }, 99 "original": { 100 "owner": "nixos", 101 - "ref": "nixos-24.11", 102 "repo": "nixpkgs", 103 "type": "github" 104 }
··· 64 "inter-fonts-src": { 65 "flake": false, 66 "locked": { 67 + "lastModified": 1731687360, 68 "narHash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=", 69 "type": "tarball", 70 "url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" ··· 89 }, 90 "nixpkgs": { 91 "locked": { 92 + "lastModified": 1746904237, 93 + "narHash": "sha256-3e+AVBczosP5dCLQmMoMEogM57gmZ2qrVSrmq9aResQ=", 94 "owner": "nixos", 95 "repo": "nixpkgs", 96 + "rev": "d89fc19e405cb2d55ce7cc114356846a0ee5e956", 97 "type": "github" 98 }, 99 "original": { 100 "owner": "nixos", 101 + "ref": "nixos-unstable", 102 "repo": "nixpkgs", 103 "type": "github" 104 }
+12 -9
flake.nix
··· 2 description = "atproto github"; 3 4 inputs = { 5 - nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; 6 indigo = { 7 url = "github:oppiliappan/indigo"; 8 flake = false; ··· 49 inherit (gitignore.lib) gitignoreSource; 50 in { 51 overlays.default = final: prev: let 52 - goModHash = "sha256-CmBuvv3duQQoc8iTW4244w1rYLGeqMQS+qQ3wwReZZg="; 53 buildCmdPackage = name: 54 final.buildGoModule { 55 pname = name; ··· 57 src = gitignoreSource ./.; 58 subPackages = ["cmd/${name}"]; 59 vendorHash = goModHash; 60 - CGO_ENABLED = 0; 61 }; 62 in { 63 indigo-lexgen = final.buildGoModule { ··· 88 doCheck = false; 89 subPackages = ["cmd/appview"]; 90 vendorHash = goModHash; 91 - CGO_ENABLED = 1; 92 stdenv = pkgsStatic.stdenv; 93 }; 94 ··· 111 112 runHook postInstall 113 ''; 114 - CGO_ENABLED = 1; 115 }; 116 knotserver-unwrapped = final.pkgsStatic.buildGoModule { 117 pname = "knotserver"; ··· 119 src = gitignoreSource ./.; 120 subPackages = ["cmd/knotserver"]; 121 vendorHash = goModHash; 122 - CGO_ENABLED = 1; 123 }; 124 repoguard = buildCmdPackage "repoguard"; 125 keyfetch = buildCmdPackage "keyfetch"; 126 }; 127 packages = forAllSystems (system: { 128 inherit ··· 133 knotserver-unwrapped 134 repoguard 135 keyfetch 136 ; 137 }); 138 defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview); ··· 162 cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 163 cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 164 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 165 ''; 166 }; 167 }); 168 apps = forAllSystems (system: let ··· 170 air-watcher = name: 171 pkgs.writeShellScriptBin "run" 172 '' 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" \ 175 -build.bin "./out/${name}.out" \ 176 -build.stop_on_error "true" \ 177 -build.include_ext "go" ··· 446 }; 447 }; 448 } 449 -
··· 2 description = "atproto github"; 3 4 inputs = { 5 + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 indigo = { 7 url = "github:oppiliappan/indigo"; 8 flake = false; ··· 49 inherit (gitignore.lib) gitignoreSource; 50 in { 51 overlays.default = final: prev: let 52 + goModHash = "sha256-zcfTNo7QsiihzLa4qHEX8uGGtbcmBn8TlSm0YHBRNw8="; 53 buildCmdPackage = name: 54 final.buildGoModule { 55 pname = name; ··· 57 src = gitignoreSource ./.; 58 subPackages = ["cmd/${name}"]; 59 vendorHash = goModHash; 60 + env.CGO_ENABLED = 0; 61 }; 62 in { 63 indigo-lexgen = final.buildGoModule { ··· 88 doCheck = false; 89 subPackages = ["cmd/appview"]; 90 vendorHash = goModHash; 91 + env.CGO_ENABLED = 1; 92 stdenv = pkgsStatic.stdenv; 93 }; 94 ··· 111 112 runHook postInstall 113 ''; 114 + env.CGO_ENABLED = 1; 115 }; 116 knotserver-unwrapped = final.pkgsStatic.buildGoModule { 117 pname = "knotserver"; ··· 119 src = gitignoreSource ./.; 120 subPackages = ["cmd/knotserver"]; 121 vendorHash = goModHash; 122 + env.CGO_ENABLED = 1; 123 }; 124 repoguard = buildCmdPackage "repoguard"; 125 keyfetch = buildCmdPackage "keyfetch"; 126 + genjwks = buildCmdPackage "genjwks"; 127 }; 128 packages = forAllSystems (system: { 129 inherit ··· 134 knotserver-unwrapped 135 repoguard 136 keyfetch 137 + genjwks 138 ; 139 }); 140 defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview); ··· 164 cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 165 cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 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)" 168 ''; 169 + env.CGO_ENABLED = 1; 170 }; 171 }); 172 apps = forAllSystems (system: let ··· 174 air-watcher = name: 175 pkgs.writeShellScriptBin "run" 176 '' 177 + ${pkgs.air}/bin/air -c /dev/null \ 178 + -build.cmd "${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \ 179 -build.bin "./out/${name}.out" \ 180 -build.stop_on_error "true" \ 181 -build.include_ext "go" ··· 450 }; 451 }; 452 }
+18 -12
go.mod
··· 1 module tangled.sh/tangled.sh/core 2 3 - go 1.23.0 4 5 - toolchain go1.23.6 6 7 require ( 8 github.com/Blank-Xu/sql-adapter v1.1.1 9 github.com/alecthomas/chroma/v2 v2.15.0 10 github.com/bluekeyes/go-gitdiff v0.8.1 11 - github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20 12 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 13 github.com/casbin/casbin/v2 v2.103.0 14 github.com/cyphar/filepath-securejoin v0.4.1 ··· 19 github.com/go-git/go-git/v5 v5.14.0 20 github.com/google/uuid v1.6.0 21 github.com/gorilla/sessions v1.4.0 22 github.com/ipfs/go-cid v0.5.0 23 github.com/mattn/go-sqlite3 v1.14.24 24 github.com/microcosm-cc/bluemonday v1.0.27 25 github.com/resend/resend-go/v2 v2.15.0 ··· 41 github.com/casbin/govaluate v1.3.0 // indirect 42 github.com/cespare/xxhash/v2 v2.3.0 // indirect 43 github.com/cloudflare/circl v1.6.0 // indirect 44 - github.com/davecgh/go-spew v1.1.1 // indirect 45 github.com/dlclark/regexp2 v1.11.5 // indirect 46 github.com/emirpasic/gods v1.18.1 // indirect 47 github.com/felixge/httpsnoop v1.0.4 // indirect 48 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 49 github.com/go-git/go-billy/v5 v5.6.2 // indirect 50 - github.com/go-logr/logr v1.4.1 // indirect 51 github.com/go-logr/stdr v1.2.2 // indirect 52 github.com/goccy/go-json v0.10.2 // indirect 53 github.com/gogo/protobuf v1.3.2 // indirect 54 github.com/gorilla/css v1.0.1 // indirect 55 github.com/gorilla/securecookie v1.1.2 // indirect 56 github.com/gorilla/websocket v1.5.1 // indirect ··· 75 github.com/kevinburke/ssh_config v1.2.0 // indirect 76 github.com/klauspost/compress v1.17.9 // indirect 77 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 78 github.com/mattn/go-isatty v0.0.20 // indirect 79 github.com/minio/sha256-simd v1.0.1 // indirect 80 github.com/mr-tron/base58 v1.2.0 // indirect ··· 86 github.com/opentracing/opentracing-go v1.2.0 // indirect 87 github.com/pjbgf/sha1cd v0.3.2 // indirect 88 github.com/pkg/errors v0.9.1 // indirect 89 - github.com/pmezard/go-difflib v1.0.0 // indirect 90 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 91 github.com/prometheus/client_golang v1.19.1 // indirect 92 github.com/prometheus/client_model v0.6.1 // indirect 93 github.com/prometheus/common v0.54.0 // indirect 94 github.com/prometheus/procfs v0.15.1 // indirect 95 github.com/sergi/go-diff v1.3.1 // indirect 96 github.com/skeema/knownhosts v1.3.1 // indirect 97 github.com/spaolacci/murmur3 v1.1.0 // indirect 98 - github.com/stretchr/testify v1.10.0 // indirect 99 github.com/xanzy/ssh-agent v0.3.3 // indirect 100 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 101 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 102 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 106 go.uber.org/atomic v1.11.0 // indirect 107 go.uber.org/multierr v1.11.0 // indirect 108 go.uber.org/zap v1.26.0 // indirect 109 golang.org/x/crypto v0.37.0 // indirect 110 golang.org/x/net v0.39.0 // indirect 111 golang.org/x/sys v0.32.0 // indirect 112 - golang.org/x/time v0.5.0 // indirect 113 google.golang.org/protobuf v1.34.2 // indirect 114 gopkg.in/warnings.v0 v0.1.2 // indirect 115 - gopkg.in/yaml.v3 v3.0.1 // indirect 116 lukechampine.com/blake3 v1.2.1 // indirect 117 ) 118
··· 1 module tangled.sh/tangled.sh/core 2 3 + go 1.24.0 4 5 + toolchain go1.24.3 6 7 require ( 8 github.com/Blank-Xu/sql-adapter v1.1.1 9 github.com/alecthomas/chroma/v2 v2.15.0 10 github.com/bluekeyes/go-gitdiff v0.8.1 11 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 12 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 13 github.com/casbin/casbin/v2 v2.103.0 14 github.com/cyphar/filepath-securejoin v0.4.1 ··· 19 github.com/go-git/go-git/v5 v5.14.0 20 github.com/google/uuid v1.6.0 21 github.com/gorilla/sessions v1.4.0 22 + github.com/haileyok/atproto-oauth-golang v0.0.2 23 github.com/ipfs/go-cid v0.5.0 24 + github.com/lestrrat-go/jwx/v2 v2.0.12 25 github.com/mattn/go-sqlite3 v1.14.24 26 github.com/microcosm-cc/bluemonday v1.0.27 27 github.com/resend/resend-go/v2 v2.15.0 ··· 43 github.com/casbin/govaluate v1.3.0 // indirect 44 github.com/cespare/xxhash/v2 v2.3.0 // indirect 45 github.com/cloudflare/circl v1.6.0 // indirect 46 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 47 github.com/dlclark/regexp2 v1.11.5 // indirect 48 github.com/emirpasic/gods v1.18.1 // indirect 49 github.com/felixge/httpsnoop v1.0.4 // indirect 50 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 51 github.com/go-git/go-billy/v5 v5.6.2 // indirect 52 + github.com/go-logr/logr v1.4.2 // indirect 53 github.com/go-logr/stdr v1.2.2 // indirect 54 github.com/goccy/go-json v0.10.2 // indirect 55 github.com/gogo/protobuf v1.3.2 // indirect 56 + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 57 github.com/gorilla/css v1.0.1 // indirect 58 github.com/gorilla/securecookie v1.1.2 // indirect 59 github.com/gorilla/websocket v1.5.1 // indirect ··· 78 github.com/kevinburke/ssh_config v1.2.0 // indirect 79 github.com/klauspost/compress v1.17.9 // indirect 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 86 github.com/mattn/go-isatty v0.0.20 // indirect 87 github.com/minio/sha256-simd v1.0.1 // indirect 88 github.com/mr-tron/base58 v1.2.0 // indirect ··· 94 github.com/opentracing/opentracing-go v1.2.0 // indirect 95 github.com/pjbgf/sha1cd v0.3.2 // indirect 96 github.com/pkg/errors v0.9.1 // indirect 97 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 98 github.com/prometheus/client_golang v1.19.1 // indirect 99 github.com/prometheus/client_model v0.6.1 // indirect 100 github.com/prometheus/common v0.54.0 // indirect 101 github.com/prometheus/procfs v0.15.1 // indirect 102 + github.com/segmentio/asm v1.2.0 // indirect 103 github.com/sergi/go-diff v1.3.1 // indirect 104 github.com/skeema/knownhosts v1.3.1 // indirect 105 github.com/spaolacci/murmur3 v1.1.0 // indirect 106 github.com/xanzy/ssh-agent v0.3.3 // indirect 107 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 108 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 109 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // 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 113 go.uber.org/atomic v1.11.0 // indirect 114 go.uber.org/multierr v1.11.0 // indirect 115 go.uber.org/zap v1.26.0 // indirect 116 golang.org/x/crypto v0.37.0 // indirect 117 golang.org/x/net v0.39.0 // indirect 118 golang.org/x/sys v0.32.0 // indirect 119 + golang.org/x/time v0.8.0 // indirect 120 google.golang.org/protobuf v1.34.2 // indirect 121 gopkg.in/warnings.v0 v0.1.2 // indirect 122 lukechampine.com/blake3 v1.2.1 // indirect 123 ) 124
+61 -16
go.sum
··· 26 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 27 github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI= 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= 31 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA= 32 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4= 33 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= ··· 52 github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 53 github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 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 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= 58 github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= 59 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= ··· 82 github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= 83 github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= 84 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= 87 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 88 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 89 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= ··· 91 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 92 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 93 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 94 github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 95 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 96 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= ··· 111 github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 112 github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 113 github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 114 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 115 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 116 github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= ··· 159 github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 160 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 161 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 162 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 163 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 164 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= ··· 177 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 178 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 179 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 180 github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 181 github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 182 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= ··· 212 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 213 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 214 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 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 217 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 218 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 219 github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= ··· 227 github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE= 228 github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= 229 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= 232 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 233 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 234 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 235 github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= ··· 246 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 247 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 248 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 249 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 250 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 251 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 252 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 253 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 254 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 255 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= ··· 270 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 271 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 272 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= 279 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 280 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 281 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 303 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 304 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 305 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 306 golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 307 golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 308 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= ··· 314 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 315 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 316 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 317 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 318 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 319 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= ··· 327 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 328 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 329 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 330 golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 331 golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 332 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 334 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 335 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 336 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 337 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 338 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= ··· 348 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 349 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 350 golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 351 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 352 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 353 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= ··· 357 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 358 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 359 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 360 golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 361 golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 362 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= ··· 364 golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 365 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 366 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 367 golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 368 golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 369 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= ··· 372 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 373 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 374 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 375 golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 376 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= 379 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 380 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 381 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= ··· 389 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 390 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 391 golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 392 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 393 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 394 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
··· 26 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 27 github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI= 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-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk= 30 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 31 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA= 32 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4= 33 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= ··· 52 github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 53 github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 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/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= 61 github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= 62 github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= 63 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= ··· 86 github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= 87 github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= 88 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 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= 91 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 92 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 93 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= ··· 95 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 96 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 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= 100 github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 101 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 102 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= ··· 117 github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 118 github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 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= 122 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 123 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 124 github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= ··· 167 github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 168 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 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= 172 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 173 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 174 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= ··· 187 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 188 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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= 204 github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 205 github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 206 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= ··· 236 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 237 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 238 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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= 242 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 243 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 244 github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= ··· 252 github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE= 253 github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= 254 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 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= 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= 260 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 261 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 262 github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= ··· 273 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 274 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 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= 278 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 279 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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= 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= 286 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 287 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 288 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= ··· 303 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 304 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 305 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 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= 312 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 313 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 314 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 336 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 337 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 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= 340 golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 341 golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 342 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= ··· 348 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 349 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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= 352 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 353 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 354 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= ··· 362 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 363 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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= 366 golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 367 golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 368 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 370 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 371 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= 374 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 375 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 376 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= ··· 385 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 386 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= 389 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 390 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 391 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= ··· 395 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 396 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= 400 golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 401 golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 402 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= ··· 404 golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 405 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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= 409 golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 410 golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 411 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= ··· 414 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 415 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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= 419 golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 420 golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 421 + golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 422 + golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 423 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 424 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 425 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= ··· 433 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 434 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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= 437 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 438 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 439 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+38
knotserver/routes.go
··· 600 name := data.Name 601 defaultBranch := data.DefaultBranch 602 603 relativeRepoPath := filepath.Join(did, name) 604 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 605 err := git.InitBare(repoPath, defaultBranch) ··· 1078 func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1079 w.Write([]byte("ok")) 1080 }
··· 600 name := data.Name 601 defaultBranch := data.DefaultBranch 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 + 609 relativeRepoPath := filepath.Join(did, name) 610 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 611 err := git.InitBare(repoPath, defaultBranch) ··· 1084 func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1085 w.Write([]byte("ok")) 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
···
··· 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
··· 22 }, 23 "name": { 24 "type": "string", 25 - "format": "string", 26 "description": "human-readable name for this key" 27 }, 28 "createdAt": {
··· 22 }, 23 "name": { 24 "type": "string", 25 "description": "human-readable name for this key" 26 }, 27 "createdAt": {
+5
scripts/generate-jwks.sh
···
··· 1 + #! /usr/bin/env bash 2 + 3 + set -e 4 + 5 + go run ./cmd/genjwks/