Monorepo for Tangled
at push-pmqotzqwskqq 368 lines 9.7 kB view raw
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}