Monorepo for Tangled tangled.org

appview/repo: add webhook management endpoints #1069

merged opened by anirudh.fi targeting master from icy/qlyxxp
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo.pull/3menyebs7uu22
+314
Diff #5
+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
··· 167 167 168 168 case "pipelines": 169 169 rp.pipelineSettings(w, r) 170 + 171 + case "hooks": 172 + rp.Webhooks(w, r) 170 173 } 171 174 } 172 175
+304
appview/repo/webhooks.go
··· 1 + package repo 2 + 3 + import ( 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 15 + func (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 42 + func (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 110 + func (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 192 + func (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 248 + func (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 + }

History

6 rounds 1 comment
sign up or login to add to the discussion
1 commit
expand
appview/repo: add webhook management endpoints
2/3 failed, 1/3 success
expand
expand 0 comments
pull request successfully merged
1 commit
expand
appview/repo: add webhook management endpoints
2/3 failed, 1/3 success
expand
expand 0 comments
1 commit
expand
appview/repo: add webhook management endpoints
2/3 failed, 1/3 success
expand
expand 0 comments
1 commit
expand
appview/repo: add webhook management endpoints
2/3 failed, 1/3 success
expand
expand 0 comments
1 commit
expand
appview/repo: add webhook management endpoints
2/3 failed, 1/3 success
expand
expand 1 comment
  • here, we can just trigger a hx-refresh
  • similarly for update hooks here, (there does not seem to be a webhooks-success element!)
  • here, similar, hx-refresh should suffice
  • likewise for toggle

changeset lgtm otherwise!

1 commit
expand
appview/repo: add webhook management endpoints
2/3 failed, 1/3 success
expand
expand 0 comments