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
+327
Diff #0
+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
+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
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!

anirudh.fi submitted #0
1 commit
expand
appview/repo: add webhook management endpoints
2/3 failed, 1/3 success
expand
expand 0 comments