···88 "fmt"
99 "io"
1010 "log"
1111+ "log/slog"
1112 "net/http"
1213 "net/url"
1314 "path/filepath"
···5152 db *db.DB
5253 enforcer *rbac.Enforcer
5354 notifier notify.Notifier
5555+ logger *slog.Logger
5456}
55575658func New(
···6365 config *config.Config,
6466 notifier notify.Notifier,
6567 enforcer *rbac.Enforcer,
6868+ logger *slog.Logger,
6669) *Repo {
6770 return &Repo{oauth: oauth,
6871 repoResolver: repoResolver,
···7376 db: db,
7477 notifier: notifier,
7578 enforcer: enforcer,
7979+ logger: logger,
7680 }
7781}
7882···627631628632// modify the spindle configured for this repo
629633func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
634634+ user := rp.oauth.GetUser(r)
635635+ l := rp.logger.With("handler", "EditSpindle")
636636+ l = l.With("did", user.Did)
637637+ l = l.With("handle", user.Handle)
638638+639639+ errorId := "operation-error"
640640+ fail := func(msg string, err error) {
641641+ l.Error(msg, "err", err)
642642+ rp.pages.Notice(w, errorId, msg)
643643+ }
644644+630645 f, err := rp.repoResolver.Resolve(r)
631646 if err != nil {
632632- log.Println("failed to get repo and knot", err)
633633- w.WriteHeader(http.StatusBadRequest)
647647+ fail("Failed to resolve repo. Try again later", err)
634648 return
635649 }
636650637651 repoAt := f.RepoAt
638652 rkey := repoAt.RecordKey().String()
639653 if rkey == "" {
640640- log.Println("invalid aturi for repo", err)
641641- w.WriteHeader(http.StatusInternalServerError)
654654+ fail("Failed to resolve repo. Try again later", err)
642655 return
643656 }
644657645645- user := rp.oauth.GetUser(r)
646646-647658 newSpindle := r.FormValue("spindle")
648659 client, err := rp.oauth.AuthorizedClient(r)
649660 if err != nil {
650650- log.Println("failed to get client")
651651- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
661661+ fail("Failed to authorize. Try again later.", err)
652662 return
653663 }
654664655665 // ensure that this is a valid spindle for this user
656666 validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
657667 if err != nil {
658658- log.Println("failed to get valid spindles")
659659- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
668668+ fail("Failed to find spindles. Try again later.", err)
660669 return
661670 }
662671663672 if !slices.Contains(validSpindles, newSpindle) {
664664- log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles)
665665- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
673673+ fail("Failed to configure spindle.", fmt.Errorf("%s is not a valid spindle: %q", newSpindle, validSpindles))
666674 return
667675 }
668676669677 // optimistic update
670678 err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)
671679 if err != nil {
672672- log.Println("failed to perform update-spindle query", err)
673673- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
680680+ fail("Failed to update spindle. Try again later.", err)
674681 return
675682 }
676683677684 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
678685 if err != nil {
679679- // failed to get record
680680- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.")
686686+ fail("Failed to update spindle, no record found on PDS.", err)
681687 return
682688 }
683689 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···698704 })
699705700706 if err != nil {
701701- log.Println("failed to perform update-spindle query", err)
702702- // failed to get record
703703- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.")
707707+ fail("Failed to update spindle, unable to save to PDS.", err)
704708 return
705709 }
706710···710714 eventconsumer.NewSpindleSource(newSpindle),
711715 )
712716713713- w.Write(fmt.Append(nil, "spindle set to: ", newSpindle))
717717+ rp.pages.HxRefresh(w)
714718}
715719716720func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
721721+ user := rp.oauth.GetUser(r)
722722+ l := rp.logger.With("handler", "AddCollaborator")
723723+ l = l.With("did", user.Did)
724724+ l = l.With("handle", user.Handle)
725725+717726 f, err := rp.repoResolver.Resolve(r)
718727 if err != nil {
719719- log.Println("failed to get repo and knot", err)
728728+ l.Error("failed to get repo and knot", "err", err)
720729 return
721730 }
722731732732+ errorId := "add-collaborator-error"
733733+ fail := func(msg string, err error) {
734734+ l.Error(msg, "err", err)
735735+ rp.pages.Notice(w, errorId, msg)
736736+ }
737737+723738 collaborator := r.FormValue("collaborator")
724739 if collaborator == "" {
725725- http.Error(w, "malformed form", http.StatusBadRequest)
740740+ fail("Invalid form.", nil)
726741 return
727742 }
728743729744 collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)
730745 if err != nil {
731731- w.Write([]byte("failed to resolve collaborator did to a handle"))
746746+ fail(fmt.Sprintf("'%s' is not a valid DID/handle.", collaborator), err)
732747 return
733748 }
734734- log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
735749736736- // TODO: create an atproto record for this
750750+ if collaboratorIdent.DID.String() == user.Did {
751751+ fail("You seem to be adding yourself as a collaborator.", nil)
752752+ return
753753+ }
754754+755755+ l = l.With("collaborator", collaboratorIdent.Handle)
756756+ l = l.With("knot", f.Knot)
757757+ l.Info("adding to knot")
737758738759 secret, err := db.GetRegistrationKey(rp.db, f.Knot)
739760 if err != nil {
740740- log.Printf("no key found for domain %s: %s\n", f.Knot, err)
761761+ fail("Failed to add to knot.", err)
741762 return
742763 }
743764744765 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
745766 if err != nil {
746746- log.Println("failed to create client to ", f.Knot)
767767+ fail("Failed to add to knot.", err)
747768 return
748769 }
749770750771 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
751772 if err != nil {
752752- log.Printf("failed to make request to %s: %s", f.Knot, err)
773773+ fail("Knot was unreachable.", err)
753774 return
754775 }
755776756777 if ksResp.StatusCode != http.StatusNoContent {
757757- w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err))
778778+ fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil)
758779 return
759780 }
760781761782 tx, err := rp.db.BeginTx(r.Context(), nil)
762783 if err != nil {
763763- log.Println("failed to start tx")
764764- w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
784784+ fail("Failed to add collaborator.", err)
765785 return
766786 }
767787 defer func() {
768788 tx.Rollback()
769789 err = rp.enforcer.E.LoadPolicy()
770790 if err != nil {
771771- log.Println("failed to rollback policies")
791791+ fail("Failed to add collaborator.", err)
772792 }
773793 }()
774794775795 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
776796 if err != nil {
777777- w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
797797+ fail("Failed to add collaborator permissions.", err)
778798 return
779799 }
780800781801 err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
782802 if err != nil {
783783- w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
803803+ fail("Failed to add collaborator.", err)
784804 return
785805 }
786806787807 err = tx.Commit()
788808 if err != nil {
789789- log.Println("failed to commit changes", err)
790790- http.Error(w, err.Error(), http.StatusInternalServerError)
809809+ fail("Failed to add collaborator.", err)
791810 return
792811 }
793812794813 err = rp.enforcer.E.SavePolicy()
795814 if err != nil {
796796- log.Println("failed to update ACLs", err)
797797- http.Error(w, err.Error(), http.StatusInternalServerError)
815815+ fail("Failed to update collaborator permissions.", err)
798816 return
799817 }
800818801801- w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String()))
802802-819819+ rp.pages.HxRefresh(w)
803820}
804821805822func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
···952969}
953970954971func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
972972+ user := rp.oauth.GetUser(r)
973973+ l := rp.logger.With("handler", "Secrets")
974974+ l = l.With("handle", user.Handle)
975975+ l = l.With("did", user.Did)
976976+955977 f, err := rp.repoResolver.Resolve(r)
956978 if err != nil {
957979 log.Println("failed to get repo and knot", err)
···98710099881010 switch r.Method {
9891011 case http.MethodPut:
10121012+ errorId := "add-secret-error"
10131013+9901014 value := r.FormValue("value")
991991- if key == "" {
10151015+ if value == "" {
9921016 w.WriteHeader(http.StatusBadRequest)
9931017 return
9941018 }
···10031027 },
10041028 )
10051029 if err != nil {
10061006- log.Println("request didnt run", "err", err)
10301030+ l.Error("Failed to add secret.", "err", err)
10311031+ rp.pages.Notice(w, errorId, "Failed to add secret.")
10071032 return
10081033 }
1009103410101035 case http.MethodDelete:
10361036+ errorId := "operation-error"
10371037+10111038 err = tangled.RepoRemoveSecret(
10121039 r.Context(),
10131040 spindleClient,
···10171044 },
10181045 )
10191046 if err != nil {
10201020- log.Println("request didnt run", "err", err)
10471047+ l.Error("Failed to delete secret.", "err", err)
10481048+ rp.pages.Notice(w, errorId, "Failed to delete secret.")
10211049 return
10221050 }
10231051 }
10521052+10531053+ rp.pages.HxRefresh(w)
10241054}
1025105510561056+type tab = map[string]any
10571057+10581058+var (
10591059+ // would be great to have ordered maps right about now
10601060+ settingsTabs []tab = []tab{
10611061+ {"Name": "general", "Icon": "sliders-horizontal"},
10621062+ {"Name": "access", "Icon": "users"},
10631063+ {"Name": "pipelines", "Icon": "layers-2"},
10641064+ }
10651065+)
10661066+10261067func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
10681068+ tabVal := r.URL.Query().Get("tab")
10691069+ if tabVal == "" {
10701070+ tabVal = "general"
10711071+ }
10721072+10731073+ switch tabVal {
10741074+ case "general":
10751075+ rp.generalSettings(w, r)
10761076+10771077+ case "access":
10781078+ rp.accessSettings(w, r)
10791079+10801080+ case "pipelines":
10811081+ rp.pipelineSettings(w, r)
10821082+ }
10831083+10841084+ // user := rp.oauth.GetUser(r)
10851085+ // repoCollaborators, err := f.Collaborators(r.Context())
10861086+ // if err != nil {
10871087+ // log.Println("failed to get collaborators", err)
10881088+ // }
10891089+10901090+ // isCollaboratorInviteAllowed := false
10911091+ // if user != nil {
10921092+ // ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
10931093+ // if err == nil && ok {
10941094+ // isCollaboratorInviteAllowed = true
10951095+ // }
10961096+ // }
10971097+10981098+ // us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
10991099+ // if err != nil {
11001100+ // log.Println("failed to create unsigned client", err)
11011101+ // return
11021102+ // }
11031103+11041104+ // result, err := us.Branches(f.OwnerDid(), f.RepoName)
11051105+ // if err != nil {
11061106+ // log.Println("failed to reach knotserver", err)
11071107+ // return
11081108+ // }
11091109+11101110+ // // all spindles that this user is a member of
11111111+ // spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
11121112+ // if err != nil {
11131113+ // log.Println("failed to fetch spindles", err)
11141114+ // return
11151115+ // }
11161116+11171117+ // var secrets []*tangled.RepoListSecrets_Secret
11181118+ // if f.Spindle != "" {
11191119+ // if spindleClient, err := rp.oauth.ServiceClient(
11201120+ // r,
11211121+ // oauth.WithService(f.Spindle),
11221122+ // oauth.WithLxm(tangled.RepoListSecretsNSID),
11231123+ // oauth.WithDev(rp.config.Core.Dev),
11241124+ // ); err != nil {
11251125+ // log.Println("failed to create spindle client", err)
11261126+ // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
11271127+ // log.Println("failed to fetch secrets", err)
11281128+ // } else {
11291129+ // secrets = resp.Secrets
11301130+ // }
11311131+ // }
11321132+11331133+ // rp.pages.RepoSettings(w, pages.RepoSettingsParams{
11341134+ // LoggedInUser: user,
11351135+ // RepoInfo: f.RepoInfo(user),
11361136+ // Collaborators: repoCollaborators,
11371137+ // IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
11381138+ // Branches: result.Branches,
11391139+ // Spindles: spindles,
11401140+ // CurrentSpindle: f.Spindle,
11411141+ // Secrets: secrets,
11421142+ // })
11431143+}
11441144+11451145+func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
10271146 f, err := rp.repoResolver.Resolve(r)
11471147+ user := rp.oauth.GetUser(r)
11481148+11491149+ us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
10281150 if err != nil {
10291029- log.Println("failed to get repo and knot", err)
11511151+ log.Println("failed to create unsigned client", err)
11521152+ return
11531153+ }
11541154+11551155+ result, err := us.Branches(f.OwnerDid(), f.RepoName)
11561156+ if err != nil {
11571157+ log.Println("failed to reach knotserver", err)
10301158 return
10311159 }
1032116010331033- switch r.Method {
10341034- case http.MethodGet:
10351035- // for now, this is just pubkeys
10361036- user := rp.oauth.GetUser(r)
10371037- repoCollaborators, err := f.Collaborators(r.Context())
10381038- if err != nil {
10391039- log.Println("failed to get collaborators", err)
10401040- }
11611161+ rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
11621162+ LoggedInUser: user,
11631163+ RepoInfo: f.RepoInfo(user),
11641164+ Branches: result.Branches,
11651165+ Tabs: settingsTabs,
11661166+ Tab: "general",
11671167+ })
11681168+}
11691169+11701170+func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) {
11711171+ f, err := rp.repoResolver.Resolve(r)
11721172+ user := rp.oauth.GetUser(r)
11731173+11741174+ repoCollaborators, err := f.Collaborators(r.Context())
11751175+ if err != nil {
11761176+ log.Println("failed to get collaborators", err)
11771177+ }
11781178+11791179+ rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
11801180+ LoggedInUser: user,
11811181+ RepoInfo: f.RepoInfo(user),
11821182+ Tabs: settingsTabs,
11831183+ Tab: "access",
11841184+ Collaborators: repoCollaborators,
11851185+ })
11861186+}
1041118710421042- isCollaboratorInviteAllowed := false
10431043- if user != nil {
10441044- ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
10451045- if err == nil && ok {
10461046- isCollaboratorInviteAllowed = true
10471047- }
10481048- }
11881188+func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) {
11891189+ f, err := rp.repoResolver.Resolve(r)
11901190+ user := rp.oauth.GetUser(r)
1049119110501050- us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
10511051- if err != nil {
10521052- log.Println("failed to create unsigned client", err)
10531053- return
10541054- }
11921192+ // all spindles that this user is a member of
11931193+ spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
11941194+ if err != nil {
11951195+ log.Println("failed to fetch spindles", err)
11961196+ return
11971197+ }
1055119810561056- result, err := us.Branches(f.OwnerDid(), f.RepoName)
10571057- if err != nil {
10581058- log.Println("failed to reach knotserver", err)
10591059- return
11991199+ var secrets []*tangled.RepoListSecrets_Secret
12001200+ if f.Spindle != "" {
12011201+ if spindleClient, err := rp.oauth.ServiceClient(
12021202+ r,
12031203+ oauth.WithService(f.Spindle),
12041204+ oauth.WithLxm(tangled.RepoListSecretsNSID),
12051205+ oauth.WithDev(rp.config.Core.Dev),
12061206+ ); err != nil {
12071207+ log.Println("failed to create spindle client", err)
12081208+ } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
12091209+ log.Println("failed to fetch secrets", err)
12101210+ } else {
12111211+ secrets = resp.Secrets
10601212 }
12131213+ }
1061121410621062- // all spindles that this user is a member of
10631063- spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
10641064- if err != nil {
10651065- log.Println("failed to fetch spindles", err)
10661066- return
10671067- }
12151215+ slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {
12161216+ return strings.Compare(a.Key, b.Key)
12171217+ })
1068121810691069- var secrets []*tangled.RepoListSecrets_Secret
10701070- if f.Spindle != "" {
10711071- if spindleClient, err := rp.oauth.ServiceClient(
10721072- r,
10731073- oauth.WithService(f.Spindle),
10741074- oauth.WithLxm(tangled.RepoListSecretsNSID),
10751075- oauth.WithDev(rp.config.Core.Dev),
10761076- ); err != nil {
10771077- log.Println("failed to create spindle client", err)
10781078- } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
10791079- log.Println("failed to fetch secrets", err)
10801080- } else {
10811081- secrets = resp.Secrets
10821082- }
10831083- }
12191219+ var dids []string
12201220+ for _, s := range secrets {
12211221+ dids = append(dids, s.CreatedBy)
12221222+ }
12231223+ resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids)
1084122410851085- rp.pages.RepoSettings(w, pages.RepoSettingsParams{
10861086- LoggedInUser: user,
10871087- RepoInfo: f.RepoInfo(user),
10881088- Collaborators: repoCollaborators,
10891089- IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
10901090- Branches: result.Branches,
10911091- Spindles: spindles,
10921092- CurrentSpindle: f.Spindle,
10931093- Secrets: secrets,
12251225+ // convert to a more manageable form
12261226+ var niceSecret []map[string]any
12271227+ for id, s := range secrets {
12281228+ when, _ := time.Parse(time.RFC3339, s.CreatedAt)
12291229+ niceSecret = append(niceSecret, map[string]any{
12301230+ "Id": id,
12311231+ "Key": s.Key,
12321232+ "CreatedAt": when,
12331233+ "CreatedBy": resolvedIdents[id].Handle.String(),
10941234 })
10951235 }
12361236+12371237+ rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
12381238+ LoggedInUser: user,
12391239+ RepoInfo: f.RepoInfo(user),
12401240+ Tabs: settingsTabs,
12411241+ Tab: "pipelines",
12421242+ Spindles: spindles,
12431243+ CurrentSpindle: f.Spindle,
12441244+ Secrets: niceSecret,
12451245+ })
10961246}
1097124710981248func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
+4-3
appview/reporesolver/resolver.go
···149149 for _, item := range repoCollaborators {
150150 // currently only two roles: owner and member
151151 var role string
152152- if item[3] == "repo:owner" {
152152+ switch item[3] {
153153+ case "repo:owner":
153154 role = "owner"
154154- } else if item[3] == "repo:collaborator" {
155155+ case "repo:collaborator":
155156 role = "collaborator"
156156- } else {
157157+ default:
157158 continue
158159 }
159160