Signed-off-by: Anirudh Oppiliappan anirudh@tangled.org
+7
appview/repo/router.go
+7
appview/repo/router.go
···
87
87
r.Put("/branches/default", rp.SetDefaultBranch)
88
88
r.Put("/secrets", rp.Secrets)
89
89
r.Delete("/secrets", rp.Secrets)
90
+
r.With(mw.RepoPermissionMiddleware("repo:owner")).Route("/hooks", func(r chi.Router) {
91
+
r.Get("/", rp.Webhooks)
92
+
r.Post("/", rp.AddWebhook)
93
+
r.Put("/{id}", rp.UpdateWebhook)
94
+
r.Delete("/{id}", rp.DeleteWebhook)
95
+
r.Post("/{id}/toggle", rp.ToggleWebhook)
96
+
})
90
97
})
91
98
})
92
99
+3
appview/repo/settings.go
+3
appview/repo/settings.go
+317
appview/repo/webhooks.go
+317
appview/repo/webhooks.go
···
1
+
package repo
2
+
3
+
import (
4
+
"crypto/rand"
5
+
"encoding/hex"
6
+
"net/http"
7
+
"strconv"
8
+
"strings"
9
+
10
+
"github.com/go-chi/chi/v5"
11
+
"tangled.org/core/appview/db"
12
+
"tangled.org/core/appview/models"
13
+
"tangled.org/core/appview/pages"
14
+
)
15
+
16
+
// Webhooks displays the webhooks settings page
17
+
func (rp *Repo) Webhooks(w http.ResponseWriter, r *http.Request) {
18
+
l := rp.logger.With("handler", "Webhooks")
19
+
20
+
f, err := rp.repoResolver.Resolve(r)
21
+
if err != nil {
22
+
l.Error("failed to get repo and knot", "err", err)
23
+
w.WriteHeader(http.StatusBadRequest)
24
+
return
25
+
}
26
+
27
+
user := rp.oauth.GetMultiAccountUser(r)
28
+
29
+
webhooks, err := db.GetWebhooksForRepo(rp.db, f.RepoAt())
30
+
if err != nil {
31
+
l.Error("failed to get webhooks", "err", err)
32
+
rp.pages.Notice(w, "webhooks-error", "Failed to load webhooks")
33
+
return
34
+
}
35
+
36
+
rp.pages.RepoWebhooksSettings(w, pages.RepoWebhooksSettingsParams{
37
+
LoggedInUser: user,
38
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
39
+
Webhooks: webhooks,
40
+
})
41
+
}
42
+
43
+
// AddWebhook creates a new webhook
44
+
func (rp *Repo) AddWebhook(w http.ResponseWriter, r *http.Request) {
45
+
l := rp.logger.With("handler", "AddWebhook")
46
+
47
+
f, err := rp.repoResolver.Resolve(r)
48
+
if err != nil {
49
+
l.Error("failed to get repo and knot", "err", err)
50
+
w.WriteHeader(http.StatusBadRequest)
51
+
return
52
+
}
53
+
54
+
url := strings.TrimSpace(r.FormValue("url"))
55
+
if url == "" {
56
+
rp.pages.Notice(w, "webhooks-error", "Webhook URL is required")
57
+
return
58
+
}
59
+
60
+
// Validate URL
61
+
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
62
+
rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://")
63
+
return
64
+
}
65
+
66
+
secret := strings.TrimSpace(r.FormValue("secret"))
67
+
if secret == "" {
68
+
// Generate a random secret if not provided
69
+
secretBytes := make([]byte, 32)
70
+
if _, err := rand.Read(secretBytes); err != nil {
71
+
l.Error("failed to generate secret", "err", err)
72
+
rp.pages.Notice(w, "webhooks-error", "Failed to generate secret")
73
+
return
74
+
}
75
+
secret = hex.EncodeToString(secretBytes)
76
+
}
77
+
78
+
active := r.FormValue("active") == "on"
79
+
80
+
// Parse events - only push events are supported for now
81
+
events := []string{}
82
+
if r.FormValue("event_push") == "on" {
83
+
events = append(events, "push")
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.HxLocation(w, "/"+f.Did+"/"+f.Name+"/settings?tab=hooks")
120
+
}
121
+
122
+
// UpdateWebhook updates an existing webhook
123
+
func (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, "push")
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.Notice(w, "webhooks-success", "Webhook updated successfully")
202
+
}
203
+
204
+
// DeleteWebhook deletes a webhook
205
+
func (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.HxLocation(w, "/"+f.Did+"/"+f.Name+"/settings?tab=hooks")
258
+
}
259
+
260
+
// ToggleWebhook toggles the active state of a webhook
261
+
func (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.HxLocation(w, "/"+f.Did+"/"+f.Name+"/settings?tab=hooks")
317
+
}
History
6 rounds
1 comment
anirudh.fi
submitted
#5
1 commit
expand
collapse
appview/repo: add webhook management endpoints
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
2/3 failed, 1/3 success
expand
collapse
expand 0 comments
pull request successfully merged
anirudh.fi
submitted
#4
1 commit
expand
collapse
appview/repo: add webhook management endpoints
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
2/3 failed, 1/3 success
expand
collapse
expand 0 comments
anirudh.fi
submitted
#3
1 commit
expand
collapse
appview/repo: add webhook management endpoints
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
2/3 failed, 1/3 success
expand
collapse
expand 0 comments
anirudh.fi
submitted
#2
1 commit
expand
collapse
appview/repo: add webhook management endpoints
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
2/3 failed, 1/3 success
expand
collapse
expand 0 comments
anirudh.fi
submitted
#1
1 commit
expand
collapse
appview/repo: add webhook management endpoints
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
2/3 failed, 1/3 success
expand
collapse
expand 1 comment
anirudh.fi
submitted
#0
1 commit
expand
collapse
appview/repo: add webhook management endpoints
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
changeset lgtm otherwise!