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 rp.pages.RepoWebhooksSettings(w, pages.RepoWebhooksSettingsParams{
35 LoggedInUser: user,
36 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
37 Webhooks: webhooks,
38 })
39}
40
41// AddWebhook creates a new webhook
42func (rp *Repo) AddWebhook(w http.ResponseWriter, r *http.Request) {
43 l := rp.logger.With("handler", "AddWebhook")
44
45 f, err := rp.repoResolver.Resolve(r)
46 if err != nil {
47 l.Error("failed to get repo and knot", "err", err)
48 w.WriteHeader(http.StatusBadRequest)
49 return
50 }
51
52 url := strings.TrimSpace(r.FormValue("url"))
53 if url == "" {
54 rp.pages.Notice(w, "webhooks-error", "Webhook URL is required")
55 return
56 }
57
58 if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
59 rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://")
60 return
61 }
62
63 secret := strings.TrimSpace(r.FormValue("secret"))
64 // if secret is empty, we don't sign
65
66 active := r.FormValue("active") == "on"
67
68 events := []string{}
69 if r.FormValue("event_push") == "on" {
70 events = append(events, string(models.WebhookEventPush))
71 }
72
73 if len(events) == 0 {
74 rp.pages.Notice(w, "webhooks-error", "Push events must be enabled")
75 return
76 }
77
78 webhook := &models.Webhook{
79 RepoAt: f.RepoAt(),
80 Url: url,
81 Secret: secret,
82 Active: active,
83 Events: events,
84 }
85
86 tx, err := rp.db.Begin()
87 if err != nil {
88 l.Error("failed to start transaction", "err", err)
89 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook")
90 return
91 }
92 defer tx.Rollback()
93
94 if err := db.AddWebhook(tx, webhook); err != nil {
95 l.Error("failed to add webhook", "err", err)
96 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook")
97 return
98 }
99
100 if err := tx.Commit(); err != nil {
101 l.Error("failed to commit transaction", "err", err)
102 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook")
103 return
104 }
105
106 rp.pages.HxRefresh(w)
107}
108
109// UpdateWebhook updates an existing webhook
110func (rp *Repo) UpdateWebhook(w http.ResponseWriter, r *http.Request) {
111 l := rp.logger.With("handler", "UpdateWebhook")
112
113 f, err := rp.repoResolver.Resolve(r)
114 if err != nil {
115 l.Error("failed to get repo and knot", "err", err)
116 w.WriteHeader(http.StatusBadRequest)
117 return
118 }
119
120 idStr := chi.URLParam(r, "id")
121 id, err := strconv.ParseInt(idStr, 10, 64)
122 if err != nil {
123 l.Error("invalid webhook id", "err", err)
124 w.WriteHeader(http.StatusBadRequest)
125 return
126 }
127
128 webhook, err := db.GetWebhook(rp.db, id)
129 if err != nil {
130 l.Error("failed to get webhook", "err", err)
131 rp.pages.Notice(w, "webhooks-error", "Webhook not found")
132 return
133 }
134
135 // Verify webhook belongs to this repo
136 if webhook.RepoAt != f.RepoAt() {
137 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt())
138 w.WriteHeader(http.StatusForbidden)
139 return
140 }
141
142 url := strings.TrimSpace(r.FormValue("url"))
143 if url != "" {
144 if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
145 rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://")
146 return
147 }
148 webhook.Url = url
149 }
150
151 secret := strings.TrimSpace(r.FormValue("secret"))
152 if secret != "" {
153 webhook.Secret = secret
154 }
155
156 webhook.Active = r.FormValue("active") == "on"
157
158 // Parse events - only push events are supported for now
159 events := []string{}
160 if r.FormValue("event_push") == "on" {
161 events = append(events, string(models.WebhookEventPush))
162 }
163
164 if len(events) > 0 {
165 webhook.Events = events
166 }
167
168 tx, err := rp.db.Begin()
169 if err != nil {
170 l.Error("failed to start transaction", "err", err)
171 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook")
172 return
173 }
174 defer tx.Rollback()
175
176 if err := db.UpdateWebhook(tx, webhook); err != nil {
177 l.Error("failed to update webhook", "err", err)
178 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook")
179 return
180 }
181
182 if err := tx.Commit(); err != nil {
183 l.Error("failed to commit transaction", "err", err)
184 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook")
185 return
186 }
187
188 rp.pages.HxRefresh(w)
189}
190
191// DeleteWebhook deletes a webhook
192func (rp *Repo) DeleteWebhook(w http.ResponseWriter, r *http.Request) {
193 l := rp.logger.With("handler", "DeleteWebhook")
194
195 f, err := rp.repoResolver.Resolve(r)
196 if err != nil {
197 l.Error("failed to get repo and knot", "err", err)
198 w.WriteHeader(http.StatusBadRequest)
199 return
200 }
201
202 idStr := chi.URLParam(r, "id")
203 id, err := strconv.ParseInt(idStr, 10, 64)
204 if err != nil {
205 l.Error("invalid webhook id", "err", err)
206 w.WriteHeader(http.StatusBadRequest)
207 return
208 }
209
210 webhook, err := db.GetWebhook(rp.db, id)
211 if err != nil {
212 l.Error("failed to get webhook", "err", err)
213 w.WriteHeader(http.StatusNotFound)
214 return
215 }
216
217 // Verify webhook belongs to this repo
218 if webhook.RepoAt != f.RepoAt() {
219 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt())
220 w.WriteHeader(http.StatusForbidden)
221 return
222 }
223
224 tx, err := rp.db.Begin()
225 if err != nil {
226 l.Error("failed to start transaction", "err", err)
227 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook")
228 return
229 }
230 defer tx.Rollback()
231
232 if err := db.DeleteWebhook(tx, id); err != nil {
233 l.Error("failed to delete webhook", "err", err)
234 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook")
235 return
236 }
237
238 if err := tx.Commit(); err != nil {
239 l.Error("failed to commit transaction", "err", err)
240 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook")
241 return
242 }
243
244 rp.pages.HxRefresh(w)
245}
246
247// ToggleWebhook toggles the active state of a webhook
248func (rp *Repo) ToggleWebhook(w http.ResponseWriter, r *http.Request) {
249 l := rp.logger.With("handler", "ToggleWebhook")
250
251 f, err := rp.repoResolver.Resolve(r)
252 if err != nil {
253 l.Error("failed to get repo and knot", "err", err)
254 w.WriteHeader(http.StatusBadRequest)
255 return
256 }
257
258 idStr := chi.URLParam(r, "id")
259 id, err := strconv.ParseInt(idStr, 10, 64)
260 if err != nil {
261 l.Error("invalid webhook id", "err", err)
262 w.WriteHeader(http.StatusBadRequest)
263 return
264 }
265
266 webhook, err := db.GetWebhook(rp.db, id)
267 if err != nil {
268 l.Error("failed to get webhook", "err", err)
269 w.WriteHeader(http.StatusNotFound)
270 return
271 }
272
273 // Verify webhook belongs to this repo
274 if webhook.RepoAt != f.RepoAt() {
275 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt())
276 w.WriteHeader(http.StatusForbidden)
277 return
278 }
279
280 // Toggle the active state
281 webhook.Active = !webhook.Active
282
283 tx, err := rp.db.Begin()
284 if err != nil {
285 l.Error("failed to start transaction", "err", err)
286 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook")
287 return
288 }
289 defer tx.Rollback()
290
291 if err := db.UpdateWebhook(tx, webhook); err != nil {
292 l.Error("failed to update webhook", "err", err)
293 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook")
294 return
295 }
296
297 if err := tx.Commit(); err != nil {
298 l.Error("failed to commit transaction", "err", err)
299 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook")
300 return
301 }
302
303 rp.pages.HxRefresh(w)
304}