arche / internal/archesrv/handlers_settings.go

commit 154431fd
  1package archesrv
  2
  3import (
  4	"fmt"
  5	"net/http"
  6	"strings"
  7)
  8
  9func (s *forgeServer) requireRepoAdmin(w http.ResponseWriter, r *http.Request) (*RepoRecord, *User, bool) {
 10	repoName := r.PathValue("repo")
 11	rec, err := s.db.GetRepo(repoName)
 12	if err != nil || rec == nil {
 13		http.NotFound(w, r)
 14		return nil, nil, false
 15	}
 16	user := s.db.currentUser(r)
 17	if user == nil {
 18		http.Error(w, "login required", http.StatusUnauthorized)
 19		return nil, nil, false
 20	}
 21	if !user.IsAdmin && !s.db.hasRole(rec.ID, user.ID, "admin") {
 22		http.Error(w, "forbidden", http.StatusForbidden)
 23		return nil, nil, false
 24	}
 25	return rec, user, true
 26}
 27
 28type srvWebhooksData struct {
 29	Repo     string
 30	User     *User
 31	Webhooks []WebhookRecord
 32	Error    string
 33}
 34
 35func (s *forgeServer) handleRepoWebhooks(w http.ResponseWriter, r *http.Request) {
 36	rec, user, ok := s.requireRepoAdmin(w, r)
 37	if !ok {
 38		return
 39	}
 40	hooks, err := s.db.ListWebhooks(rec.ID)
 41	if err != nil {
 42		http.Error(w, "list webhooks: "+err.Error(), http.StatusInternalServerError)
 43		return
 44	}
 45	s.render(w, "srv_repo_webhooks.html", srvWebhooksData{Repo: rec.Name, User: user, Webhooks: hooks})
 46}
 47
 48func (s *forgeServer) handleRepoCreateWebhook(w http.ResponseWriter, r *http.Request) {
 49	rec, user, ok := s.requireRepoAdmin(w, r)
 50	if !ok {
 51		return
 52	}
 53	r.ParseForm() //nolint:errcheck
 54	hookURL := strings.TrimSpace(r.FormValue("url"))
 55	secret := r.FormValue("secret")
 56	events := r.FormValue("events")
 57	if events == "" {
 58		events = "push"
 59	}
 60	if hookURL == "" {
 61		hooks, _ := s.db.ListWebhooks(rec.ID)
 62		s.render(w, "srv_repo_webhooks.html", srvWebhooksData{Repo: rec.Name, User: user, Webhooks: hooks, Error: "URL required"})
 63		return
 64	}
 65	if _, err := s.db.CreateWebhook(rec.ID, hookURL, secret, events); err != nil {
 66		http.Error(w, "create webhook: "+err.Error(), http.StatusInternalServerError)
 67		return
 68	}
 69	http.Redirect(w, r, "/"+rec.Name+"/settings/webhooks", http.StatusFound)
 70}
 71
 72func (s *forgeServer) handleRepoDeleteWebhook(w http.ResponseWriter, r *http.Request) {
 73	rec, _, ok := s.requireRepoAdmin(w, r)
 74	if !ok {
 75		return
 76	}
 77	var id int64
 78	if _, err := fmt.Sscan(r.PathValue("id"), &id); err != nil {
 79		http.Error(w, "invalid id", http.StatusBadRequest)
 80		return
 81	}
 82	hook, err := s.db.GetWebhook(id)
 83	if err != nil || hook == nil || hook.RepoID != rec.ID {
 84		http.Error(w, "webhook not found", http.StatusNotFound)
 85		return
 86	}
 87	if err := s.db.DeleteWebhook(id); err != nil {
 88		http.Error(w, "delete webhook: "+err.Error(), http.StatusInternalServerError)
 89		return
 90	}
 91	w.WriteHeader(http.StatusNoContent)
 92}
 93
 94type srvDeliveriesData struct {
 95	Repo       string
 96	User       *User
 97	WebhookID  int64
 98	WebhookURL string
 99	Deliveries []WebhookDelivery
100}
101
102func (s *forgeServer) handleWebhookDeliveries(w http.ResponseWriter, r *http.Request) {
103	rec, user, ok := s.requireRepoAdmin(w, r)
104	if !ok {
105		return
106	}
107	var id int64
108	if _, err := fmt.Sscan(r.PathValue("id"), &id); err != nil {
109		http.Error(w, "invalid id", http.StatusBadRequest)
110		return
111	}
112	hook, err := s.db.GetWebhook(id)
113	if err != nil || hook == nil || hook.RepoID != rec.ID {
114		http.Error(w, "webhook not found", http.StatusNotFound)
115		return
116	}
117	deliveries, err := s.db.ListDeliveries(id)
118	if err != nil {
119		http.Error(w, "list deliveries: "+err.Error(), http.StatusInternalServerError)
120		return
121	}
122	s.render(w, "srv_webhook_deliveries.html", srvDeliveriesData{
123		Repo:       rec.Name,
124		User:       user,
125		WebhookID:  id,
126		WebhookURL: hook.URL,
127		Deliveries: deliveries,
128	})
129}
130
131func (s *forgeServer) handleWebhookReplay(w http.ResponseWriter, r *http.Request) {
132	rec, _, ok := s.requireRepoAdmin(w, r)
133	if !ok {
134		return
135	}
136	var hookID, deliveryID int64
137	if _, err := fmt.Sscan(r.PathValue("id"), &hookID); err != nil {
138		http.Error(w, "invalid webhook id", http.StatusBadRequest)
139		return
140	}
141	if _, err := fmt.Sscan(r.PathValue("delivery"), &deliveryID); err != nil {
142		http.Error(w, "invalid delivery id", http.StatusBadRequest)
143		return
144	}
145	hook, err := s.db.GetWebhook(hookID)
146	if err != nil || hook == nil || hook.RepoID != rec.ID {
147		http.Error(w, "webhook not found", http.StatusNotFound)
148		return
149	}
150	if err := s.db.ReplayDelivery(deliveryID); err != nil {
151		http.Error(w, "replay: "+err.Error(), http.StatusInternalServerError)
152		return
153	}
154	http.Redirect(w, r, "/"+rec.Name+"/settings/webhooks/"+r.PathValue("id")+"/deliveries", http.StatusFound)
155}
156
157type srvSettingsKeysData struct {
158	User  *User
159	Keys  []SSHKey
160	Error string
161}
162
163func (s *forgeServer) handleSettingsKeys(w http.ResponseWriter, r *http.Request) {
164	user := s.db.currentUser(r)
165	if user == nil {
166		http.Redirect(w, r, "/login", http.StatusFound)
167		return
168	}
169	keys, err := s.db.ListSSHKeys(user.ID)
170	if err != nil {
171		http.Error(w, "list keys: "+err.Error(), http.StatusInternalServerError)
172		return
173	}
174	s.render(w, "srv_settings_keys.html", srvSettingsKeysData{User: user, Keys: keys})
175}
176
177func (s *forgeServer) handleSettingsAddKey(w http.ResponseWriter, r *http.Request) {
178	user := s.db.currentUser(r)
179	if user == nil {
180		http.Redirect(w, r, "/login", http.StatusFound)
181		return
182	}
183	r.ParseForm() //nolint:errcheck
184	label := strings.TrimSpace(r.FormValue("label"))
185	publicKey := strings.TrimSpace(r.FormValue("public_key"))
186	if publicKey == "" {
187		keys, _ := s.db.ListSSHKeys(user.ID)
188		s.render(w, "srv_settings_keys.html", srvSettingsKeysData{User: user, Keys: keys, Error: "public key required"})
189		return
190	}
191	if _, err := s.db.AddSSHKey(user.ID, label, publicKey); err != nil {
192		keys, _ := s.db.ListSSHKeys(user.ID)
193		s.render(w, "srv_settings_keys.html", srvSettingsKeysData{User: user, Keys: keys, Error: "invalid key: " + err.Error()})
194		return
195	}
196	http.Redirect(w, r, "/settings/keys", http.StatusFound)
197}
198
199func (s *forgeServer) handleSettingsDeleteKey(w http.ResponseWriter, r *http.Request) {
200	user := s.db.currentUser(r)
201	if user == nil {
202		http.Error(w, "login required", http.StatusUnauthorized)
203		return
204	}
205	var id int64
206	if _, err := fmt.Sscan(r.PathValue("id"), &id); err != nil {
207		http.Error(w, "invalid id", http.StatusBadRequest)
208		return
209	}
210	if err := s.db.DeleteSSHKey(id, user.ID); err != nil {
211		http.Error(w, "delete key: "+err.Error(), http.StatusInternalServerError)
212		return
213	}
214	w.WriteHeader(http.StatusNoContent)
215}
216
217type srvSettingsMTLSData struct {
218	User  *User
219	Certs []MTLSCert
220	Error string
221}
222
223func (s *forgeServer) handleSettingsMTLSCerts(w http.ResponseWriter, r *http.Request) {
224	user := s.db.currentUser(r)
225	if user == nil {
226		http.Redirect(w, r, "/login", http.StatusFound)
227		return
228	}
229	certs, err := s.db.ListMTLSCerts(user.ID)
230	if err != nil {
231		http.Error(w, "list certs: "+err.Error(), http.StatusInternalServerError)
232		return
233	}
234	s.render(w, "srv_settings_mtls.html", srvSettingsMTLSData{User: user, Certs: certs})
235}
236
237func (s *forgeServer) handleSettingsAddMTLSCert(w http.ResponseWriter, r *http.Request) {
238	user := s.db.currentUser(r)
239	if user == nil {
240		http.Redirect(w, r, "/login", http.StatusFound)
241		return
242	}
243	r.ParseForm() //nolint:errcheck
244	label := strings.TrimSpace(r.FormValue("label"))
245	certPEM := strings.TrimSpace(r.FormValue("cert_pem"))
246	if certPEM == "" {
247		certs, _ := s.db.ListMTLSCerts(user.ID)
248		s.render(w, "srv_settings_mtls.html", srvSettingsMTLSData{User: user, Certs: certs, Error: "certificate PEM required"})
249		return
250	}
251	if _, err := s.db.AddMTLSCert(user.ID, label, certPEM); err != nil {
252		certs, _ := s.db.ListMTLSCerts(user.ID)
253		s.render(w, "srv_settings_mtls.html", srvSettingsMTLSData{User: user, Certs: certs, Error: "invalid cert: " + err.Error()})
254		return
255	}
256	http.Redirect(w, r, "/settings/mtls", http.StatusFound)
257}
258
259func (s *forgeServer) handleSettingsDeleteMTLSCert(w http.ResponseWriter, r *http.Request) {
260	user := s.db.currentUser(r)
261	if user == nil {
262		http.Error(w, "login required", http.StatusUnauthorized)
263		return
264	}
265	var id int64
266	if _, err := fmt.Sscan(r.PathValue("id"), &id); err != nil {
267		http.Error(w, "invalid id", http.StatusBadRequest)
268		return
269	}
270	if err := s.db.DeleteMTLSCert(id, user.ID); err != nil {
271		http.Error(w, "delete cert: "+err.Error(), http.StatusInternalServerError)
272		return
273	}
274	w.WriteHeader(http.StatusNoContent)
275}
276
277type srvSettingsTokenData struct {
278	User     *User
279	Tokens   []APIToken
280	NewToken string
281	Error    string
282}
283
284func (s *forgeServer) handleSettingsToken(w http.ResponseWriter, r *http.Request) {
285	user := s.db.currentUser(r)
286	if user == nil {
287		http.Redirect(w, r, "/login", http.StatusFound)
288		return
289	}
290	tokens, err := s.db.ListAPITokens(user.ID)
291	if err != nil {
292		http.Error(w, "list tokens: "+err.Error(), http.StatusInternalServerError)
293		return
294	}
295	newTok := r.URL.Query().Get("new")
296	s.render(w, "srv_settings_token.html", srvSettingsTokenData{User: user, Tokens: tokens, NewToken: newTok})
297}
298
299func (s *forgeServer) handleSettingsCreateToken(w http.ResponseWriter, r *http.Request) {
300	user := s.db.currentUser(r)
301	if user == nil {
302		http.Redirect(w, r, "/login", http.StatusFound)
303		return
304	}
305	r.ParseForm() //nolint:errcheck
306	label := strings.TrimSpace(r.FormValue("label"))
307	tok, err := s.db.CreateAPIToken(user.ID, label)
308	if err != nil {
309		tokens, _ := s.db.ListAPITokens(user.ID)
310		s.render(w, "srv_settings_token.html", srvSettingsTokenData{User: user, Tokens: tokens, Error: "create token: " + err.Error()})
311		return
312	}
313	http.Redirect(w, r, "/settings/token?new="+tok, http.StatusFound)
314}
315
316func (s *forgeServer) handleSettingsDeleteToken(w http.ResponseWriter, r *http.Request) {
317	user := s.db.currentUser(r)
318	if user == nil {
319		http.Error(w, "login required", http.StatusUnauthorized)
320		return
321	}
322	var id int64
323	if _, err := fmt.Sscan(r.PathValue("id"), &id); err != nil {
324		http.Error(w, "invalid id", http.StatusBadRequest)
325		return
326	}
327	if err := s.db.DeleteAPIToken(id, user.ID); err != nil {
328		http.Error(w, "delete token: "+err.Error(), http.StatusInternalServerError)
329		return
330	}
331	w.WriteHeader(http.StatusNoContent)
332}
333
334type srvRepoSettingsData struct {
335	Repo          string
336	User          *User
337	Description   string
338	Visibility    string
339	Collaborators []CollabEntry
340	Error         string
341}
342
343func (s *forgeServer) handleRepoSettingsPage(w http.ResponseWriter, r *http.Request) {
344	rec, user, ok := s.requireRepoAdmin(w, r)
345	if !ok {
346		return
347	}
348	collabs, err := s.db.ListCollaborators(rec.ID)
349	if err != nil {
350		http.Error(w, "list collaborators: "+err.Error(), http.StatusInternalServerError)
351		return
352	}
353	s.render(w, "srv_repo_settings.html", srvRepoSettingsData{
354		Repo:          rec.Name,
355		User:          user,
356		Description:   rec.Description,
357		Visibility:    rec.Visibility,
358		Collaborators: collabs,
359	})
360}
361
362func (s *forgeServer) handleRepoUpdateSettings(w http.ResponseWriter, r *http.Request) {
363	rec, user, ok := s.requireRepoAdmin(w, r)
364	if !ok {
365		return
366	}
367	r.ParseForm() //nolint:errcheck
368	description := strings.TrimSpace(r.FormValue("description"))
369	visibility := r.FormValue("visibility")
370
371	if err := s.db.UpdateRepo(rec.Name, description, visibility); err != nil {
372		collabs, _ := s.db.ListCollaborators(rec.ID)
373		s.render(w, "srv_repo_settings.html", srvRepoSettingsData{
374			Repo:          rec.Name,
375			User:          user,
376			Description:   description,
377			Visibility:    visibility,
378			Collaborators: collabs,
379			Error:         "update failed: " + err.Error(),
380		})
381		return
382	}
383	http.Redirect(w, r, "/"+rec.Name+"/settings", http.StatusFound)
384}
385
386func (s *forgeServer) handleRepoAddCollaborator(w http.ResponseWriter, r *http.Request) {
387	rec, user, ok := s.requireRepoAdmin(w, r)
388	if !ok {
389		return
390	}
391	r.ParseForm() //nolint:errcheck
392	username := strings.TrimSpace(r.FormValue("username"))
393	role := r.FormValue("role")
394	if role != "read" && role != "write" && role != "admin" {
395		role = "read"
396	}
397
398	renderErr := func(msg string) {
399		collabs, _ := s.db.ListCollaborators(rec.ID)
400		s.render(w, "srv_repo_settings.html", srvRepoSettingsData{
401			Repo:          rec.Name,
402			User:          user,
403			Description:   rec.Description,
404			Visibility:    rec.Visibility,
405			Collaborators: collabs,
406			Error:         msg,
407		})
408	}
409
410	if username == "" {
411		renderErr("username required")
412		return
413	}
414	target, _, err := s.db.GetUserByName(username)
415	if err != nil || target == nil {
416		renderErr("user not found: " + username)
417		return
418	}
419	if err := s.db.SetPermission(rec.ID, target.ID, role); err != nil {
420		renderErr("set permission: " + err.Error())
421		return
422	}
423
424	if role == "write" || role == "admin" {
425		allowed, _, _ := s.db.GetRepoHookConfig(rec.ID)
426		if allowed {
427			_ = s.db.SetRepoAllowShellHooks(rec.ID, false, "")
428			s.log.Warn("allow_shell_hooks revoked", "repo", rec.Name, "collaborator", username, "role", role)
429		}
430	}
431
432	http.Redirect(w, r, "/"+rec.Name+"/settings", http.StatusFound)
433}
434
435func (s *forgeServer) handleRepoRemoveCollaborator(w http.ResponseWriter, r *http.Request) {
436	rec, _, ok := s.requireRepoAdmin(w, r)
437	if !ok {
438		return
439	}
440	var userID int64
441	if _, err := fmt.Sscan(r.PathValue("id"), &userID); err != nil {
442		http.Error(w, "invalid id", http.StatusBadRequest)
443		return
444	}
445	if err := s.db.RemovePermission(rec.ID, userID); err != nil {
446		http.Error(w, "remove permission: "+err.Error(), http.StatusInternalServerError)
447		return
448	}
449	w.WriteHeader(http.StatusNoContent)
450}
451
452func (s *forgeServer) handleRepoDeleteRepo(w http.ResponseWriter, r *http.Request) {
453	rec, _, ok := s.requireRepoAdmin(w, r)
454	if !ok {
455		return
456	}
457	if err := s.db.DeleteRepo(rec.Name); err != nil {
458		http.Error(w, "delete repo: "+err.Error(), http.StatusInternalServerError)
459		return
460	}
461	w.WriteHeader(http.StatusNoContent)
462}