Commit c086411ce4

c086411ce422436eb1e6a1f57c3c77117c7df8b0

parent: 9d96712a69

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-18 02:31 UTC

httpd: settings topics gated on admin access, dashboard drops dead Pinned

settingsSubmit's topics case resolved the repository with RepoByPath and
answered "Saved the topics." before any policy check ran, reachable by
any signed-in user. It now resolves through repoForUser(policyCanAdmin)
at the top of the function like settingsForm does, and uses that row's
ID. When the remove half of a topics save succeeds and the add half
fails, the re-render's message now says the removals already applied.

dashboard's PinnedRepos loop, per-repo AccessRole calls and Pinned field
were dead: the rail's pinned list is built in railFor, not here.

fieldLabel: require-mr and archive read as full phrases instead of a
raw flag name.

Ref #218
internal/httpd/settings.go +16 −7
@@ -82,6 +82,10 @@ func (s *Server) settingsRedirect(w http.ResponseWriter, r *http.Request, msg st
8282// settingsSubmit routes one form to its command. Keeping the mapping in
8383// one place makes what the page can reach obvious.
8484func (s *Server) settingsSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
85 row, ok := s.repoForUser(w, r, u, policyCanAdmin)
86 if !ok {
87 return
88 }
8589 repo := r.PathValue("owner") + "/" + r.PathValue("repo")
8690 v := func(k string) string { return strings.TrimSpace(r.FormValue(k)) }
8791 field := r.FormValue("field")
@@ -131,11 +135,6 @@ func (s *Server) settingsSubmit(w http.ResponseWriter, r *http.Request, u store.
131135 }
132136 argv = []string{"repo", verb, repo}
133137 case "topics":
134 row, err := s.st.RepoByPath(repo)
135 if err != nil {
136 http.NotFound(w, r)
137 return
138 }
139138 want := map[string]bool{}
140139 var order []string
141140 for _, t := range strings.Split(v("topics"), ",") {
@@ -160,11 +159,13 @@ func (s *Server) settingsSubmit(w http.ResponseWriter, r *http.Request, u store.
160159 remove = append(remove, t)
161160 }
162161 }
162 removed := false
163163 if len(remove) > 0 {
164164 if _, msg, ok := s.runControl(u, append([]string{"repo", "topics", "remove", repo}, remove...)); !ok {
165165 s.settingsFormWith(w, r, u, msg, r.Form)
166166 return
167167 }
168 removed = true
168169 }
169170 if len(add) > 0 {
170171 argv = append([]string{"repo", "topics", "add", repo}, add...)
@@ -172,6 +173,14 @@ func (s *Server) settingsSubmit(w http.ResponseWriter, r *http.Request, u store.
172173 s.settingsRedirect(w, r, "Saved the topics.")
173174 return
174175 }
176 if removed {
177 if _, msg, ok := s.runControl(u, argv); !ok {
178 s.settingsFormWith(w, r, u, "Removed "+strings.Join(remove, ", ")+"; "+msg, r.Form)
179 return
180 }
181 s.settingsRedirect(w, r, "Saved the "+fieldLabel(field)+".")
182 return
183 }
175184 case "runner-add":
176185 body := v("key")
177186 if body == "" {
@@ -223,7 +232,7 @@ func fieldLabel(field string) string {
223232 case "require-codeowners":
224233 return "CODEOWNERS"
225234 case "require-mr":
226 return "require-MR"
235 return "merge request requirement"
227236 case "require-signed":
228237 return "signed commits"
229238 case "protect", "unprotect":
@@ -233,7 +242,7 @@ func fieldLabel(field string) string {
233242 case "deps":
234243 return "dependency scanning"
235244 case "archive":
236 return "archive"
245 return "archived state"
237246 case "topics":
238247 return "topics"
239248 case "runner-add", "runner-remove":
internal/httpd/web.go +1 −10
@@ -193,14 +193,6 @@ var landingPicture = func() bool {
193193}()
194194
195195func (s *Server) dashboard(w http.ResponseWriter, r *http.Request, viewer store.User) {
196 pinned, _ := s.st.PinnedRepos(viewer.ID)
197 var visible []store.Repo
198 for _, rp := range pinned {
199 grant, _ := s.st.AccessRole(rp.ID, viewer.ID)
200 if policy.CanRead(viewer, rp, grant) {
201 visible = append(visible, rp)
202 }
203 }
204196 mrs, _ := s.st.DashboardMRs(viewer.ID)
205197 issues, _ := s.st.DashboardIssues(viewer.ID)
206198 reviews, _ := s.st.ReviewQueue(viewer.ID)
@@ -208,13 +200,12 @@ func (s *Server) dashboard(w http.ResponseWriter, r *http.Request, viewer store.
208200 events, _ := s.st.RecentEvents(viewer.ID, 20, 0)
209201 s.render(w, "dashboard.html", struct {
210202 basePage
211 Pinned []store.Repo
212203 Reviews []store.DashboardItem
213204 Assigned []store.DashboardItem
214205 MRs []store.DashboardItem
215206 Issues []store.DashboardItem
216207 Feed []feedLine
217 }{s.baseFor(viewer), visible, reviews, assigned, mrs, issues, feedLines(events)})
208 }{s.baseFor(viewer), reviews, assigned, mrs, issues, feedLines(events)})
218209}
219210
220211func (s *Server) explore(w http.ResponseWriter, r *http.Request) {