This repository has no description
1package repo
2
3import (
4 "net/http"
5 "strconv"
6 "strings"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 "github.com/go-chi/chi/v5"
10 "tangled.org/core/appview/db"
11 "tangled.org/core/appview/models"
12 "tangled.org/core/appview/pages"
13)
14
15// webhookEventFields maps event checkbox form fields to webhook events
16var webhookEventFields = []struct {
17 field string
18 event models.WebhookEvent
19}{
20 {"event_push", models.WebhookEventPush},
21 {"event_repo_renamed", models.WebhookEventRepoRenamed},
22 {"event_pull_request_created", models.WebhookEventPullRequestCreated},
23 {"event_pull_request_resubmitted", models.WebhookEventPullRequestResubmitted},
24 {"event_pull_request_merged", models.WebhookEventPullRequestMerged},
25 {"event_pull_request_closed", models.WebhookEventPullRequestClosed},
26 {"event_pull_request_reopened", models.WebhookEventPullRequestReopened},
27}
28
29func webhookEventsFromForm(r *http.Request) []string {
30 events := []string{}
31 for _, ef := range webhookEventFields {
32 if r.FormValue(ef.field) == "on" {
33 events = append(events, string(ef.event))
34 }
35 }
36 return events
37}
38
39// Webhooks displays the webhooks settings page
40func (rp *Repo) Webhooks(w http.ResponseWriter, r *http.Request) {
41 l := rp.logger.With("handler", "Webhooks")
42
43 f, err := rp.repoResolver.Resolve(r)
44 if err != nil {
45 l.Error("failed to get repo and knot", "err", err)
46 w.WriteHeader(http.StatusBadRequest)
47 return
48 }
49
50 user := rp.oauth.GetMultiAccountUser(r)
51
52 webhooks, err := db.GetWebhooksForRepo(rp.db, f.RepoDid)
53 if err != nil {
54 l.Error("failed to get webhooks", "err", err)
55 rp.pages.Notice(w, "webhooks-error", "Failed to load webhooks")
56 return
57 }
58
59 // fetch recent deliveries for each webhook
60 deliveriesMap := make(map[int64][]models.WebhookDelivery)
61 for _, webhook := range webhooks {
62 deliveries, err := db.GetWebhookDeliveries(rp.db, webhook.Id, 4)
63 if err != nil {
64 l.Error("failed to get webhook deliveries", "webhook_id", webhook.Id, "err", err)
65 // continue even if we can't get deliveries for one webhook
66 continue
67 }
68 deliveriesMap[webhook.Id] = deliveries
69 }
70
71 rp.pages.RepoWebhooksSettings(w, pages.RepoWebhooksSettingsParams{
72 BaseParams: pages.BaseParamsFromContext(r.Context()),
73 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
74 Webhooks: webhooks,
75 WebhookDeliveries: deliveriesMap,
76 })
77}
78
79// AddWebhook creates a new webhook
80func (rp *Repo) AddWebhook(w http.ResponseWriter, r *http.Request) {
81 l := rp.logger.With("handler", "AddWebhook")
82
83 f, err := rp.repoResolver.Resolve(r)
84 if err != nil {
85 l.Error("failed to get repo and knot", "err", err)
86 w.WriteHeader(http.StatusBadRequest)
87 return
88 }
89
90 url := strings.TrimSpace(r.FormValue("url"))
91 if url == "" {
92 rp.pages.Notice(w, "webhooks-error", "Webhook URL is required")
93 return
94 }
95
96 if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
97 rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://")
98 return
99 }
100
101 secret := strings.TrimSpace(r.FormValue("secret"))
102 // if secret is empty, we don't sign
103
104 active := r.FormValue("active") == "on"
105
106 events := webhookEventsFromForm(r)
107 if len(events) == 0 {
108 rp.pages.Notice(w, "webhooks-error", "At least one event must be enabled")
109 return
110 }
111
112 webhook := &models.Webhook{
113 RepoDid: syntax.DID(f.RepoDid),
114 Url: url,
115 Secret: secret,
116 Active: active,
117 Events: events,
118 }
119
120 tx, err := rp.db.Begin()
121 if err != nil {
122 l.Error("failed to start transaction", "err", err)
123 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook")
124 return
125 }
126 defer tx.Rollback()
127
128 if err := db.AddWebhook(tx, webhook); err != nil {
129 l.Error("failed to add webhook", "err", err)
130 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook")
131 return
132 }
133
134 if err := tx.Commit(); err != nil {
135 l.Error("failed to commit transaction", "err", err)
136 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook")
137 return
138 }
139
140 rp.pages.HxRefresh(w)
141}
142
143// UpdateWebhook updates an existing webhook
144func (rp *Repo) UpdateWebhook(w http.ResponseWriter, r *http.Request) {
145 l := rp.logger.With("handler", "UpdateWebhook")
146
147 f, err := rp.repoResolver.Resolve(r)
148 if err != nil {
149 l.Error("failed to get repo and knot", "err", err)
150 w.WriteHeader(http.StatusBadRequest)
151 return
152 }
153
154 idStr := chi.URLParam(r, "id")
155 id, err := strconv.ParseInt(idStr, 10, 64)
156 if err != nil {
157 l.Error("invalid webhook id", "err", err)
158 w.WriteHeader(http.StatusBadRequest)
159 return
160 }
161
162 webhook, err := db.GetWebhook(rp.db, id)
163 if err != nil {
164 l.Error("failed to get webhook", "err", err)
165 rp.pages.Notice(w, "webhooks-error", "Webhook not found")
166 return
167 }
168
169 // Verify webhook belongs to this repo
170 if string(webhook.RepoDid) != f.RepoDid {
171 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid)
172 w.WriteHeader(http.StatusForbidden)
173 return
174 }
175
176 url := strings.TrimSpace(r.FormValue("url"))
177 if url != "" {
178 if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
179 rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://")
180 return
181 }
182 webhook.Url = url
183 }
184
185 secret := strings.TrimSpace(r.FormValue("secret"))
186 if secret != "" {
187 webhook.Secret = secret
188 }
189
190 webhook.Active = r.FormValue("active") == "on"
191
192 events := webhookEventsFromForm(r)
193 if len(events) > 0 {
194 webhook.Events = events
195 }
196
197 tx, err := rp.db.Begin()
198 if err != nil {
199 l.Error("failed to start transaction", "err", err)
200 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook")
201 return
202 }
203 defer tx.Rollback()
204
205 if err := db.UpdateWebhook(tx, webhook); err != nil {
206 l.Error("failed to update webhook", "err", err)
207 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook")
208 return
209 }
210
211 if err := tx.Commit(); err != nil {
212 l.Error("failed to commit transaction", "err", err)
213 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook")
214 return
215 }
216
217 rp.pages.HxRefresh(w)
218}
219
220// DeleteWebhook deletes a webhook
221func (rp *Repo) DeleteWebhook(w http.ResponseWriter, r *http.Request) {
222 l := rp.logger.With("handler", "DeleteWebhook")
223
224 f, err := rp.repoResolver.Resolve(r)
225 if err != nil {
226 l.Error("failed to get repo and knot", "err", err)
227 w.WriteHeader(http.StatusBadRequest)
228 return
229 }
230
231 idStr := chi.URLParam(r, "id")
232 id, err := strconv.ParseInt(idStr, 10, 64)
233 if err != nil {
234 l.Error("invalid webhook id", "err", err)
235 w.WriteHeader(http.StatusBadRequest)
236 return
237 }
238
239 webhook, err := db.GetWebhook(rp.db, id)
240 if err != nil {
241 l.Error("failed to get webhook", "err", err)
242 w.WriteHeader(http.StatusNotFound)
243 return
244 }
245
246 // Verify webhook belongs to this repo
247 if string(webhook.RepoDid) != f.RepoDid {
248 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid)
249 w.WriteHeader(http.StatusForbidden)
250 return
251 }
252
253 tx, err := rp.db.Begin()
254 if err != nil {
255 l.Error("failed to start transaction", "err", err)
256 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook")
257 return
258 }
259 defer tx.Rollback()
260
261 if err := db.DeleteWebhook(tx, id); err != nil {
262 l.Error("failed to delete webhook", "err", err)
263 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook")
264 return
265 }
266
267 if err := tx.Commit(); err != nil {
268 l.Error("failed to commit transaction", "err", err)
269 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook")
270 return
271 }
272
273 rp.pages.HxRefresh(w)
274}
275
276// ToggleWebhook toggles the active state of a webhook
277func (rp *Repo) ToggleWebhook(w http.ResponseWriter, r *http.Request) {
278 l := rp.logger.With("handler", "ToggleWebhook")
279
280 f, err := rp.repoResolver.Resolve(r)
281 if err != nil {
282 l.Error("failed to get repo and knot", "err", err)
283 w.WriteHeader(http.StatusBadRequest)
284 return
285 }
286
287 idStr := chi.URLParam(r, "id")
288 id, err := strconv.ParseInt(idStr, 10, 64)
289 if err != nil {
290 l.Error("invalid webhook id", "err", err)
291 w.WriteHeader(http.StatusBadRequest)
292 return
293 }
294
295 webhook, err := db.GetWebhook(rp.db, id)
296 if err != nil {
297 l.Error("failed to get webhook", "err", err)
298 w.WriteHeader(http.StatusNotFound)
299 return
300 }
301
302 // Verify webhook belongs to this repo
303 if string(webhook.RepoDid) != f.RepoDid {
304 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid)
305 w.WriteHeader(http.StatusForbidden)
306 return
307 }
308
309 // Toggle the active state
310 webhook.Active = !webhook.Active
311
312 tx, err := rp.db.Begin()
313 if err != nil {
314 l.Error("failed to start transaction", "err", err)
315 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook")
316 return
317 }
318 defer tx.Rollback()
319
320 if err := db.UpdateWebhook(tx, webhook); err != nil {
321 l.Error("failed to update webhook", "err", err)
322 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook")
323 return
324 }
325
326 if err := tx.Commit(); err != nil {
327 l.Error("failed to commit transaction", "err", err)
328 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook")
329 return
330 }
331
332 rp.pages.HxRefresh(w)
333}
334
335// WebhookDeliveries returns all deliveries for a webhook (for modal display)
336func (rp *Repo) WebhookDeliveries(w http.ResponseWriter, r *http.Request) {
337 l := rp.logger.With("handler", "WebhookDeliveries")
338
339 f, err := rp.repoResolver.Resolve(r)
340 if err != nil {
341 l.Error("failed to get repo and knot", "err", err)
342 w.WriteHeader(http.StatusBadRequest)
343 return
344 }
345
346 idStr := chi.URLParam(r, "id")
347 id, err := strconv.ParseInt(idStr, 10, 64)
348 if err != nil {
349 l.Error("invalid webhook id", "err", err)
350 w.WriteHeader(http.StatusBadRequest)
351 return
352 }
353
354 webhook, err := db.GetWebhook(rp.db, id)
355 if err != nil {
356 l.Error("failed to get webhook", "err", err)
357 w.WriteHeader(http.StatusNotFound)
358 return
359 }
360
361 // Verify webhook belongs to this repo
362 if string(webhook.RepoDid) != f.RepoDid {
363 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid)
364 w.WriteHeader(http.StatusForbidden)
365 return
366 }
367
368 deliveries, err := db.GetWebhookDeliveries(rp.db, webhook.Id, 100)
369 if err != nil {
370 l.Error("failed to get webhook deliveries", "err", err)
371 rp.pages.Notice(w, "webhooks-error", "Failed to load deliveries")
372 return
373 }
374
375 user := rp.oauth.GetMultiAccountUser(r)
376
377 rp.pages.WebhookDeliveriesList(w, pages.WebhookDeliveriesListParams{
378 BaseParams: pages.BaseParamsFromContext(r.Context()),
379 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
380 Webhook: webhook,
381 Deliveries: deliveries,
382 })
383}