Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

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

+341 -71
api/tangled/tangledrepo.go

This is a binary file and will not be displayed.

+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 ? ··· 33 32 34 33 for rows.Next() { 35 34 var repo Repo 36 - err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Created) 35 + err := scanRepo( 36 + rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, 37 + ) 37 38 if err != nil { 38 39 return nil, err 39 40 } ··· 52 49 func GetAllReposByDid(e Execer, did string) ([]Repo, error) { 53 50 var repos []Repo 54 51 55 - rows, err := e.Query(`select did, name, knot, rkey, created from repos where did = ?`, did) 52 + rows, err := e.Query(`select did, name, knot, rkey, description, created from repos where did = ?`, did) 56 53 if err != nil { 57 54 return nil, err 58 55 } ··· 60 57 61 58 for rows.Next() { 62 59 var repo Repo 63 - err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Created) 60 + err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created) 64 61 if err != nil { 65 62 return nil, err 66 63 } ··· 76 73 77 74 func GetRepo(e Execer, did, name string) (*Repo, error) { 78 75 var repo Repo 76 + var nullableDescription sql.NullString 79 77 80 - row := e.QueryRow(`select did, name, knot, created, at_uri from repos where did = ? and name = ?`, did, name) 78 + row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name) 81 79 82 80 var createdAt string 83 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri); err != nil { 81 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 84 82 return nil, err 85 83 } 86 84 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 87 85 repo.Created = createdAtTime 86 + 87 + if nullableDescription.Valid { 88 + repo.Description = nullableDescription.String 89 + } else { 90 + repo.Description = "" 91 + } 88 92 89 93 return &repo, nil 90 94 } 91 95 92 96 func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { 93 97 var repo Repo 98 + var nullableDescription sql.NullString 94 99 95 - row := e.QueryRow(`select did, name, knot, created, at_uri from repos where at_uri = ?`, atUri) 100 + row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 96 101 97 102 var createdAt string 98 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri); err != nil { 103 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 99 104 return nil, err 100 105 } 101 106 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 102 107 repo.Created = createdAtTime 103 108 109 + if nullableDescription.Valid { 110 + repo.Description = nullableDescription.String 111 + } else { 112 + repo.Description = "" 113 + } 114 + 104 115 return &repo, nil 105 116 } 106 117 107 118 func AddRepo(e Execer, repo *Repo) error { 108 - _, err := e.Exec(`insert into repos (did, name, knot, rkey, at_uri) values (?, ?, ?, ?, ?)`, repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri) 119 + _, err := e.Exec( 120 + `insert into repos 121 + (did, name, knot, rkey, at_uri, description) 122 + values (?, ?, ?, ?, ?, ?)`, 123 + repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, 124 + ) 109 125 return err 110 126 } 111 127 ··· 141 119 return err 142 120 } 143 121 122 + func UpdateDescription(e Execer, repoAt, newDescription string) error { 123 + _, err := e.Exec( 124 + `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 125 + return err 126 + } 127 + 144 128 func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) { 145 129 var repos []Repo 146 130 ··· 158 130 159 131 for rows.Next() { 160 132 var repo Repo 161 - err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Created) 133 + err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created) 162 134 if err != nil { 163 135 return nil, err 164 136 } ··· 177 149 IssueCount IssueCount 178 150 } 179 151 180 - func scanRepo(rows *sql.Rows, did, name, knot, rkey *string, created *time.Time) error { 152 + func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time) error { 181 153 var createdAt string 182 - if err := rows.Scan(did, name, knot, rkey, &createdAt); err != nil { 154 + var nullableDescription sql.NullString 155 + if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt); err != nil { 183 156 return err 157 + } 158 + 159 + if nullableDescription.Valid { 160 + *description = nullableDescription.String 161 + } else { 162 + *description = "" 184 163 } 185 164 186 165 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" ··· 195 194 return p.executePlain("fragments/star", w, params) 196 195 } 197 196 197 + type RepoDescriptionParams struct { 198 + RepoInfo RepoInfo 199 + } 200 + 201 + func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 202 + return p.executePlain("fragments/editRepoDescription", w, params) 203 + } 204 + 205 + func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 206 + return p.executePlain("fragments/repoDescription", w, params) 207 + } 208 + 198 209 type RepoInfo struct { 199 - Name string 200 - OwnerDid string 201 - OwnerHandle string 202 - Description string 203 - Knot string 204 - RepoAt syntax.ATURI 205 - SettingsAllowed bool 206 - IsStarred bool 207 - Stats db.RepoStats 210 + Name string 211 + OwnerDid string 212 + OwnerHandle string 213 + Description string 214 + Knot string 215 + RepoAt syntax.ATURI 216 + IsStarred bool 217 + Stats db.RepoStats 218 + Roles RolesInRepo 219 + } 220 + 221 + type RolesInRepo struct { 222 + Roles []string 223 + } 224 + 225 + func (r RolesInRepo) SettingsAllowed() bool { 226 + return slices.Contains(r.Roles, "repo:settings") 227 + } 228 + 229 + func (r RolesInRepo) IsOwner() bool { 230 + return slices.Contains(r.Roles, "repo:owner") 208 231 } 209 232 210 233 func (r RepoInfo) OwnerWithAt() string { ··· 250 225 {"pulls", "/pulls"}, 251 226 } 252 227 253 - if r.SettingsAllowed { 228 + if r.Roles.SettingsAllowed() { 254 229 tabs = append(tabs, []string{"settings", "/settings"}) 255 230 } 256 231
+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 { ··· 551 457 } 552 458 553 459 type FullyResolvedRepo struct { 554 - Knot string 555 - OwnerId identity.Identity 556 - RepoName string 557 - RepoAt syntax.ATURI 460 + Knot string 461 + OwnerId identity.Identity 462 + RepoName string 463 + RepoAt syntax.ATURI 464 + Description string 465 + AddedAt string 558 466 } 559 467 560 468 func (f *FullyResolvedRepo) OwnerDid() string { ··· 637 541 } 638 542 639 543 return pages.RepoInfo{ 640 - OwnerDid: f.OwnerDid(), 641 - OwnerHandle: f.OwnerHandle(), 642 - Name: f.RepoName, 643 - RepoAt: f.RepoAt, 644 - SettingsAllowed: settingsAllowed(s, u, f), 645 - IsStarred: isStarred, 646 - Knot: knot, 544 + OwnerDid: f.OwnerDid(), 545 + OwnerHandle: f.OwnerHandle(), 546 + Name: f.RepoName, 547 + RepoAt: f.RepoAt, 548 + Description: f.Description, 549 + IsStarred: isStarred, 550 + Knot: knot, 551 + Roles: rolesInRepo(s, u, f), 647 552 Stats: db.RepoStats{ 648 553 StarCount: starCount, 649 554 IssueCount: issueCount, ··· 1050 953 return nil, fmt.Errorf("malformed middleware") 1051 954 } 1052 955 956 + // pass through values from the middleware 957 + description, ok := r.Context().Value("repoDescription").(string) 958 + addedAt, ok := r.Context().Value("repoAddedAt").(string) 959 + 1053 960 return &FullyResolvedRepo{ 1054 - Knot: knot, 1055 - OwnerId: id, 1056 - RepoName: repoName, 1057 - RepoAt: parsedRepoAt, 961 + Knot: knot, 962 + OwnerId: id, 963 + RepoName: repoName, 964 + RepoAt: parsedRepoAt, 965 + Description: description, 966 + AddedAt: addedAt, 1058 967 }, nil 1059 968 } 1060 969 1061 - func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool { 1062 - settingsAllowed := false 970 + func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 1063 971 if u != nil { 1064 - ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo()) 1065 - if err == nil && ok { 1066 - settingsAllowed = true 1067 - } else { 1068 - log.Println(err, ok) 1069 - } 972 + r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) 973 + log.Println(r) 974 + return pages.RolesInRepo{r} 975 + } else { 976 + return pages.RolesInRepo{} 1070 977 } 1071 - 1072 - return settingsAllowed 1073 978 }
+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 { ··· 126 109 Endpoint = "/member/add" 127 110 ) 128 111 129 - body, _ := json.Marshal(map[string]interface{}{ 112 + body, _ := json.Marshal(map[string]any{ 130 113 "did": did, 131 114 }) 132 115 ··· 144 127 ) 145 128 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 146 129 147 - body, _ := json.Marshal(map[string]interface{}{ 130 + body, _ := json.Marshal(map[string]any{ 148 131 "did": memberDid, 149 132 }) 150 133
+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.") ··· 609 607 610 608 rkey := s.TID() 611 609 repo := &db.Repo{ 612 - Did: user.Did, 613 - Name: repoName, 614 - Knot: domain, 615 - Rkey: rkey, 610 + Did: user.Did, 611 + Name: repoName, 612 + Knot: domain, 613 + Rkey: rkey, 614 + Description: description, 616 615 } 617 616 618 617 xrpcClient, _ := s.auth.AuthorizedClient(r) ··· 650 647 if err != nil { 651 648 log.Println("failed to rollback policies") 652 649 } 650 + client.RemoveRepo(user.Did, repoName) 653 651 }() 654 652 655 653 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) ··· 879 875 // settings routes, needs auth 880 876 r.Group(func(r chi.Router) { 881 877 r.Use(AuthMiddleware(s)) 878 + // repo description can only be edited by owner 879 + r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { 880 + r.Put("/", s.RepoDescription) 881 + r.Get("/", s.RepoDescription) 882 + r.Get("/edit", s.RepoDescriptionEdit) 883 + }) 882 884 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 883 885 r.Get("/", s.RepoSettings) 884 886 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" ··· 515 514 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 516 515 if err != nil { 517 516 l.Error("adding repo permissions", "error", err.Error()) 517 + writeError(w, err.Error(), http.StatusInternalServerError) 518 + return 519 + } 520 + 521 + w.WriteHeader(http.StatusNoContent) 522 + } 523 + 524 + func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 525 + l := h.l.With("handler", "RemoveRepo") 526 + 527 + data := struct { 528 + Did string `json:"did"` 529 + Name string `json:"name"` 530 + }{} 531 + 532 + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 533 + writeError(w, "invalid request body", http.StatusBadRequest) 534 + return 535 + } 536 + 537 + did := data.Did 538 + name := data.Name 539 + 540 + if did == "" || name == "" { 541 + l.Error("invalid request body, empty did or name") 542 + w.WriteHeader(http.StatusBadRequest) 543 + return 544 + } 545 + 546 + relativeRepoPath := filepath.Join(did, name) 547 + repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 548 + err := os.RemoveAll(repoPath) 549 + if err != nil { 550 + l.Error("removing repo", "error", err.Error()) 518 551 writeError(w, err.Error(), http.StatusInternalServerError) 519 552 return 520 553 }
+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 }