forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

mvp: edit repo descriptions

- implemented in a backwards compatible way, introduces migrations and
updates an existing lexicon
- newly added field is not marked required, so we should still be able
to index old repo-create events properly

Changed files
+342 -71
api
tangled
appview
knotserver
lexicons
rbac
+1
api/tangled/tangledrepo.go
··· 19 type Repo struct { 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo" cborgen:"$type,const=sh.tangled.repo"` 21 AddedAt *string `json:"addedAt,omitempty" cborgen:"addedAt,omitempty"` 22 // knot: knot where the repo was created 23 Knot string `json:"knot" cborgen:"knot"` 24 // name: name of the repo
··· 19 type Repo struct { 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo" cborgen:"$type,const=sh.tangled.repo"` 21 AddedAt *string `json:"addedAt,omitempty" cborgen:"addedAt,omitempty"` 22 + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` 23 // knot: knot where the repo was created 24 Knot string `json:"knot" cborgen:"knot"` 25 // name: name of the repo
+1 -1
appview/auth/auth.go
··· 140 clientSession.Values[appview.SessionPds] = pdsEndpoint 141 clientSession.Values[appview.SessionAccessJwt] = atSessionish.GetAccessJwt() 142 clientSession.Values[appview.SessionRefreshJwt] = atSessionish.GetRefreshJwt() 143 - clientSession.Values[appview.SessionExpiry] = time.Now().Add(time.Second * 5).Format(time.RFC3339) 144 clientSession.Values[appview.SessionAuthenticated] = true 145 return clientSession.Save(r, w) 146 }
··· 140 clientSession.Values[appview.SessionPds] = pdsEndpoint 141 clientSession.Values[appview.SessionAccessJwt] = atSessionish.GetAccessJwt() 142 clientSession.Values[appview.SessionRefreshJwt] = atSessionish.GetRefreshJwt() 143 + clientSession.Values[appview.SessionExpiry] = time.Now().Add(time.Minute * 15).Format(time.RFC3339) 144 clientSession.Values[appview.SessionAuthenticated] = true 145 return clientSession.Save(r, w) 146 }
+53 -18
appview/db/repos.go
··· 6 ) 7 8 type Repo struct { 9 - Did string 10 - Name string 11 - Knot string 12 - Rkey string 13 - Created time.Time 14 - AtUri string 15 } 16 17 func GetAllRepos(e Execer, limit int) ([]Repo, error) { 18 var repos []Repo 19 20 rows, err := e.Query( 21 - `select did, name, knot, rkey, created 22 from repos 23 order by created desc 24 limit ? ··· 32 33 for rows.Next() { 34 var repo Repo 35 - err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Created) 36 if err != nil { 37 return nil, err 38 } ··· 49 func GetAllReposByDid(e Execer, did string) ([]Repo, error) { 50 var repos []Repo 51 52 - rows, err := e.Query(`select did, name, knot, rkey, created from repos where did = ?`, did) 53 if err != nil { 54 return nil, err 55 } ··· 57 58 for rows.Next() { 59 var repo Repo 60 - err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Created) 61 if err != nil { 62 return nil, err 63 } ··· 73 74 func GetRepo(e Execer, did, name string) (*Repo, error) { 75 var repo Repo 76 77 - row := e.QueryRow(`select did, name, knot, created, at_uri from repos where did = ? and name = ?`, did, name) 78 79 var createdAt string 80 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri); err != nil { 81 return nil, err 82 } 83 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 84 repo.Created = createdAtTime 85 86 return &repo, nil 87 } 88 89 func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { 90 var repo Repo 91 92 - row := e.QueryRow(`select did, name, knot, created, at_uri from repos where at_uri = ?`, atUri) 93 94 var createdAt string 95 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri); err != nil { 96 return nil, err 97 } 98 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 99 repo.Created = createdAtTime 100 101 return &repo, nil 102 } 103 104 func AddRepo(e Execer, repo *Repo) error { 105 - _, err := e.Exec(`insert into repos (did, name, knot, rkey, at_uri) values (?, ?, ?, ?, ?)`, repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri) 106 return err 107 } 108 ··· 119 return err 120 } 121 122 func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) { 123 var repos []Repo 124 ··· 130 131 for rows.Next() { 132 var repo Repo 133 - err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Created) 134 if err != nil { 135 return nil, err 136 } ··· 149 IssueCount IssueCount 150 } 151 152 - func scanRepo(rows *sql.Rows, did, name, knot, rkey *string, created *time.Time) error { 153 var createdAt string 154 - if err := rows.Scan(did, name, knot, rkey, &createdAt); err != nil { 155 return err 156 } 157 158 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
··· 6 ) 7 8 type Repo struct { 9 + Did string 10 + Name string 11 + Knot string 12 + Rkey string 13 + Created time.Time 14 + AtUri string 15 + Description string 16 } 17 18 func GetAllRepos(e Execer, limit int) ([]Repo, error) { 19 var repos []Repo 20 21 rows, err := e.Query( 22 + `select did, name, knot, rkey, description, created 23 from repos 24 order by created desc 25 limit ? ··· 33 34 for rows.Next() { 35 var repo Repo 36 + err := scanRepo( 37 + rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, 38 + ) 39 if err != nil { 40 return nil, err 41 } ··· 52 func GetAllReposByDid(e Execer, did string) ([]Repo, error) { 53 var repos []Repo 54 55 + rows, err := e.Query(`select did, name, knot, rkey, description, created from repos where did = ?`, did) 56 if err != nil { 57 return nil, err 58 } ··· 60 61 for rows.Next() { 62 var repo Repo 63 + err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created) 64 if err != nil { 65 return nil, err 66 } ··· 76 77 func GetRepo(e Execer, did, name string) (*Repo, error) { 78 var repo Repo 79 + var nullableDescription sql.NullString 80 81 + row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name) 82 83 var createdAt string 84 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 85 return nil, err 86 } 87 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 88 repo.Created = createdAtTime 89 90 + if nullableDescription.Valid { 91 + repo.Description = nullableDescription.String 92 + } else { 93 + repo.Description = "" 94 + } 95 + 96 return &repo, nil 97 } 98 99 func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { 100 var repo Repo 101 + var nullableDescription sql.NullString 102 103 + row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 104 105 var createdAt string 106 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 107 return nil, err 108 } 109 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 110 repo.Created = createdAtTime 111 + 112 + if nullableDescription.Valid { 113 + repo.Description = nullableDescription.String 114 + } else { 115 + repo.Description = "" 116 + } 117 118 return &repo, nil 119 } 120 121 func AddRepo(e Execer, repo *Repo) error { 122 + _, err := e.Exec( 123 + `insert into repos 124 + (did, name, knot, rkey, at_uri, description) 125 + values (?, ?, ?, ?, ?, ?)`, 126 + repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, 127 + ) 128 return err 129 } 130 ··· 141 return err 142 } 143 144 + func UpdateDescription(e Execer, repoAt, newDescription string) error { 145 + _, err := e.Exec( 146 + `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 147 + return err 148 + } 149 + 150 func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) { 151 var repos []Repo 152 ··· 158 159 for rows.Next() { 160 var repo Repo 161 + err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created) 162 if err != nil { 163 return nil, err 164 } ··· 177 IssueCount IssueCount 178 } 179 180 + func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time) error { 181 var createdAt string 182 + var nullableDescription sql.NullString 183 + if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt); err != nil { 184 return err 185 + } 186 + 187 + if nullableDescription.Valid { 188 + *description = nullableDescription.String 189 + } else { 190 + *description = "" 191 } 192 193 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
+35 -10
appview/pages/pages.go
··· 11 "net/http" 12 "path" 13 "path/filepath" 14 "strings" 15 16 "github.com/alecthomas/chroma/v2" ··· 194 return p.executePlain("fragments/star", w, params) 195 } 196 197 type RepoInfo struct { 198 - Name string 199 - OwnerDid string 200 - OwnerHandle string 201 - Description string 202 - Knot string 203 - RepoAt syntax.ATURI 204 - SettingsAllowed bool 205 - IsStarred bool 206 - Stats db.RepoStats 207 } 208 209 func (r RepoInfo) OwnerWithAt() string { ··· 225 {"pulls", "/pulls"}, 226 } 227 228 - if r.SettingsAllowed { 229 tabs = append(tabs, []string{"settings", "/settings"}) 230 } 231
··· 11 "net/http" 12 "path" 13 "path/filepath" 14 + "slices" 15 "strings" 16 17 "github.com/alecthomas/chroma/v2" ··· 195 return p.executePlain("fragments/star", w, params) 196 } 197 198 + type RepoDescriptionParams struct { 199 + RepoInfo RepoInfo 200 + } 201 + 202 + func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 203 + return p.executePlain("fragments/editRepoDescription", w, params) 204 + } 205 + 206 + func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 207 + return p.executePlain("fragments/repoDescription", w, params) 208 + } 209 + 210 type RepoInfo struct { 211 + Name string 212 + OwnerDid string 213 + OwnerHandle string 214 + Description string 215 + Knot string 216 + RepoAt syntax.ATURI 217 + IsStarred bool 218 + Stats db.RepoStats 219 + Roles RolesInRepo 220 + } 221 + 222 + type RolesInRepo struct { 223 + Roles []string 224 + } 225 + 226 + func (r RolesInRepo) SettingsAllowed() bool { 227 + return slices.Contains(r.Roles, "repo:settings") 228 + } 229 + 230 + func (r RolesInRepo) IsOwner() bool { 231 + return slices.Contains(r.Roles, "repo:owner") 232 } 233 234 func (r RepoInfo) OwnerWithAt() string { ··· 250 {"pulls", "/pulls"}, 251 } 252 253 + if r.Roles.SettingsAllowed() { 254 tabs = append(tabs, []string{"settings", "/settings"}) 255 } 256
+9
appview/pages/templates/fragments/editRepoDescription.html
···
··· 1 + {{ define "fragments/editRepoDescription" }} 2 + <form hx-put="/{{ .RepoInfo.FullName }}/description" hx-target="this" hx-swap="outerHTML"> 3 + <input type="text" name="description" value="{{ .RepoInfo.Description }}" class="input"> 4 + <button type="submit" class="btn">save</button> 5 + <button type="button" class="btn" hx-get="/{{ .RepoInfo.FullName }}/description" > 6 + cancel 7 + </button> 8 + </form> 9 + {{ end }}
+15
appview/pages/templates/fragments/repoDescription.html
···
··· 1 + {{ define "fragments/repoDescription" }} 2 + <span id="repo-description" hx-target="this" hx-swap="outerHTML"> 3 + {{ if .RepoInfo.Description }} 4 + {{ .RepoInfo.Description }} 5 + {{ else }} 6 + <span class="italic">this repo has no description</span> 7 + {{ end }} 8 + 9 + {{ if .RepoInfo.Roles.IsOwner }} 10 + <button class="btn" hx-get="/{{ .RepoInfo.FullName }}/description/edit"> 11 + edit 12 + </button> 13 + {{ end }} 14 + </span> 15 + {{ end }}
+1 -7
appview/pages/templates/layouts/repobase.html
··· 10 </p> 11 {{ template "fragments/star" .RepoInfo }} 12 </div> 13 - <span> 14 - {{ if .RepoInfo.Description }} 15 - {{ .RepoInfo.Description }} 16 - {{ else }} 17 - <span class="italic">this repo has no description</span> 18 - {{ end }} 19 - </span> 20 </section> 21 <section id="repo-links" class="min-h-screen flex flex-col drop-shadow-sm"> 22 <nav class="w-full mx-auto ml-4">
··· 10 </p> 11 {{ template "fragments/star" .RepoInfo }} 12 </div> 13 + {{ template "fragments/repoDescription" . }} 14 </section> 15 <section id="repo-links" class="min-h-screen flex flex-col drop-shadow-sm"> 16 <nav class="w-full mx-auto ml-4">
+9 -1
appview/pages/templates/repo/new.html
··· 17 /> 18 <p class="text-sm text-gray-500">All repositories are publicly visible.</p> 19 20 - <label for="name" class="block uppercase font-bold text-sm">Default branch</label> 21 <input 22 type="text" 23 id="branch" 24 name="branch" 25 value="main" 26 required 27 class="w-full max-w-md" 28 /> 29 </div>
··· 17 /> 18 <p class="text-sm text-gray-500">All repositories are publicly visible.</p> 19 20 + <label for="branch" class="block uppercase font-bold text-sm">Default branch</label> 21 <input 22 type="text" 23 id="branch" 24 name="branch" 25 value="main" 26 required 27 + class="w-full max-w-md" 28 + /> 29 + 30 + <label for="description" class="block uppercase font-bold text-sm">Description</label> 31 + <input 32 + type="text" 33 + id="description" 34 + name="description" 35 class="w-full max-w-md" 36 /> 37 </div>
+2
appview/state/middleware.go
··· 199 200 ctx := context.WithValue(req.Context(), "knot", repo.Knot) 201 ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 202 next.ServeHTTP(w, req.WithContext(ctx)) 203 }) 204 }
··· 199 200 ctx := context.WithValue(req.Context(), "knot", repo.Knot) 201 ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 202 + ctx = context.WithValue(ctx, "repoDescription", repo.Description) 203 + ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) 204 next.ServeHTTP(w, req.WithContext(ctx)) 205 }) 206 }
+124 -25
appview/state/repo.go
··· 130 return 131 } 132 133 func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 134 f, err := fullyResolvedRepo(r) 135 if err != nil { ··· 457 } 458 459 type FullyResolvedRepo struct { 460 - Knot string 461 - OwnerId identity.Identity 462 - RepoName string 463 - RepoAt syntax.ATURI 464 } 465 466 func (f *FullyResolvedRepo) OwnerDid() string { ··· 541 } 542 543 return pages.RepoInfo{ 544 - OwnerDid: f.OwnerDid(), 545 - OwnerHandle: f.OwnerHandle(), 546 - Name: f.RepoName, 547 - RepoAt: f.RepoAt, 548 - SettingsAllowed: settingsAllowed(s, u, f), 549 - IsStarred: isStarred, 550 - Knot: knot, 551 Stats: db.RepoStats{ 552 StarCount: starCount, 553 IssueCount: issueCount, ··· 953 return nil, fmt.Errorf("malformed middleware") 954 } 955 956 return &FullyResolvedRepo{ 957 - Knot: knot, 958 - OwnerId: id, 959 - RepoName: repoName, 960 - RepoAt: parsedRepoAt, 961 }, nil 962 } 963 964 - func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool { 965 - settingsAllowed := false 966 if u != nil { 967 - ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo()) 968 - if err == nil && ok { 969 - settingsAllowed = true 970 - } else { 971 - log.Println(err, ok) 972 - } 973 } 974 - 975 - return settingsAllowed 976 }
··· 130 return 131 } 132 133 + func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 134 + f, err := fullyResolvedRepo(r) 135 + if err != nil { 136 + log.Println("failed to get repo and knot", err) 137 + w.WriteHeader(http.StatusBadRequest) 138 + return 139 + } 140 + 141 + user := s.auth.GetUser(r) 142 + s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 143 + RepoInfo: f.RepoInfo(s, user), 144 + }) 145 + return 146 + } 147 + 148 + func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { 149 + f, err := fullyResolvedRepo(r) 150 + if err != nil { 151 + log.Println("failed to get repo and knot", err) 152 + w.WriteHeader(http.StatusBadRequest) 153 + return 154 + } 155 + 156 + repoAt := f.RepoAt 157 + rkey := repoAt.RecordKey().String() 158 + if rkey == "" { 159 + log.Println("invalid aturi for repo", err) 160 + w.WriteHeader(http.StatusInternalServerError) 161 + return 162 + } 163 + 164 + user := s.auth.GetUser(r) 165 + 166 + switch r.Method { 167 + case http.MethodGet: 168 + s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 169 + RepoInfo: f.RepoInfo(s, user), 170 + }) 171 + return 172 + case http.MethodPut: 173 + user := s.auth.GetUser(r) 174 + newDescription := r.FormValue("description") 175 + client, _ := s.auth.AuthorizedClient(r) 176 + 177 + // optimistic update 178 + err = db.UpdateDescription(s.db, string(repoAt), newDescription) 179 + if err != nil { 180 + log.Println("failed to perferom update-description query", err) 181 + s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 182 + return 183 + } 184 + 185 + // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 186 + // 187 + // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests 188 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey) 189 + if err != nil { 190 + // failed to get record 191 + s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 192 + return 193 + } 194 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 195 + Collection: tangled.RepoNSID, 196 + Repo: user.Did, 197 + Rkey: rkey, 198 + SwapRecord: ex.Cid, 199 + Record: &lexutil.LexiconTypeDecoder{ 200 + Val: &tangled.Repo{ 201 + Knot: f.Knot, 202 + Name: f.RepoName, 203 + Owner: user.Did, 204 + AddedAt: &f.AddedAt, 205 + Description: &newDescription, 206 + }, 207 + }, 208 + }) 209 + 210 + if err != nil { 211 + log.Println("failed to perferom update-description query", err) 212 + // failed to get record 213 + s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.") 214 + return 215 + } 216 + 217 + newRepoInfo := f.RepoInfo(s, user) 218 + newRepoInfo.Description = newDescription 219 + 220 + s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 221 + RepoInfo: newRepoInfo, 222 + }) 223 + return 224 + } 225 + } 226 + 227 func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 228 f, err := fullyResolvedRepo(r) 229 if err != nil { ··· 551 } 552 553 type FullyResolvedRepo struct { 554 + Knot string 555 + OwnerId identity.Identity 556 + RepoName string 557 + RepoAt syntax.ATURI 558 + Description string 559 + AddedAt string 560 } 561 562 func (f *FullyResolvedRepo) OwnerDid() string { ··· 637 } 638 639 return pages.RepoInfo{ 640 + OwnerDid: f.OwnerDid(), 641 + OwnerHandle: f.OwnerHandle(), 642 + Name: f.RepoName, 643 + RepoAt: f.RepoAt, 644 + Description: f.Description, 645 + IsStarred: isStarred, 646 + Knot: knot, 647 + Roles: rolesInRepo(s, u, f), 648 Stats: db.RepoStats{ 649 StarCount: starCount, 650 IssueCount: issueCount, ··· 1050 return nil, fmt.Errorf("malformed middleware") 1051 } 1052 1053 + // pass through values from the middleware 1054 + description, ok := r.Context().Value("repoDescription").(string) 1055 + addedAt, ok := r.Context().Value("repoAddedAt").(string) 1056 + 1057 return &FullyResolvedRepo{ 1058 + Knot: knot, 1059 + OwnerId: id, 1060 + RepoName: repoName, 1061 + RepoAt: parsedRepoAt, 1062 + Description: description, 1063 + AddedAt: addedAt, 1064 }, nil 1065 } 1066 1067 + func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 1068 if u != nil { 1069 + r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) 1070 + log.Println(r) 1071 + return pages.RolesInRepo{r} 1072 + } else { 1073 + return pages.RolesInRepo{} 1074 } 1075 }
+22 -5
appview/state/signer.go
··· 69 Endpoint = "/init" 70 ) 71 72 - body, _ := json.Marshal(map[string]interface{}{ 73 "did": did, 74 }) 75 ··· 87 Endpoint = "/repo/new" 88 ) 89 90 - body, _ := json.Marshal(map[string]interface{}{ 91 "did": did, 92 "name": repoName, 93 "default_branch": defaultBranch, 94 }) 95 96 - fmt.Println(body) 97 98 req, err := s.newRequest(Method, Endpoint, body) 99 if err != nil { ··· 109 Endpoint = "/member/add" 110 ) 111 112 - body, _ := json.Marshal(map[string]interface{}{ 113 "did": did, 114 }) 115 ··· 127 ) 128 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 129 130 - body, _ := json.Marshal(map[string]interface{}{ 131 "did": memberDid, 132 }) 133
··· 69 Endpoint = "/init" 70 ) 71 72 + body, _ := json.Marshal(map[string]any{ 73 "did": did, 74 }) 75 ··· 87 Endpoint = "/repo/new" 88 ) 89 90 + body, _ := json.Marshal(map[string]any{ 91 "did": did, 92 "name": repoName, 93 "default_branch": defaultBranch, 94 }) 95 96 + req, err := s.newRequest(Method, Endpoint, body) 97 + if err != nil { 98 + return nil, err 99 + } 100 + 101 + return s.client.Do(req) 102 + } 103 + 104 + func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) { 105 + const ( 106 + Method = "DELETE" 107 + Endpoint = "/repo" 108 + ) 109 + 110 + body, _ := json.Marshal(map[string]any{ 111 + "did": did, 112 + "name": repoName, 113 + }) 114 115 req, err := s.newRequest(Method, Endpoint, body) 116 if err != nil { ··· 126 Endpoint = "/member/add" 127 ) 128 129 + body, _ := json.Marshal(map[string]any{ 130 "did": did, 131 }) 132 ··· 144 ) 145 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 146 147 + body, _ := json.Marshal(map[string]any{ 148 "did": memberDid, 149 }) 150
+14 -4
appview/state/state.go
··· 581 defaultBranch = "main" 582 } 583 584 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 585 if err != nil || !ok { 586 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") ··· 607 608 rkey := s.TID() 609 repo := &db.Repo{ 610 - Did: user.Did, 611 - Name: repoName, 612 - Knot: domain, 613 - Rkey: rkey, 614 } 615 616 xrpcClient, _ := s.auth.AuthorizedClient(r) ··· 647 if err != nil { 648 log.Println("failed to rollback policies") 649 } 650 }() 651 652 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) ··· 875 // settings routes, needs auth 876 r.Group(func(r chi.Router) { 877 r.Use(AuthMiddleware(s)) 878 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 879 r.Get("/", s.RepoSettings) 880 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
··· 581 defaultBranch = "main" 582 } 583 584 + description := r.FormValue("description") 585 + 586 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 587 if err != nil || !ok { 588 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") ··· 609 610 rkey := s.TID() 611 repo := &db.Repo{ 612 + Did: user.Did, 613 + Name: repoName, 614 + Knot: domain, 615 + Rkey: rkey, 616 + Description: description, 617 } 618 619 xrpcClient, _ := s.auth.AuthorizedClient(r) ··· 650 if err != nil { 651 log.Println("failed to rollback policies") 652 } 653 + client.RemoveRepo(user.Did, repoName) 654 }() 655 656 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) ··· 879 // settings routes, needs auth 880 r.Group(func(r chi.Router) { 881 r.Use(AuthMiddleware(s)) 882 + // repo description can only be edited by owner 883 + r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { 884 + r.Put("/", s.RepoDescription) 885 + r.Get("/", s.RepoDescription) 886 + r.Get("/edit", s.RepoDescriptionEdit) 887 + }) 888 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 889 r.Get("/", s.RepoSettings) 890 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
+1
knotserver/handler.go
··· 96 r.Route("/repo", func(r chi.Router) { 97 r.Use(h.VerifySignature) 98 r.Put("/new", h.NewRepo) 99 }) 100 101 r.Route("/member", func(r chi.Router) {
··· 96 r.Route("/repo", func(r chi.Router) { 97 r.Use(h.VerifySignature) 98 r.Put("/new", h.NewRepo) 99 + r.Delete("/", h.RemoveRepo) 100 }) 101 102 r.Route("/member", func(r chi.Router) {
+35
knotserver/routes.go
··· 10 "fmt" 11 "log" 12 "net/http" 13 "path/filepath" 14 "strconv" 15 "strings" ··· 514 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 515 if err != nil { 516 l.Error("adding repo permissions", "error", err.Error()) 517 writeError(w, err.Error(), http.StatusInternalServerError) 518 return 519 }
··· 10 "fmt" 11 "log" 12 "net/http" 13 + "os" 14 "path/filepath" 15 "strconv" 16 "strings" ··· 515 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 516 if err != nil { 517 l.Error("adding repo permissions", "error", err.Error()) 518 + writeError(w, err.Error(), http.StatusInternalServerError) 519 + return 520 + } 521 + 522 + w.WriteHeader(http.StatusNoContent) 523 + } 524 + 525 + func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 526 + l := h.l.With("handler", "RemoveRepo") 527 + 528 + data := struct { 529 + Did string `json:"did"` 530 + Name string `json:"name"` 531 + }{} 532 + 533 + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 534 + writeError(w, "invalid request body", http.StatusBadRequest) 535 + return 536 + } 537 + 538 + did := data.Did 539 + name := data.Name 540 + 541 + if did == "" || name == "" { 542 + l.Error("invalid request body, empty did or name") 543 + w.WriteHeader(http.StatusBadRequest) 544 + return 545 + } 546 + 547 + relativeRepoPath := filepath.Join(did, name) 548 + repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 549 + err := os.RemoveAll(repoPath) 550 + if err != nil { 551 + l.Error("removing repo", "error", err.Error()) 552 writeError(w, err.Error(), http.StatusInternalServerError) 553 return 554 }
+6
lexicons/repo.json
··· 26 "addedAt": { 27 "type": "string", 28 "format": "datetime" 29 } 30 } 31 }
··· 26 "addedAt": { 27 "type": "string", 28 "format": "datetime" 29 + }, 30 + "description": { 31 + "type": "string", 32 + "format": "datetime", 33 + "minLength": 1, 34 + "maxLength": 140 35 } 36 } 37 }
+14
rbac/rbac.go
··· 165 return e.E.Enforce(user, domain, repo, "repo:settings") 166 } 167 168 func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) { 169 return e.E.Enforce(user, domain, repo, "repo:invite") 170 }
··· 165 return e.E.Enforce(user, domain, repo, "repo:settings") 166 } 167 168 + // given a repo, what permissions does this user have? repo:owner? repo:invite? etc. 169 + func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string { 170 + var permissions []string 171 + res := e.E.GetPermissionsForUserInDomain(user, domain) 172 + for _, p := range res { 173 + // get only permissions for this resource/repo 174 + if p[2] == repo { 175 + permissions = append(permissions, p[3]) 176 + } 177 + } 178 + 179 + return permissions 180 + } 181 + 182 func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) { 183 return e.E.Enforce(user, domain, repo, "repo:invite") 184 }