Monorepo for Tangled tangled.org

appview: add spindle to repo settings form

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 345b3621 1d0275d6

verified
Changed files
+179 -24
appview
db
middleware
pages
templates
repo
reporesolver
+8
appview/db/db.go
··· 455 }) 456 db.Exec("pragma foreign_keys = on;") 457 458 return &DB{db}, nil 459 } 460
··· 455 }) 456 db.Exec("pragma foreign_keys = on;") 457 458 + // run migrations 459 + runMigration(db, "add-spindle-to-repos", func(tx *sql.Tx) error { 460 + tx.Exec(` 461 + alter table repos add column spindle text; 462 + `) 463 + return nil 464 + }) 465 + 466 return &DB{db}, nil 467 } 468
+23 -7
appview/db/repos.go
··· 18 Created time.Time 19 AtUri string 20 Description string 21 22 // optionally, populate this when querying for reverse mappings 23 RepoStats *RepoStats ··· 138 139 func GetRepo(e Execer, did, name string) (*Repo, error) { 140 var repo Repo 141 - var nullableDescription sql.NullString 142 143 - row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name) 144 145 var createdAt string 146 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 147 return nil, err 148 } 149 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 150 repo.Created = createdAtTime 151 152 - if nullableDescription.Valid { 153 - repo.Description = nullableDescription.String 154 - } else { 155 - repo.Description = "" 156 } 157 158 return &repo, nil ··· 302 func UpdateDescription(e Execer, repoAt, newDescription string) error { 303 _, err := e.Exec( 304 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 305 return err 306 } 307
··· 18 Created time.Time 19 AtUri string 20 Description string 21 + Spindle string 22 23 // optionally, populate this when querying for reverse mappings 24 RepoStats *RepoStats ··· 139 140 func GetRepo(e Execer, did, name string) (*Repo, error) { 141 var repo Repo 142 + var description, spindle sql.NullString 143 144 + row := e.QueryRow(` 145 + select did, name, knot, created, at_uri, description, spindle 146 + from repos 147 + where did = ? and name = ? 148 + `, 149 + did, 150 + name, 151 + ) 152 153 var createdAt string 154 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil { 155 return nil, err 156 } 157 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 158 repo.Created = createdAtTime 159 160 + if description.Valid { 161 + repo.Description = description.String 162 + } 163 + 164 + if spindle.Valid { 165 + repo.Spindle = spindle.String 166 } 167 168 return &repo, nil ··· 312 func UpdateDescription(e Execer, repoAt, newDescription string) error { 313 _, err := e.Exec( 314 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 315 + return err 316 + } 317 + 318 + func UpdateSpindle(e Execer, repoAt, spindle string) error { 319 + _, err := e.Exec( 320 + `update repos set spindle = ? where at_uri = ?`, spindle, repoAt) 321 return err 322 } 323
+1
appview/middleware/middleware.go
··· 225 ctx := context.WithValue(req.Context(), "knot", repo.Knot) 226 ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 227 ctx = context.WithValue(ctx, "repoDescription", repo.Description) 228 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) 229 next.ServeHTTP(w, req.WithContext(ctx)) 230 })
··· 225 ctx := context.WithValue(req.Context(), "knot", repo.Knot) 226 ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 227 ctx = context.WithValue(ctx, "repoDescription", repo.Description) 228 + ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle) 229 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) 230 next.ServeHTTP(w, req.WithContext(ctx)) 231 })
+7 -5
appview/pages/pages.go
··· 616 } 617 618 type RepoSettingsParams struct { 619 - LoggedInUser *oauth.User 620 - RepoInfo repoinfo.RepoInfo 621 - Collaborators []Collaborator 622 - Active string 623 - Branches []types.Branch 624 // TODO: use repoinfo.roles 625 IsCollaboratorInviteAllowed bool 626 }
··· 616 } 617 618 type RepoSettingsParams struct { 619 + LoggedInUser *oauth.User 620 + RepoInfo repoinfo.RepoInfo 621 + Collaborators []Collaborator 622 + Active string 623 + Branches []types.Branch 624 + Spindles []string 625 + CurrentSpindle string 626 // TODO: use repoinfo.roles 627 IsCollaboratorInviteAllowed bool 628 }
+37 -2
appview/pages/templates/repo/settings.html
··· 81 </div> 82 </form> 83 84 {{ if .RepoInfo.Roles.RepoDeleteAllowed }} 85 <form 86 hx-confirm="Are you sure you want to delete this repository?" ··· 89 hx-indicator="#delete-repo-spinner" 90 > 91 <label for="branch">delete repository</label> 92 - <button class="btn my-2 flex gap-2 items-center" type="text"> 93 <span>delete</span> 94 <span id="delete-repo-spinner" class="group"> 95 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 96 </span> 97 </button> 98 <span>
··· 81 </div> 82 </form> 83 84 + {{ if .RepoInfo.Roles.IsOwner }} 85 + <form 86 + hx-put="/{{ $.RepoInfo.FullName }}/settings/spindle" 87 + class="mt-6 group" 88 + > 89 + <label for="spindle">spindle</label> 90 + <div class="flex gap-2 items-center"> 91 + <select id="spindle" name="spindle" required class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"> 92 + <option 93 + value="" 94 + disabled 95 + selected 96 + > 97 + Choose a spindle 98 + </option> 99 + {{ range .Spindles }} 100 + <option 101 + value="{{ . }}" 102 + class="py-1" 103 + {{ if .eq . $.Repo.Spindle }} 104 + selected 105 + {{ end }} 106 + > 107 + {{ . }} 108 + </option> 109 + {{ end }} 110 + </select> 111 + <button class="btn my-2 flex gap-2 items-center" type="submit"> 112 + <span>save</span> 113 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 114 + </button> 115 + </div> 116 + </form> 117 + {{ end }} 118 + 119 {{ if .RepoInfo.Roles.RepoDeleteAllowed }} 120 <form 121 hx-confirm="Are you sure you want to delete this repository?" ··· 124 hx-indicator="#delete-repo-spinner" 125 > 126 <label for="branch">delete repository</label> 127 + <button class="btn my-2 flex items-center" type="text"> 128 <span>delete</span> 129 <span id="delete-repo-spinner" class="group"> 130 + {{ i "loader-circle" "pl-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 131 </span> 132 </button> 133 <span>
+99 -10
appview/repo/repo.go
··· 367 }) 368 return 369 case http.MethodPut: 370 - user := rp.oauth.GetUser(r) 371 newDescription := r.FormValue("description") 372 client, err := rp.oauth.AuthorizedClient(r) 373 if err != nil { ··· 753 return 754 } 755 756 func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 757 f, err := rp.repoResolver.Resolve(r) 758 if err != nil { ··· 794 } 795 796 if ksResp.StatusCode != http.StatusNoContent { 797 - w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err))) 798 return 799 } 800 801 tx, err := rp.db.BeginTx(r.Context(), nil) 802 if err != nil { 803 log.Println("failed to start tx") 804 - w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 805 return 806 } 807 defer func() { ··· 814 815 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 816 if err != nil { 817 - w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 818 return 819 } 820 821 err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 822 if err != nil { 823 - w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 824 return 825 } 826 ··· 838 return 839 } 840 841 - w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) 842 843 } 844 ··· 897 tx, err := rp.db.BeginTx(r.Context(), nil) 898 if err != nil { 899 log.Println("failed to start tx") 900 - w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 901 return 902 } 903 defer func() { ··· 988 return 989 } 990 991 - w.Write([]byte(fmt.Sprint("default branch set to: ", branch))) 992 } 993 994 func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) { ··· 1027 return 1028 } 1029 1030 rp.pages.RepoSettings(w, pages.RepoSettingsParams{ 1031 LoggedInUser: user, 1032 RepoInfo: f.RepoInfo(user), 1033 Collaborators: repoCollaborators, 1034 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 1035 Branches: result.Branches, 1036 }) 1037 } 1038 } ··· 1049 case http.MethodPost: 1050 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 1051 if err != nil { 1052 - rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", f.Knot)) 1053 return 1054 } 1055 ··· 1135 } 1136 secret, err := db.GetRegistrationKey(rp.db, knot) 1137 if err != nil { 1138 - rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", knot)) 1139 return 1140 } 1141
··· 367 }) 368 return 369 case http.MethodPut: 370 newDescription := r.FormValue("description") 371 client, err := rp.oauth.AuthorizedClient(r) 372 if err != nil { ··· 752 return 753 } 754 755 + // modify the spindle configured for this repo 756 + func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { 757 + f, err := rp.repoResolver.Resolve(r) 758 + if err != nil { 759 + log.Println("failed to get repo and knot", err) 760 + w.WriteHeader(http.StatusBadRequest) 761 + return 762 + } 763 + 764 + repoAt := f.RepoAt 765 + rkey := repoAt.RecordKey().String() 766 + if rkey == "" { 767 + log.Println("invalid aturi for repo", err) 768 + w.WriteHeader(http.StatusInternalServerError) 769 + return 770 + } 771 + 772 + user := rp.oauth.GetUser(r) 773 + 774 + newSpindle := r.FormValue("spindle") 775 + client, err := rp.oauth.AuthorizedClient(r) 776 + if err != nil { 777 + log.Println("failed to get client") 778 + rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.") 779 + return 780 + } 781 + 782 + // ensure that this is a valid spindle for this user 783 + validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 784 + if err != nil { 785 + log.Println("failed to get valid spindles") 786 + rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.") 787 + return 788 + } 789 + 790 + if !slices.Contains(validSpindles, newSpindle) { 791 + log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles) 792 + rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.") 793 + return 794 + } 795 + 796 + // optimistic update 797 + err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle) 798 + if err != nil { 799 + log.Println("failed to perform update-spindle query", err) 800 + rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.") 801 + return 802 + } 803 + 804 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 805 + if err != nil { 806 + // failed to get record 807 + rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.") 808 + return 809 + } 810 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 811 + Collection: tangled.RepoNSID, 812 + Repo: user.Did, 813 + Rkey: rkey, 814 + SwapRecord: ex.Cid, 815 + Record: &lexutil.LexiconTypeDecoder{ 816 + Val: &tangled.Repo{ 817 + Knot: f.Knot, 818 + Name: f.RepoName, 819 + Owner: user.Did, 820 + CreatedAt: f.CreatedAt, 821 + Description: &f.Description, 822 + Spindle: &newSpindle, 823 + }, 824 + }, 825 + }) 826 + 827 + if err != nil { 828 + log.Println("failed to perform update-spindle query", err) 829 + // failed to get record 830 + rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.") 831 + return 832 + } 833 + 834 + w.Write(fmt.Append(nil, "spindle set to: ", newSpindle)) 835 + } 836 + 837 func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 838 f, err := rp.repoResolver.Resolve(r) 839 if err != nil { ··· 875 } 876 877 if ksResp.StatusCode != http.StatusNoContent { 878 + w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err)) 879 return 880 } 881 882 tx, err := rp.db.BeginTx(r.Context(), nil) 883 if err != nil { 884 log.Println("failed to start tx") 885 + w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 886 return 887 } 888 defer func() { ··· 895 896 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 897 if err != nil { 898 + w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 899 return 900 } 901 902 err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 903 if err != nil { 904 + w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 905 return 906 } 907 ··· 919 return 920 } 921 922 + w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String())) 923 924 } 925 ··· 978 tx, err := rp.db.BeginTx(r.Context(), nil) 979 if err != nil { 980 log.Println("failed to start tx") 981 + w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 982 return 983 } 984 defer func() { ··· 1069 return 1070 } 1071 1072 + w.Write(fmt.Append(nil, "default branch set to: ", branch)) 1073 } 1074 1075 func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) { ··· 1108 return 1109 } 1110 1111 + spindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 1112 + if err != nil { 1113 + log.Println("failed to fetch spindles", err) 1114 + return 1115 + } 1116 + 1117 rp.pages.RepoSettings(w, pages.RepoSettingsParams{ 1118 LoggedInUser: user, 1119 RepoInfo: f.RepoInfo(user), 1120 Collaborators: repoCollaborators, 1121 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 1122 Branches: result.Branches, 1123 + Spindles: spindles, 1124 + CurrentSpindle: f.Spindle, 1125 }) 1126 } 1127 } ··· 1138 case http.MethodPost: 1139 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 1140 if err != nil { 1141 + rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot)) 1142 return 1143 } 1144 ··· 1224 } 1225 secret, err := db.GetRegistrationKey(rp.db, knot) 1226 if err != nil { 1227 + rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) 1228 return 1229 } 1230
+1
appview/repo/router.go
··· 70 }) 71 r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) { 72 r.Get("/", rp.RepoSettings) 73 r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator) 74 r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo) 75 r.Put("/branches/default", rp.SetDefaultBranch)
··· 70 }) 71 r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) { 72 r.Get("/", rp.RepoSettings) 73 + r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/spindle", rp.EditSpindle) 74 r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator) 75 r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo) 76 r.Put("/branches/default", rp.SetDefaultBranch)
+3
appview/reporesolver/resolver.go
··· 31 RepoName string 32 RepoAt syntax.ATURI 33 Description string 34 CreatedAt string 35 Ref string 36 CurrentDir string ··· 95 // pass through values from the middleware 96 description, ok := r.Context().Value("repoDescription").(string) 97 addedAt, ok := r.Context().Value("repoAddedAt").(string) 98 99 return &ResolvedRepo{ 100 Knot: knot, ··· 105 CreatedAt: addedAt, 106 Ref: ref, 107 CurrentDir: currentDir, 108 109 rr: rr, 110 }, nil
··· 31 RepoName string 32 RepoAt syntax.ATURI 33 Description string 34 + Spindle string 35 CreatedAt string 36 Ref string 37 CurrentDir string ··· 96 // pass through values from the middleware 97 description, ok := r.Context().Value("repoDescription").(string) 98 addedAt, ok := r.Context().Value("repoAddedAt").(string) 99 + spindle, ok := r.Context().Value("repoSpindle").(string) 100 101 return &ResolvedRepo{ 102 Knot: knot, ··· 107 CreatedAt: addedAt, 108 Ref: ref, 109 CurrentDir: currentDir, 110 + Spindle: spindle, 111 112 rr: rr, 113 }, nil