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 RepoDid: f.RepoDid,
94 Url: url,
95 Secret: secret,
96 Active: active,
97 Events: events,
98 }
99
100 tx, err := rp.db.Begin()
101 if err != nil {
102 l.Error("failed to start transaction", "err", err)
103 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook")
104 return
105 }
106 defer tx.Rollback()
107
108 if err := db.AddWebhook(tx, webhook); err != nil {
109 l.Error("failed to add webhook", "err", err)
110 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook")
111 return
112 }
113
114 if err := tx.Commit(); err != nil {
115 l.Error("failed to commit transaction", "err", err)
116 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook")
117 return
118 }
119
120 rp.pages.HxRefresh(w)
121}
122
123// UpdateWebhook updates an existing webhook
124func (rp *Repo) UpdateWebhook(w http.ResponseWriter, r *http.Request) {
125 l := rp.logger.With("handler", "UpdateWebhook")
126
127 f, err := rp.repoResolver.Resolve(r)
128 if err != nil {
129 l.Error("failed to get repo and knot", "err", err)
130 w.WriteHeader(http.StatusBadRequest)
131 return
132 }
133
134 idStr := chi.URLParam(r, "id")
135 id, err := strconv.ParseInt(idStr, 10, 64)
136 if err != nil {
137 l.Error("invalid webhook id", "err", err)
138 w.WriteHeader(http.StatusBadRequest)
139 return
140 }
141
142 webhook, err := db.GetWebhook(rp.db, id)
143 if err != nil {
144 l.Error("failed to get webhook", "err", err)
145 rp.pages.Notice(w, "webhooks-error", "Webhook not found")
146 return
147 }
148
149 // Verify webhook belongs to this repo
150 if webhook.RepoAt != f.RepoAt() {
151 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt())
152 w.WriteHeader(http.StatusForbidden)
153 return
154 }
155
156 url := strings.TrimSpace(r.FormValue("url"))
157 if url != "" {
158 if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
159 rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://")
160 return
161 }
162 webhook.Url = url
163 }
164
165 secret := strings.TrimSpace(r.FormValue("secret"))
166 if secret != "" {
167 webhook.Secret = secret
168 }
169
170 webhook.Active = r.FormValue("active") == "on"
171
172 // Parse events - only push events are supported for now
173 events := []string{}
174 if r.FormValue("event_push") == "on" {
175 events = append(events, string(models.WebhookEventPush))
176 }
177
178 if len(events) > 0 {
179 webhook.Events = events
180 }
181
182 tx, err := rp.db.Begin()
183 if err != nil {
184 l.Error("failed to start transaction", "err", err)
185 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook")
186 return
187 }
188 defer tx.Rollback()
189
190 if err := db.UpdateWebhook(tx, webhook); err != nil {
191 l.Error("failed to update webhook", "err", err)
192 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook")
193 return
194 }
195
196 if err := tx.Commit(); err != nil {
197 l.Error("failed to commit transaction", "err", err)
198 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook")
199 return
200 }
201
202 rp.pages.HxRefresh(w)
203}
204
205// DeleteWebhook deletes a webhook
206func (rp *Repo) DeleteWebhook(w http.ResponseWriter, r *http.Request) {
207 l := rp.logger.With("handler", "DeleteWebhook")
208
209 f, err := rp.repoResolver.Resolve(r)
210 if err != nil {
211 l.Error("failed to get repo and knot", "err", err)
212 w.WriteHeader(http.StatusBadRequest)
213 return
214 }
215
216 idStr := chi.URLParam(r, "id")
217 id, err := strconv.ParseInt(idStr, 10, 64)
218 if err != nil {
219 l.Error("invalid webhook id", "err", err)
220 w.WriteHeader(http.StatusBadRequest)
221 return
222 }
223
224 webhook, err := db.GetWebhook(rp.db, id)
225 if err != nil {
226 l.Error("failed to get webhook", "err", err)
227 w.WriteHeader(http.StatusNotFound)
228 return
229 }
230
231 // Verify webhook belongs to this repo
232 if webhook.RepoAt != f.RepoAt() {
233 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt())
234 w.WriteHeader(http.StatusForbidden)
235 return
236 }
237
238 tx, err := rp.db.Begin()
239 if err != nil {
240 l.Error("failed to start transaction", "err", err)
241 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook")
242 return
243 }
244 defer tx.Rollback()
245
246 if err := db.DeleteWebhook(tx, id); err != nil {
247 l.Error("failed to delete webhook", "err", err)
248 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook")
249 return
250 }
251
252 if err := tx.Commit(); err != nil {
253 l.Error("failed to commit transaction", "err", err)
254 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook")
255 return
256 }
257
258 rp.pages.HxRefresh(w)
259}
260
261// ToggleWebhook toggles the active state of a webhook
262func (rp *Repo) ToggleWebhook(w http.ResponseWriter, r *http.Request) {
263 l := rp.logger.With("handler", "ToggleWebhook")
264
265 f, err := rp.repoResolver.Resolve(r)
266 if err != nil {
267 l.Error("failed to get repo and knot", "err", err)
268 w.WriteHeader(http.StatusBadRequest)
269 return
270 }
271
272 idStr := chi.URLParam(r, "id")
273 id, err := strconv.ParseInt(idStr, 10, 64)
274 if err != nil {
275 l.Error("invalid webhook id", "err", err)
276 w.WriteHeader(http.StatusBadRequest)
277 return
278 }
279
280 webhook, err := db.GetWebhook(rp.db, id)
281 if err != nil {
282 l.Error("failed to get webhook", "err", err)
283 w.WriteHeader(http.StatusNotFound)
284 return
285 }
286
287 // Verify webhook belongs to this repo
288 if webhook.RepoAt != f.RepoAt() {
289 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt())
290 w.WriteHeader(http.StatusForbidden)
291 return
292 }
293
294 // Toggle the active state
295 webhook.Active = !webhook.Active
296
297 tx, err := rp.db.Begin()
298 if err != nil {
299 l.Error("failed to start transaction", "err", err)
300 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook")
301 return
302 }
303 defer tx.Rollback()
304
305 if err := db.UpdateWebhook(tx, webhook); err != nil {
306 l.Error("failed to update webhook", "err", err)
307 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook")
308 return
309 }
310
311 if err := tx.Commit(); err != nil {
312 l.Error("failed to commit transaction", "err", err)
313 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook")
314 return
315 }
316
317 rp.pages.HxRefresh(w)
318}
319
320// WebhookDeliveries returns all deliveries for a webhook (for modal display)
321func (rp *Repo) WebhookDeliveries(w http.ResponseWriter, r *http.Request) {
322 l := rp.logger.With("handler", "WebhookDeliveries")
323
324 f, err := rp.repoResolver.Resolve(r)
325 if err != nil {
326 l.Error("failed to get repo and knot", "err", err)
327 w.WriteHeader(http.StatusBadRequest)
328 return
329 }
330
331 idStr := chi.URLParam(r, "id")
332 id, err := strconv.ParseInt(idStr, 10, 64)
333 if err != nil {
334 l.Error("invalid webhook id", "err", err)
335 w.WriteHeader(http.StatusBadRequest)
336 return
337 }
338
339 webhook, err := db.GetWebhook(rp.db, id)
340 if err != nil {
341 l.Error("failed to get webhook", "err", err)
342 w.WriteHeader(http.StatusNotFound)
343 return
344 }
345
346 // Verify webhook belongs to this repo
347 if webhook.RepoAt != f.RepoAt() {
348 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt())
349 w.WriteHeader(http.StatusForbidden)
350 return
351 }
352
353 deliveries, err := db.GetWebhookDeliveries(rp.db, webhook.Id, 100)
354 if err != nil {
355 l.Error("failed to get webhook deliveries", "err", err)
356 rp.pages.Notice(w, "webhooks-error", "Failed to load deliveries")
357 return
358 }
359
360 user := rp.oauth.GetMultiAccountUser(r)
361
362 rp.pages.WebhookDeliveriesList(w, pages.WebhookDeliveriesListParams{
363 LoggedInUser: user,
364 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
365 Webhook: webhook,
366 Deliveries: deliveries,
367 })
368}