Monorepo for Tangled
at push-ntmmpnmptnvp 367 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 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.HxRefresh(w) 120} 121 122// UpdateWebhook updates an existing webhook 123func (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, string(models.WebhookEventPush)) 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.HxRefresh(w) 202} 203 204// DeleteWebhook deletes a webhook 205func (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.HxRefresh(w) 258} 259 260// ToggleWebhook toggles the active state of a webhook 261func (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.HxRefresh(w) 317} 318 319// WebhookDeliveries returns all deliveries for a webhook (for modal display) 320func (rp *Repo) WebhookDeliveries(w http.ResponseWriter, r *http.Request) { 321 l := rp.logger.With("handler", "WebhookDeliveries") 322 323 f, err := rp.repoResolver.Resolve(r) 324 if err != nil { 325 l.Error("failed to get repo and knot", "err", err) 326 w.WriteHeader(http.StatusBadRequest) 327 return 328 } 329 330 idStr := chi.URLParam(r, "id") 331 id, err := strconv.ParseInt(idStr, 10, 64) 332 if err != nil { 333 l.Error("invalid webhook id", "err", err) 334 w.WriteHeader(http.StatusBadRequest) 335 return 336 } 337 338 webhook, err := db.GetWebhook(rp.db, id) 339 if err != nil { 340 l.Error("failed to get webhook", "err", err) 341 w.WriteHeader(http.StatusNotFound) 342 return 343 } 344 345 // Verify webhook belongs to this repo 346 if webhook.RepoAt != f.RepoAt() { 347 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt()) 348 w.WriteHeader(http.StatusForbidden) 349 return 350 } 351 352 deliveries, err := db.GetWebhookDeliveries(rp.db, webhook.Id, 100) 353 if err != nil { 354 l.Error("failed to get webhook deliveries", "err", err) 355 rp.pages.Notice(w, "webhooks-error", "Failed to load deliveries") 356 return 357 } 358 359 user := rp.oauth.GetMultiAccountUser(r) 360 361 rp.pages.WebhookDeliveriesList(w, pages.WebhookDeliveriesListParams{ 362 LoggedInUser: user, 363 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 364 Webhook: webhook, 365 Deliveries: deliveries, 366 }) 367}