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