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 455 }) 456 456 db.Exec("pragma foreign_keys = on;") 457 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 + 458 466 return &DB{db}, nil 459 467 } 460 468
+23 -7
appview/db/repos.go
··· 18 18 Created time.Time 19 19 AtUri string 20 20 Description string 21 + Spindle string 21 22 22 23 // optionally, populate this when querying for reverse mappings 23 24 RepoStats *RepoStats ··· 138 139 139 140 func GetRepo(e Execer, did, name string) (*Repo, error) { 140 141 var repo Repo 141 - var nullableDescription sql.NullString 142 + var description, spindle sql.NullString 142 143 143 - row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name) 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 + ) 144 152 145 153 var createdAt string 146 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 154 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil { 147 155 return nil, err 148 156 } 149 157 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 150 158 repo.Created = createdAtTime 151 159 152 - if nullableDescription.Valid { 153 - repo.Description = nullableDescription.String 154 - } else { 155 - repo.Description = "" 160 + if description.Valid { 161 + repo.Description = description.String 162 + } 163 + 164 + if spindle.Valid { 165 + repo.Spindle = spindle.String 156 166 } 157 167 158 168 return &repo, nil ··· 302 312 func UpdateDescription(e Execer, repoAt, newDescription string) error { 303 313 _, err := e.Exec( 304 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) 305 321 return err 306 322 } 307 323
+1
appview/middleware/middleware.go
··· 225 225 ctx := context.WithValue(req.Context(), "knot", repo.Knot) 226 226 ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 227 227 ctx = context.WithValue(ctx, "repoDescription", repo.Description) 228 + ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle) 228 229 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) 229 230 next.ServeHTTP(w, req.WithContext(ctx)) 230 231 })
+7 -5
appview/pages/pages.go
··· 616 616 } 617 617 618 618 type RepoSettingsParams struct { 619 - LoggedInUser *oauth.User 620 - RepoInfo repoinfo.RepoInfo 621 - Collaborators []Collaborator 622 - Active string 623 - Branches []types.Branch 619 + LoggedInUser *oauth.User 620 + RepoInfo repoinfo.RepoInfo 621 + Collaborators []Collaborator 622 + Active string 623 + Branches []types.Branch 624 + Spindles []string 625 + CurrentSpindle string 624 626 // TODO: use repoinfo.roles 625 627 IsCollaboratorInviteAllowed bool 626 628 }
+37 -2
appview/pages/templates/repo/settings.html
··· 81 81 </div> 82 82 </form> 83 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 + 84 119 {{ if .RepoInfo.Roles.RepoDeleteAllowed }} 85 120 <form 86 121 hx-confirm="Are you sure you want to delete this repository?" ··· 89 124 hx-indicator="#delete-repo-spinner" 90 125 > 91 126 <label for="branch">delete repository</label> 92 - <button class="btn my-2 flex gap-2 items-center" type="text"> 127 + <button class="btn my-2 flex items-center" type="text"> 93 128 <span>delete</span> 94 129 <span id="delete-repo-spinner" class="group"> 95 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 130 + {{ i "loader-circle" "pl-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 96 131 </span> 97 132 </button> 98 133 <span>
+99 -10
appview/repo/repo.go
··· 367 367 }) 368 368 return 369 369 case http.MethodPut: 370 - user := rp.oauth.GetUser(r) 371 370 newDescription := r.FormValue("description") 372 371 client, err := rp.oauth.AuthorizedClient(r) 373 372 if err != nil { ··· 753 752 return 754 753 } 755 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 + 756 837 func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 757 838 f, err := rp.repoResolver.Resolve(r) 758 839 if err != nil { ··· 794 875 } 795 876 796 877 if ksResp.StatusCode != http.StatusNoContent { 797 - w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err))) 878 + w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err)) 798 879 return 799 880 } 800 881 801 882 tx, err := rp.db.BeginTx(r.Context(), nil) 802 883 if err != nil { 803 884 log.Println("failed to start tx") 804 - w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 885 + w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 805 886 return 806 887 } 807 888 defer func() { ··· 814 895 815 896 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 816 897 if err != nil { 817 - w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 898 + w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 818 899 return 819 900 } 820 901 821 902 err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 822 903 if err != nil { 823 - w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 904 + w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 824 905 return 825 906 } 826 907 ··· 838 919 return 839 920 } 840 921 841 - w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) 922 + w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String())) 842 923 843 924 } 844 925 ··· 897 978 tx, err := rp.db.BeginTx(r.Context(), nil) 898 979 if err != nil { 899 980 log.Println("failed to start tx") 900 - w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 981 + w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 901 982 return 902 983 } 903 984 defer func() { ··· 988 1069 return 989 1070 } 990 1071 991 - w.Write([]byte(fmt.Sprint("default branch set to: ", branch))) 1072 + w.Write(fmt.Append(nil, "default branch set to: ", branch)) 992 1073 } 993 1074 994 1075 func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) { ··· 1027 1108 return 1028 1109 } 1029 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 + 1030 1117 rp.pages.RepoSettings(w, pages.RepoSettingsParams{ 1031 1118 LoggedInUser: user, 1032 1119 RepoInfo: f.RepoInfo(user), 1033 1120 Collaborators: repoCollaborators, 1034 1121 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 1035 1122 Branches: result.Branches, 1123 + Spindles: spindles, 1124 + CurrentSpindle: f.Spindle, 1036 1125 }) 1037 1126 } 1038 1127 } ··· 1049 1138 case http.MethodPost: 1050 1139 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 1051 1140 if err != nil { 1052 - rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", f.Knot)) 1141 + rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot)) 1053 1142 return 1054 1143 } 1055 1144 ··· 1135 1224 } 1136 1225 secret, err := db.GetRegistrationKey(rp.db, knot) 1137 1226 if err != nil { 1138 - rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", knot)) 1227 + rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) 1139 1228 return 1140 1229 } 1141 1230
+1
appview/repo/router.go
··· 70 70 }) 71 71 r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) { 72 72 r.Get("/", rp.RepoSettings) 73 + r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/spindle", rp.EditSpindle) 73 74 r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator) 74 75 r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo) 75 76 r.Put("/branches/default", rp.SetDefaultBranch)
+3
appview/reporesolver/resolver.go
··· 31 31 RepoName string 32 32 RepoAt syntax.ATURI 33 33 Description string 34 + Spindle string 34 35 CreatedAt string 35 36 Ref string 36 37 CurrentDir string ··· 95 96 // pass through values from the middleware 96 97 description, ok := r.Context().Value("repoDescription").(string) 97 98 addedAt, ok := r.Context().Value("repoAddedAt").(string) 99 + spindle, ok := r.Context().Value("repoSpindle").(string) 98 100 99 101 return &ResolvedRepo{ 100 102 Knot: knot, ··· 105 107 CreatedAt: addedAt, 106 108 Ref: ref, 107 109 CurrentDir: currentDir, 110 + Spindle: spindle, 108 111 109 112 rr: rr, 110 113 }, nil