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}