forked from tangled.org/core
Monorepo for Tangled

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 19 type Repo struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo" cborgen:"$type,const=sh.tangled.repo"` 21 21 AddedAt *string `json:"addedAt,omitempty" cborgen:"addedAt,omitempty"` 22 + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` 22 23 // knot: knot where the repo was created 23 24 Knot string `json:"knot" cborgen:"knot"` 24 25 // name: name of the repo
+1 -1
appview/auth/auth.go
··· 140 140 clientSession.Values[appview.SessionPds] = pdsEndpoint 141 141 clientSession.Values[appview.SessionAccessJwt] = atSessionish.GetAccessJwt() 142 142 clientSession.Values[appview.SessionRefreshJwt] = atSessionish.GetRefreshJwt() 143 - clientSession.Values[appview.SessionExpiry] = time.Now().Add(time.Second * 5).Format(time.RFC3339) 143 + clientSession.Values[appview.SessionExpiry] = time.Now().Add(time.Minute * 15).Format(time.RFC3339) 144 144 clientSession.Values[appview.SessionAuthenticated] = true 145 145 return clientSession.Save(r, w) 146 146 }
+53 -18
appview/db/repos.go
··· 6 6 ) 7 7 8 8 type Repo struct { 9 - Did string 10 - Name string 11 - Knot string 12 - Rkey string 13 - Created time.Time 14 - AtUri string 9 + Did string 10 + Name string 11 + Knot string 12 + Rkey string 13 + Created time.Time 14 + AtUri string 15 + Description string 15 16 } 16 17 17 18 func GetAllRepos(e Execer, limit int) ([]Repo, error) { 18 19 var repos []Repo 19 20 20 21 rows, err := e.Query( 21 - `select did, name, knot, rkey, created 22 + `select did, name, knot, rkey, description, created 22 23 from repos 23 24 order by created desc 24 25 limit ? ··· 32 33 33 34 for rows.Next() { 34 35 var repo Repo 35 - err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Created) 36 + err := scanRepo( 37 + rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, 38 + ) 36 39 if err != nil { 37 40 return nil, err 38 41 } ··· 49 52 func GetAllReposByDid(e Execer, did string) ([]Repo, error) { 50 53 var repos []Repo 51 54 52 - rows, err := e.Query(`select did, name, knot, rkey, created from repos where did = ?`, did) 55 + rows, err := e.Query(`select did, name, knot, rkey, description, created from repos where did = ?`, did) 53 56 if err != nil { 54 57 return nil, err 55 58 } ··· 57 60 58 61 for rows.Next() { 59 62 var repo Repo 60 - err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Created) 63 + err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created) 61 64 if err != nil { 62 65 return nil, err 63 66 } ··· 73 76 74 77 func GetRepo(e Execer, did, name string) (*Repo, error) { 75 78 var repo Repo 79 + var nullableDescription sql.NullString 76 80 77 - row := e.QueryRow(`select did, name, knot, created, at_uri from repos where did = ? and name = ?`, did, name) 81 + row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name) 78 82 79 83 var createdAt string 80 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri); err != nil { 84 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 81 85 return nil, err 82 86 } 83 87 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 84 88 repo.Created = createdAtTime 85 89 90 + if nullableDescription.Valid { 91 + repo.Description = nullableDescription.String 92 + } else { 93 + repo.Description = "" 94 + } 95 + 86 96 return &repo, nil 87 97 } 88 98 89 99 func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { 90 100 var repo Repo 101 + var nullableDescription sql.NullString 91 102 92 - row := e.QueryRow(`select did, name, knot, created, at_uri from repos where at_uri = ?`, atUri) 103 + row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 93 104 94 105 var createdAt string 95 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri); err != nil { 106 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 96 107 return nil, err 97 108 } 98 109 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 99 110 repo.Created = createdAtTime 111 + 112 + if nullableDescription.Valid { 113 + repo.Description = nullableDescription.String 114 + } else { 115 + repo.Description = "" 116 + } 100 117 101 118 return &repo, nil 102 119 } 103 120 104 121 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) 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 + ) 106 128 return err 107 129 } 108 130 ··· 119 141 return err 120 142 } 121 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 + 122 150 func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) { 123 151 var repos []Repo 124 152 ··· 130 158 131 159 for rows.Next() { 132 160 var repo Repo 133 - err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Created) 161 + err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created) 134 162 if err != nil { 135 163 return nil, err 136 164 } ··· 149 177 IssueCount IssueCount 150 178 } 151 179 152 - func scanRepo(rows *sql.Rows, did, name, knot, rkey *string, created *time.Time) error { 180 + func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time) error { 153 181 var createdAt string 154 - if err := rows.Scan(did, name, knot, rkey, &createdAt); err != nil { 182 + var nullableDescription sql.NullString 183 + if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt); err != nil { 155 184 return err 185 + } 186 + 187 + if nullableDescription.Valid { 188 + *description = nullableDescription.String 189 + } else { 190 + *description = "" 156 191 } 157 192 158 193 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
+35 -10
appview/pages/pages.go
··· 11 11 "net/http" 12 12 "path" 13 13 "path/filepath" 14 + "slices" 14 15 "strings" 15 16 16 17 "github.com/alecthomas/chroma/v2" ··· 194 195 return p.executePlain("fragments/star", w, params) 195 196 } 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 + 197 210 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 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") 207 232 } 208 233 209 234 func (r RepoInfo) OwnerWithAt() string { ··· 225 250 {"pulls", "/pulls"}, 226 251 } 227 252 228 - if r.SettingsAllowed { 253 + if r.Roles.SettingsAllowed() { 229 254 tabs = append(tabs, []string{"settings", "/settings"}) 230 255 } 231 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 10 </p> 11 11 {{ template "fragments/star" .RepoInfo }} 12 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> 13 + {{ template "fragments/repoDescription" . }} 20 14 </section> 21 15 <section id="repo-links" class="min-h-screen flex flex-col drop-shadow-sm"> 22 16 <nav class="w-full mx-auto ml-4">
+9 -1
appview/pages/templates/repo/new.html
··· 17 17 /> 18 18 <p class="text-sm text-gray-500">All repositories are publicly visible.</p> 19 19 20 - <label for="name" class="block uppercase font-bold text-sm">Default branch</label> 20 + <label for="branch" class="block uppercase font-bold text-sm">Default branch</label> 21 21 <input 22 22 type="text" 23 23 id="branch" 24 24 name="branch" 25 25 value="main" 26 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" 27 35 class="w-full max-w-md" 28 36 /> 29 37 </div>
+2
appview/state/middleware.go
··· 199 199 200 200 ctx := context.WithValue(req.Context(), "knot", repo.Knot) 201 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)) 202 204 next.ServeHTTP(w, req.WithContext(ctx)) 203 205 }) 204 206 }
+124 -25
appview/state/repo.go
··· 130 130 return 131 131 } 132 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 + 133 227 func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 134 228 f, err := fullyResolvedRepo(r) 135 229 if err != nil { ··· 457 551 } 458 552 459 553 type FullyResolvedRepo struct { 460 - Knot string 461 - OwnerId identity.Identity 462 - RepoName string 463 - RepoAt syntax.ATURI 554 + Knot string 555 + OwnerId identity.Identity 556 + RepoName string 557 + RepoAt syntax.ATURI 558 + Description string 559 + AddedAt string 464 560 } 465 561 466 562 func (f *FullyResolvedRepo) OwnerDid() string { ··· 541 637 } 542 638 543 639 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, 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), 551 648 Stats: db.RepoStats{ 552 649 StarCount: starCount, 553 650 IssueCount: issueCount, ··· 953 1050 return nil, fmt.Errorf("malformed middleware") 954 1051 } 955 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 + 956 1057 return &FullyResolvedRepo{ 957 - Knot: knot, 958 - OwnerId: id, 959 - RepoName: repoName, 960 - RepoAt: parsedRepoAt, 1058 + Knot: knot, 1059 + OwnerId: id, 1060 + RepoName: repoName, 1061 + RepoAt: parsedRepoAt, 1062 + Description: description, 1063 + AddedAt: addedAt, 961 1064 }, nil 962 1065 } 963 1066 964 - func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool { 965 - settingsAllowed := false 1067 + func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 966 1068 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 - } 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{} 973 1074 } 974 - 975 - return settingsAllowed 976 1075 }
+22 -5
appview/state/signer.go
··· 69 69 Endpoint = "/init" 70 70 ) 71 71 72 - body, _ := json.Marshal(map[string]interface{}{ 72 + body, _ := json.Marshal(map[string]any{ 73 73 "did": did, 74 74 }) 75 75 ··· 87 87 Endpoint = "/repo/new" 88 88 ) 89 89 90 - body, _ := json.Marshal(map[string]interface{}{ 90 + body, _ := json.Marshal(map[string]any{ 91 91 "did": did, 92 92 "name": repoName, 93 93 "default_branch": defaultBranch, 94 94 }) 95 95 96 - fmt.Println(body) 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 + }) 97 114 98 115 req, err := s.newRequest(Method, Endpoint, body) 99 116 if err != nil { ··· 109 126 Endpoint = "/member/add" 110 127 ) 111 128 112 - body, _ := json.Marshal(map[string]interface{}{ 129 + body, _ := json.Marshal(map[string]any{ 113 130 "did": did, 114 131 }) 115 132 ··· 127 144 ) 128 145 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 129 146 130 - body, _ := json.Marshal(map[string]interface{}{ 147 + body, _ := json.Marshal(map[string]any{ 131 148 "did": memberDid, 132 149 }) 133 150
+14 -4
appview/state/state.go
··· 581 581 defaultBranch = "main" 582 582 } 583 583 584 + description := r.FormValue("description") 585 + 584 586 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 585 587 if err != nil || !ok { 586 588 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") ··· 607 609 608 610 rkey := s.TID() 609 611 repo := &db.Repo{ 610 - Did: user.Did, 611 - Name: repoName, 612 - Knot: domain, 613 - Rkey: rkey, 612 + Did: user.Did, 613 + Name: repoName, 614 + Knot: domain, 615 + Rkey: rkey, 616 + Description: description, 614 617 } 615 618 616 619 xrpcClient, _ := s.auth.AuthorizedClient(r) ··· 647 650 if err != nil { 648 651 log.Println("failed to rollback policies") 649 652 } 653 + client.RemoveRepo(user.Did, repoName) 650 654 }() 651 655 652 656 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) ··· 875 879 // settings routes, needs auth 876 880 r.Group(func(r chi.Router) { 877 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 + }) 878 888 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 879 889 r.Get("/", s.RepoSettings) 880 890 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
+1
knotserver/handler.go
··· 96 96 r.Route("/repo", func(r chi.Router) { 97 97 r.Use(h.VerifySignature) 98 98 r.Put("/new", h.NewRepo) 99 + r.Delete("/", h.RemoveRepo) 99 100 }) 100 101 101 102 r.Route("/member", func(r chi.Router) {
+35
knotserver/routes.go
··· 10 10 "fmt" 11 11 "log" 12 12 "net/http" 13 + "os" 13 14 "path/filepath" 14 15 "strconv" 15 16 "strings" ··· 514 515 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 515 516 if err != nil { 516 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()) 517 552 writeError(w, err.Error(), http.StatusInternalServerError) 518 553 return 519 554 }
+6
lexicons/repo.json
··· 26 26 "addedAt": { 27 27 "type": "string", 28 28 "format": "datetime" 29 + }, 30 + "description": { 31 + "type": "string", 32 + "format": "datetime", 33 + "minLength": 1, 34 + "maxLength": 140 29 35 } 30 36 } 31 37 }
+14
rbac/rbac.go
··· 165 165 return e.E.Enforce(user, domain, repo, "repo:settings") 166 166 } 167 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 + 168 182 func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) { 169 183 return e.E.Enforce(user, domain, repo, "repo:invite") 170 184 }