bullet_train-outgoing_webhooks 1.25.1 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/account/webhooks/outgoing/endpoints_controller.rb +30 -1
  3. data/app/controllers/concerns/api/v1/webhooks/outgoing/endpoints/controller_base.rb +1 -0
  4. data/app/mailers/webhooks/outgoing/endpoint_mailer.rb +35 -0
  5. data/app/models/concerns/webhooks/outgoing/delivery_support.rb +35 -3
  6. data/app/models/concerns/webhooks/outgoing/endpoint_deactivatable.rb +78 -0
  7. data/app/models/webhooks/outgoing/endpoint.rb +1 -0
  8. data/app/views/account/webhooks/outgoing/deliveries/_index.html.erb +1 -1
  9. data/app/views/account/webhooks/outgoing/deliveries/show.html.erb +1 -1
  10. data/app/views/account/webhooks/outgoing/endpoints/_index.html.erb +19 -0
  11. data/app/views/api/v1/webhooks/outgoing/endpoints/_endpoint.json.jbuilder +3 -0
  12. data/app/views/webhooks/outgoing/endpoint_mailer/deactivated.html.erb +49 -0
  13. data/app/views/webhooks/outgoing/endpoint_mailer/deactivation_limit_reached.html.erb +45 -0
  14. data/config/locales/en/webhooks/outgoing/endpoint_mailer.en.yml +55 -0
  15. data/config/locales/en/webhooks/outgoing/endpoints.en.yml +37 -1
  16. data/config/routes.rb +4 -0
  17. data/db/migrate/20250522080219_add_deactivation_fields_to_endpoints.rb +10 -0
  18. data/db/migrate/20250526132225_add_invitations_table.rb +16 -0
  19. data/db/migrate/20250528082127_update_users_table_for_dummy_app.rb +34 -0
  20. data/db/migrate/20250528083628_add_locale_to_team.rb +5 -0
  21. data/db/migrate/20250616143032_add_failed_count_to_webhook_endpoints.rb +5 -0
  22. data/lib/bullet_train/outgoing_webhooks/engine.rb +6 -1
  23. data/lib/bullet_train/outgoing_webhooks/version.rb +1 -1
  24. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6231c0042625ad6748864d4ca1e0beb7c19a429d8e44bbe80eb806fcce82131f
4
- data.tar.gz: 4f54ddf9560e553ad35ae3e3a2b116fd935b84414c1d6a84feddca36a30089e8
3
+ metadata.gz: 36479ad4e919366dc9e89a68a38682e16806e5fc13871afca777677fe78e03c6
4
+ data.tar.gz: ee4b1266f2988a289b295708d90b45ede44e0e9dde8a0e890e992a0b41275611
5
5
  SHA512:
6
- metadata.gz: 2dfcceb4b6e4bafece0d922b74153c2af6079ae6542ad2a907997c63a5cd3ab3ddaa36ef537aef6ebced323535cf7472fd6a165baae24c87101050cf1165777c
7
- data.tar.gz: 48669fe6f62b620c2c44123476c8caa6c83286ba3b2f59b881e866dbb789be8a58fbbbcd92f3565a7b5781602bf3dd4211451e41d35066097a05ebcbe710dcc2
6
+ metadata.gz: f18aadf46809c1353f7b8b8596c9ffe1c26670c0eeb72dfd4ea0af5987a0afcdfd88502763e9672556b392f64459b8ba2ca36823743b659b94b818e02d7bd8cb
7
+ data.tar.gz: c0848a23a118193782037328ea2f8a6bdd94ea52b3ea2726906f8f7f91678dda750820aa89318e74cea6414722399830c6befb1d6dc37968523fdcf51f9226a3
@@ -1,5 +1,8 @@
1
1
  class Account::Webhooks::Outgoing::EndpointsController < Account::ApplicationController
2
- account_load_and_authorize_resource :endpoint, through: BulletTrain::OutgoingWebhooks.parent_association, through_association: :webhooks_outgoing_endpoints
2
+ account_load_and_authorize_resource :endpoint,
3
+ through: BulletTrain::OutgoingWebhooks.parent_association,
4
+ through_association: :webhooks_outgoing_endpoints,
5
+ member_actions: [:activate, :deactivate]
3
6
  before_action { @parent = instance_variable_get("@#{BulletTrain::OutgoingWebhooks.parent_association}") }
4
7
 
5
8
  # GET /account/teams/:team_id/webhooks/outgoing/endpoints
@@ -71,6 +74,32 @@ class Account::Webhooks::Outgoing::EndpointsController < Account::ApplicationCon
71
74
  end
72
75
  end
73
76
 
77
+ # POST /account/webhooks/outgoing/endpoints/:id/activate
78
+ def activate
79
+ respond_to do |format|
80
+ if @endpoint.reactivate!
81
+ format.html { redirect_to [:account, @parent, :webhooks_outgoing_endpoints], notice: I18n.t("webhooks/outgoing/endpoints.notifications.activated") }
82
+ format.json { render :show, status: :ok, location: [:account, @endpoint] }
83
+ else
84
+ format.html { redirect_to [:account, @parent, :webhooks_outgoing_endpoints], alert: I18n.t("webhooks/outgoing/endpoints.notifications.activation_failed") }
85
+ format.json { render json: @endpoint.errors, status: :unprocessable_entity }
86
+ end
87
+ end
88
+ end
89
+
90
+ # DELETE /account/webhooks/outgoing/endpoints/:id/deactivate
91
+ def deactivate
92
+ respond_to do |format|
93
+ if @endpoint.deactivate!
94
+ format.html { redirect_to [:account, @parent, :webhooks_outgoing_endpoints], notice: I18n.t("webhooks/outgoing/endpoints.notifications.deactivated") }
95
+ format.json { render :show, status: :ok, location: [:account, @endpoint] }
96
+ else
97
+ format.html { redirect_to [:account, @parent, :webhooks_outgoing_endpoints], alert: I18n.t("webhooks/outgoing/endpoints.notifications.deactivation_failed") }
98
+ format.json { render json: @endpoint.errors, status: :unprocessable_entity }
99
+ end
100
+ end
101
+ end
102
+
74
103
  private
75
104
 
76
105
  # Never trust parameters from the scary internet, only allow the white list through.
@@ -10,6 +10,7 @@ module Api::V1::Webhooks::Outgoing::Endpoints::ControllerBase
10
10
  :name,
11
11
  :api_version,
12
12
  :scaffolding_absolutely_abstract_creative_concept_id,
13
+ :deactivated_at,
13
14
  # 🚅 super scaffolding will insert new fields above this line.
14
15
  *permitted_arrays,
15
16
  event_type_ids: [],
@@ -0,0 +1,35 @@
1
+ class Webhooks::Outgoing::EndpointMailer < ApplicationMailer
2
+ def deactivation_limit_reached(endpoint)
3
+ @endpoint = endpoint
4
+ email = @endpoint.team.formatted_email_address
5
+ return if email.blank?
6
+ set_values(@endpoint)
7
+
8
+ mail(
9
+ to: email,
10
+ subject: I18n.t("webhooks.outgoing.endpoint_mailer.deactivation_limit_reached.subject", endpoint_name: @endpoint.name)
11
+ )
12
+ end
13
+
14
+ def deactivated(endpoint)
15
+ @endpoint = endpoint
16
+ email = @endpoint.team.formatted_email_address
17
+ return if email.blank?
18
+ set_values(@endpoint)
19
+
20
+ mail(
21
+ to: email,
22
+ subject: I18n.t("webhooks.outgoing.endpoint_mailer.deactivated.subject", endpoint_name: @endpoint.name)
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ def set_values(endpoint)
29
+ @values ||= {
30
+ endpoint_name: endpoint.name,
31
+ endpoint_events: endpoint.event_type_ids.join(", "),
32
+ app_name: I18n.t("application.name"),
33
+ }
34
+ end
35
+ end
@@ -15,6 +15,7 @@ module Webhooks::Outgoing::DeliverySupport
15
15
  3 => 5.minutes,
16
16
  4 => 15.minutes,
17
17
  5 => 1.hour,
18
+ 6 => 24.hours,
18
19
  }
19
20
 
20
21
  def label_string
@@ -28,14 +29,21 @@ module Webhooks::Outgoing::DeliverySupport
28
29
  def deliver_async
29
30
  if still_attempting?
30
31
  Webhooks::Outgoing::DeliveryJob.set(wait: next_reattempt_delay).perform_later(self)
32
+ else
33
+ # All delivery attempts have now failed, should we deactivate the endpoint?
34
+ endpoint.handle_exhausted_delivery_attempts
31
35
  end
32
36
  end
33
37
 
34
- def deliver
38
+ # This method is used to create an attempt and deliver a webhook.
39
+ # If the endpoint is disabled, the attempt will not be created and the webhook will not be delivered.
40
+ # You can bypass this condition by passing `force: true`
41
+ def deliver(force: false)
42
+ return if endpoint.deactivated? && !force
35
43
  # TODO If we ever do away with the `async: true` default for webhook generation, then I believe this needs to
36
44
  # change otherwise we'd be attempting the first delivery of webhooks inline.
37
45
  if delivery_attempts.new.attempt
38
- touch(:delivered_at)
46
+ mark_as_delivered!
39
47
  else
40
48
  deliver_async
41
49
  end
@@ -55,7 +63,24 @@ module Webhooks::Outgoing::DeliverySupport
55
63
  end
56
64
 
57
65
  def failed?
58
- !(delivered? || still_attempting?)
66
+ !delivered? && !still_attempting?
67
+ end
68
+
69
+ def not_attempted?
70
+ attempt_count.zero?
71
+ end
72
+
73
+ def attempts_schedule_period_elapsed?
74
+ max_attempts_period = ATTEMPT_SCHEDULE.values.sum
75
+ created_at < max_attempts_period.ago
76
+ end
77
+
78
+ # This method is used to display delivery statuses in the UI.
79
+ # For deactivated endpoints, we don't make any delivery attempts, however, the delivery itself is still created,
80
+ # so we show it as "failed" in the UI.
81
+ # We also show deliveries as failed when the maximum attempt period has elapsed.
82
+ def failed_or_not_attempted_or_elapsed?
83
+ failed? || not_attempted? || attempts_schedule_period_elapsed?
59
84
  end
60
85
 
61
86
  def name
@@ -65,4 +90,11 @@ module Webhooks::Outgoing::DeliverySupport
65
90
  def max_attempts
66
91
  ATTEMPT_SCHEDULE.keys.max
67
92
  end
93
+
94
+ def mark_as_delivered!
95
+ ActiveRecord::Base.transaction do
96
+ touch(:delivered_at)
97
+ endpoint.reset_failed_deliveries_tracking!
98
+ end
99
+ end
68
100
  end
@@ -0,0 +1,78 @@
1
+ module Webhooks::Outgoing::EndpointDeactivatable
2
+ extend ActiveSupport::Concern
3
+
4
+ def active?
5
+ deactivated_at.nil?
6
+ end
7
+
8
+ def deactivated?
9
+ deactivated_at.present?
10
+ end
11
+
12
+ def marked_for_deactivation?
13
+ deactivation_limit_reached_at.present? && deactivated_at.nil?
14
+ end
15
+
16
+ def reset_failed_deliveries_tracking!
17
+ update_columns(deactivation_limit_reached_at: nil, consecutive_failed_deliveries: 0)
18
+ end
19
+
20
+ def deactivate!
21
+ return if deactivated?
22
+
23
+ update!(deactivated_at: Time.current)
24
+ end
25
+
26
+ def mark_for_deactivation!
27
+ return if marked_for_deactivation?
28
+ return if deactivated?
29
+
30
+ update!(deactivation_limit_reached_at: Time.current)
31
+ end
32
+
33
+ def reactivate!
34
+ return unless deactivated?
35
+
36
+ update!(deactivated_at: nil, deactivation_limit_reached_at: nil, consecutive_failed_deliveries: 0)
37
+ end
38
+
39
+ def handle_exhausted_delivery_attempts
40
+ return unless BulletTrain::OutgoingWebhooks::Engine.config.outgoing_webhooks[:automatic_endpoint_deactivation_enabled]
41
+ return if deactivated?
42
+
43
+ increment!(:consecutive_failed_deliveries)
44
+
45
+ # If the endpoint is marked for deactivation, we check if the cooling-off period (deactivation_in setting) has passed.
46
+ # If so, we mark it as deactivated.
47
+ if should_be_deactivated?
48
+ deactivate!
49
+ notify_deactivated
50
+ elsif should_be_marked_for_deactivation?
51
+ mark_for_deactivation!
52
+ notify_deactivation_limit_reached
53
+ end
54
+ end
55
+
56
+ def should_be_deactivated?
57
+ return false unless marked_for_deactivation?
58
+ return false if deactivated?
59
+
60
+ deactivation_limit_reached_at <= BulletTrain::OutgoingWebhooks::Engine.config.outgoing_webhooks.dig(:automatic_endpoint_deactivation_settings, :deactivation_in).ago
61
+ end
62
+
63
+ def should_be_marked_for_deactivation?
64
+ return false if deactivated?
65
+ return false if marked_for_deactivation?
66
+
67
+ max_limit = BulletTrain::OutgoingWebhooks::Engine.config.outgoing_webhooks.dig(:automatic_endpoint_deactivation_settings, :max_limit)
68
+ consecutive_failed_deliveries >= max_limit
69
+ end
70
+
71
+ def notify_deactivation_limit_reached
72
+ Webhooks::Outgoing::EndpointMailer.deactivation_limit_reached(self).deliver_later
73
+ end
74
+
75
+ def notify_deactivated
76
+ Webhooks::Outgoing::EndpointMailer.deactivated(self).deliver_later
77
+ end
78
+ end
@@ -1,5 +1,6 @@
1
1
  class Webhooks::Outgoing::Endpoint < BulletTrain::OutgoingWebhooks.base_class.constantize
2
2
  include Webhooks::Outgoing::EndpointSupport
3
+ include Webhooks::Outgoing::EndpointDeactivatable
3
4
  # 🚅 add concerns above.
4
5
 
5
6
  # 🚅 add belongs_to associations above.
@@ -31,7 +31,7 @@
31
31
  <td><%= render 'shared/attributes/code', attribute: :event_type_name %></td>
32
32
  <% end %>
33
33
  <td><%= render 'shared/attributes/code', attribute: :endpoint_url %></td>
34
- <td class="text-center"><%= render 'shared/attributes/attempt', attribute: :status, success_method: :delivered?, attempting_method: :still_attempting?, failure_method: :failed? %></td>
34
+ <td class="text-center"><%= render 'shared/attributes/attempt', attribute: :status, success_method: :delivered?, attempting_method: :still_attempting?, failure_method: :failed_or_not_attempted_or_elapsed? %></td>
35
35
  <%# 🚅 super scaffolding will insert new fields above this line. %>
36
36
  <td><%= render 'shared/attributes/date_and_time', attribute: :created_at %></td>
37
37
  <td class="buttons">
@@ -17,7 +17,7 @@
17
17
  <pre><%= JSON.pretty_generate(@delivery.event.payload) %></pre>
18
18
  <% end %>
19
19
  <% end %>
20
- <%= render 'shared/attributes/attempt', attribute: :status, success_method: :delivered?, attempting_method: :still_attempting?, failure_method: :failed? %>
20
+ <%= render 'shared/attributes/attempt', attribute: :status, success_method: :delivered?, attempting_method: :still_attempting?, failure_method: :failed_or_not_attempted_or_elapsed? %>
21
21
  <%= render 'shared/attributes/code', attribute: :endpoint_url %>
22
22
  <%= render 'shared/attributes/date_and_time', attribute: :delivered_at %>
23
23
  <%# 🚅 super scaffolding will insert new fields above this line. %>
@@ -16,6 +16,7 @@
16
16
  <th><%= t('.fields.name.heading') %></th>
17
17
  <th><%= t('.fields.url.heading') %></th>
18
18
  <th><%= t('.fields.webhook_secret.heading') %></th>
19
+ <th><%= t('.status.heading') %></th>
19
20
  <%# 🚅 super scaffolding will insert new field headers above this line. %>
20
21
  <th class="text-right"></th>
21
22
  </tr>
@@ -27,12 +28,30 @@
27
28
  <td><%= render 'shared/attributes/text', attribute: :name, url: [:account, endpoint] %></td>
28
29
  <td><%= render 'shared/attributes/code', attribute: :url %></td>
29
30
  <td><%= render 'shared/attributes/code', attribute: :webhook_secret, secret: true %></td>
31
+ <td>
32
+ <% if endpoint.active? %>
33
+ <%= t('.status.active') %>
34
+ <% if endpoint.deactivation_limit_reached_at.present? %>
35
+ <br>
36
+ <%= t('.deactivation_limit_reached.description', reached_at: l(endpoint.deactivation_limit_reached_at, format: :date_and_time_field)) %>
37
+ <% end %>
38
+ <% else %>
39
+ <%= t('.status.deactivated') %>
40
+ <% end %>
41
+ </td>
30
42
  <%# 🚅 super scaffolding will insert new fields above this line. %>
31
43
  <td class="buttons">
32
44
  <% unless hide_actions %>
33
45
  <% if can? :edit, endpoint %>
34
46
  <%= link_to t('.buttons.shorthand.edit'), [:edit, :account, endpoint], class: 'button-secondary button-smaller' %>
35
47
  <% end %>
48
+ <% if can? :edit, endpoint %>
49
+ <% if endpoint.active? %>
50
+ <%= button_to t('.buttons.shorthand.deactivate'), [:deactivate, :account, endpoint], method: :delete, data: { turbo_confirm: t('.buttons.confirmations.deactivate', model_locales(endpoint)) }, class: 'button-secondary button-smaller' %>
51
+ <% else %>
52
+ <%= button_to t('.buttons.shorthand.activate'), [:activate, :account, endpoint], method: :post, class: 'button-secondary button-smaller' %>
53
+ <% end %>
54
+ <% end %>
36
55
  <% if can? :destroy, endpoint %>
37
56
  <%= button_to t('.buttons.shorthand.destroy'), [:account, endpoint], method: :delete, data: { turbo_confirm: t('.buttons.confirmations.destroy', model_locales(endpoint)) }, class: 'button-secondary button-smaller' %>
38
57
  <% end %>
@@ -4,6 +4,9 @@ json.extract! endpoint,
4
4
  :url,
5
5
  :name,
6
6
  :event_type_ids,
7
+ :deactivation_limit_reached_at,
8
+ :deactivated_at,
9
+ :consecutive_failed_deliveries,
7
10
  # 🚅 super scaffolding will insert new fields above this line.
8
11
  :created_at,
9
12
  :updated_at
@@ -0,0 +1,49 @@
1
+ <% content_for :preheader do %>
2
+ <%= t('.preview', @values) %>
3
+ <% end %>
4
+
5
+ <h1><%= t('.heading') %></h1>
6
+ <br>
7
+ <%= t('.body.html', @values) %>
8
+
9
+ <br>
10
+ <div class="endpoint-details">
11
+ <%= t('.details.html', @values) %>
12
+ </div>
13
+
14
+ <br>
15
+ <h2><%= t('.explanation.heading', @values) %></h2>
16
+ <br>
17
+ <p><%= t('.explanation.text', @values) %></p>
18
+
19
+ <br>
20
+ <h2><%= t('.action.heading', @values) %></h2>
21
+ <br>
22
+ <%= t('.action.html', @values) %>
23
+ <br>
24
+
25
+ <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
26
+ <tr>
27
+ <td align="center">
28
+ <!-- Border based button https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
29
+ <table width="100%" border="0" cellspacing="0" cellpadding="0">
30
+ <tr>
31
+ <td align="center">
32
+ <table border="0" cellspacing="0" cellpadding="0">
33
+ <tr>
34
+ <td>
35
+ <a href="<%= account_webhooks_outgoing_endpoint_url(@endpoint) %>" target="_blank" class="button">
36
+ <%= t('.action.button', @values) %>
37
+ </a>
38
+ </td>
39
+ </tr>
40
+ </table>
41
+ </td>
42
+ </tr>
43
+ </table>
44
+ </td>
45
+ </tr>
46
+ </table>
47
+
48
+ <br>
49
+ <%= t('.signature.html', @values.merge({support_email: t('application.support_email')})) %>
@@ -0,0 +1,45 @@
1
+ <% content_for :preheader do %>
2
+ <%= t('.preview', @values) %>
3
+ <% end %>
4
+
5
+ <h1><%= t('.heading') %></h1>
6
+ <br>
7
+ <%= t('.body.html', @values) %>
8
+ <br>
9
+ <div class="endpoint-details">
10
+ <%= t('.details.html', @values) %>
11
+ </div>
12
+ <br>
13
+ <h2><%= t('.explanation.heading', @values) %></h2>
14
+ <br>
15
+ <p><%= t('.explanation.text', @values) %></p>
16
+ <br>
17
+ <h2><%= t('.action.heading', @values) %></h2>
18
+ <br>
19
+ <%= t('.action.html', @values) %>
20
+
21
+ <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
22
+ <tr>
23
+ <td align="center">
24
+ <!-- Border based button https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
25
+ <table width="100%" border="0" cellspacing="0" cellpadding="0">
26
+ <tr>
27
+ <td align="center">
28
+ <table border="0" cellspacing="0" cellpadding="0">
29
+ <tr>
30
+ <td>
31
+ <a href="<%= account_webhooks_outgoing_endpoint_url(@endpoint) %>" target="_blank" class="button">
32
+ <%= t('.action.button', @values) %>
33
+ </a>
34
+ </td>
35
+ </tr>
36
+ </table>
37
+ </td>
38
+ </tr>
39
+ </table>
40
+ </td>
41
+ </tr>
42
+ </table>
43
+
44
+ <br>
45
+ <%= t('.signature.html', @values.merge({support_email: t('application.support_email')})) %>
@@ -0,0 +1,55 @@
1
+ en:
2
+ webhooks:
3
+ outgoing:
4
+ endpoint_mailer:
5
+ deactivation_limit_reached:
6
+ subject: "Webhook Endpoint Failure Limit Reached - %{endpoint_name}"
7
+ preview: "Your webhook endpoint %{endpoint_name} has reached the failure limit and may be deactivated"
8
+ heading: "Webhook Endpoint Failure Limit Reached"
9
+ body:
10
+ html: |
11
+ <p>Hello,</p>
12
+ <p>We wanted to notify you that your webhook endpoint "<strong>%{endpoint_name}</strong>" has reached the maximum number of consecutive failed delivery attempts. The endpoint is now marked for potential deactivation.</p>
13
+ details:
14
+ html: |
15
+ <p>Endpoint Name: <strong>%{endpoint_name}</strong></p>
16
+ <p>Endpoint Events: <strong>%{endpoint_events}</strong></p>
17
+ explanation:
18
+ heading: "What this means:"
19
+ text: "Your webhook endpoint is experiencing delivery failures. If this continues, the endpoint will be automatically deactivated to prevent further failed attempts."
20
+ action:
21
+ heading: "What you should do:"
22
+ html: |
23
+ <ul>
24
+ <li>Check that your webhook endpoint URL is accessible and responding correctly</li>
25
+ <li>Verify that your endpoint is returning HTTP 2xx status codes</li>
26
+ </ul>
27
+ button: &open_endpoint Open the endpoint
28
+ signature: &SIGNATURE
29
+ html:
30
+ <p>If you have any questions, please don't hesitate to <a href="mailto:%{support_email}">send us an email</a>.</p>
31
+ <p>Thanks,<br>%{app_name}</p>
32
+ deactivated:
33
+ subject: "Webhook Endpoint Deactivated - %{endpoint_name}"
34
+ preview: "Your webhook endpoint %{endpoint_name} has been deactivated due to repeated failures"
35
+ heading: "Webhook Endpoint Deactivated"
36
+ body:
37
+ html: |
38
+ <p>Hello,</p>
39
+ <p>We wanted to notify you that your webhook endpoint "<strong>%{endpoint_name}</strong>" has been automatically deactivated due to repeated delivery failures.</p>
40
+ details:
41
+ html: |
42
+ <p>Endpoint Name: <strong>%{endpoint_name}</strong></p>
43
+ <p>Endpoint Events: <strong>%{endpoint_events}</strong></p>
44
+ explanation:
45
+ heading: "What this means:"
46
+ text: "Your webhook endpoint has been deactivated to prevent further failed delivery attempts. No webhook events will be sent to this endpoint until it is reactivated."
47
+ action:
48
+ heading: "What you can do:"
49
+ html: |
50
+ <ul>
51
+ <li>Leave it deactivated</li>
52
+ <li>Open the endpoint and reactivate it again if you fixed the initial delivery error</li>
53
+ </ul>
54
+ button: *open_endpoint
55
+ signature: *SIGNATURE
@@ -16,11 +16,14 @@ en:
16
16
  shorthand:
17
17
  edit: Settings
18
18
  destroy: Delete
19
+ activate: Activate
20
+ deactivate: Deactivate
19
21
  confirmations:
20
22
  destroy: Are you sure you want to remove %{endpoint_name}? This will also remove it's associated data. This can't be undone.
21
23
  rotate_secret: >
22
24
  Are you sure you want to rotate the secret of the endpoint %{endpoint_name}?
23
25
  Your webhook event verification system will need to start handling the new secret.
26
+ deactivate: Are you sure you want to deactivate %{endpoint_name}? This will prevent webhooks from being sent to this endpoint.
24
27
  fields: &fields
25
28
  id:
26
29
  _: &id Endpoint ID
@@ -68,7 +71,8 @@ en:
68
71
  all: All Events
69
72
  event_types: *event_types
70
73
 
71
- scaffolding_absolutely_abstract_creative_concept_id: &scaffolding_absolutely_abstract_creative_concept
74
+ scaffolding_absolutely_abstract_creative_concept_id:
75
+ &scaffolding_absolutely_abstract_creative_concept
72
76
  _: &scaffolding_absolutely_abstract_creative_concept_id Within Creative Concept
73
77
  label: *scaffolding_absolutely_abstract_creative_concept_id
74
78
  heading: *scaffolding_absolutely_abstract_creative_concept_id
@@ -86,6 +90,28 @@ en:
86
90
  via the API, the secret is only shown after creating the endpoint. If you missed to store
87
91
  it, you will need to look it up on the app's UI endpoint show or index pages.
88
92
  truncation_with_instructions: ... (use button below to rotate)
93
+
94
+ deactivation_limit_reached_at:
95
+ _: &deactivation_limit_reached_at Deactivation Limit Reached At
96
+ label: *deactivation_limit_reached_at
97
+ heading: *deactivation_limit_reached_at
98
+ api_title: *deactivation_limit_reached_at
99
+ api_description: *deactivation_limit_reached_at
100
+
101
+ deactivated_at:
102
+ _: &deactivated_at Deactivated At
103
+ label: *deactivated_at
104
+ heading: *deactivated_at
105
+ api_title: *deactivated_at
106
+ api_description: *deactivated_at
107
+
108
+ consecutive_failed_deliveries:
109
+ _: &consecutive_failed_deliveries Consecutive Failed Deliveries
110
+ label: *consecutive_failed_deliveries
111
+ heading: *consecutive_failed_deliveries
112
+ api_title: *consecutive_failed_deliveries
113
+ api_description: *consecutive_failed_deliveries
114
+
89
115
  # 🚅 super scaffolding will insert new fields above this line.
90
116
  created_at:
91
117
  _: &created_at Added
@@ -118,6 +144,12 @@ en:
118
144
  description_empty: No Endpoints have been added for %{team_name}.
119
145
  fields: *fields
120
146
  buttons: *buttons
147
+ status:
148
+ heading: Status
149
+ active: Active
150
+ deactivated: Deactivated
151
+ deactivation_limit_reached:
152
+ description: ⚠️ You have reached the maximum number of failed deliveries allowed for the endpoint. The limit was reached at %{reached_at}, and the endpoint will soon be deactivated if it does not process the webhooks.
121
153
  show:
122
154
  section: "%{endpoint_name}"
123
155
  header: Webhooks Endpoint Details
@@ -143,6 +175,10 @@ en:
143
175
  updated: Endpoint was successfully updated.
144
176
  destroyed: Endpoint was successfully destroyed.
145
177
  secret_rotated: Endpoint's webhook secret successfully rotated.
178
+ activated: Endpoint was successfully activated.
179
+ deactivated: Endpoint was successfully deactivated.
180
+ activation_failed: Failed to activate endpoint.
181
+ deactivation_failed: Failed to deactivate endpoint.
146
182
  account:
147
183
  webhooks:
148
184
  outgoing:
data/config/routes.rb CHANGED
@@ -7,6 +7,10 @@ Rails.application.routes.draw do
7
7
  namespace :webhooks do
8
8
  namespace :outgoing do
9
9
  resources :endpoints do
10
+ member do
11
+ post :activate
12
+ delete :deactivate
13
+ end
10
14
  resources :deliveries, only: %i[index show] do
11
15
  resources :delivery_attempts, only: %i[index show]
12
16
  end
@@ -0,0 +1,10 @@
1
+ class AddDeactivationFieldsToEndpoints < ActiveRecord::Migration[8.0]
2
+ disable_ddl_transaction!
3
+
4
+ def change
5
+ add_column :webhooks_outgoing_endpoints, :deactivation_limit_reached_at, :datetime
6
+ add_column :webhooks_outgoing_endpoints, :deactivated_at, :datetime
7
+ parent_association = BulletTrain::OutgoingWebhooks.parent_association.to_s.foreign_key.to_sym
8
+ add_index :webhooks_outgoing_endpoints, [parent_association, :deactivated_at], algorithm: :concurrently
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ class AddInvitationsTable < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table "invitations", id: :serial, force: :cascade do |t|
4
+ t.string "email"
5
+ t.string "uuid"
6
+ t.integer "from_membership_id"
7
+ t.datetime "created_at", precision: nil, null: false
8
+ t.datetime "updated_at", precision: nil, null: false
9
+ t.integer "team_id"
10
+ t.bigint "invitation_list_id"
11
+ t.index ["invitation_list_id"], name: "index_invitations_on_invitation_list_id"
12
+ t.index ["team_id"], name: "index_invitations_on_team_id"
13
+ end
14
+ add_reference :memberships, :invitation, index: true
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ class UpdateUsersTableForDummyApp < ActiveRecord::Migration[8.0]
2
+ def change
3
+ change_table :users do |t|
4
+ t.string "reset_password_token"
5
+ t.datetime "reset_password_sent_at", precision: nil
6
+ t.datetime "remember_created_at", precision: nil
7
+ t.integer "sign_in_count", default: 0, null: false
8
+ t.datetime "current_sign_in_at", precision: nil
9
+ t.datetime "last_sign_in_at", precision: nil
10
+ t.inet "current_sign_in_ip"
11
+ t.inet "last_sign_in_ip"
12
+ t.datetime "last_seen_at", precision: nil
13
+ t.string "profile_photo_id"
14
+ t.datetime "last_notification_email_sent_at", precision: nil
15
+ t.boolean "former_user", default: false, null: false
16
+ t.string "encrypted_otp_secret"
17
+ t.string "encrypted_otp_secret_iv"
18
+ t.string "encrypted_otp_secret_salt"
19
+ t.integer "consumed_timestep"
20
+ t.boolean "otp_required_for_login"
21
+ t.string "otp_backup_codes", array: true
22
+ t.string "locale"
23
+ t.bigint "platform_agent_of_id"
24
+ t.string "otp_secret"
25
+ t.integer "failed_attempts", default: 0, null: false
26
+ t.string "unlock_token"
27
+ t.datetime "locked_at"
28
+ t.index ["email"], name: "index_users_on_email", unique: true
29
+ t.index ["platform_agent_of_id"], name: "index_users_on_platform_agent_of_id"
30
+ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
31
+ t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ class AddLocaleToTeam < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :teams, :locale, :string
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddFailedCountToWebhookEndpoints < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :webhooks_outgoing_endpoints, :consecutive_failed_deliveries, :integer, default: 0, null: false
4
+ end
5
+ end
@@ -24,7 +24,12 @@ module BulletTrain
24
24
  custom_allow_callback: nil,
25
25
  audit_callback: ->(obj, uri) { Rails.logger.error("BlockedURI obj=#{obj.persisted? ? obj.to_global_id : "New #{obj.class}"} uri=#{uri}") },
26
26
  webhook_headers_namespace: "X-Webhook-Bullet-Train",
27
- event_verification_tolerance_seconds: 600 # 5-10 minutes is industry-default.
27
+ event_verification_tolerance_seconds: 600, # 5-10 minutes is industry-default.
28
+ automatic_endpoint_deactivation_enabled: false,
29
+ automatic_endpoint_deactivation_settings: {
30
+ max_limit: 50,
31
+ deactivation_in: 3.days
32
+ },
28
33
  }
29
34
  end
30
35
  end
@@ -1,5 +1,5 @@
1
1
  module BulletTrain
2
2
  module OutgoingWebhooks
3
- VERSION = "1.25.1"
3
+ VERSION = "1.26.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet_train-outgoing_webhooks
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.25.1
4
+ version: 1.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Culver
@@ -117,8 +117,10 @@ files:
117
117
  - app/controllers/concerns/api/v1/webhooks/outgoing/events/controller_base.rb
118
118
  - app/jobs/webhooks/outgoing/delivery_job.rb
119
119
  - app/jobs/webhooks/outgoing/generate_job.rb
120
+ - app/mailers/webhooks/outgoing/endpoint_mailer.rb
120
121
  - app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb
121
122
  - app/models/concerns/webhooks/outgoing/delivery_support.rb
123
+ - app/models/concerns/webhooks/outgoing/endpoint_deactivatable.rb
122
124
  - app/models/concerns/webhooks/outgoing/endpoint_support.rb
123
125
  - app/models/concerns/webhooks/outgoing/event_support.rb
124
126
  - app/models/concerns/webhooks/outgoing/issuing_model.rb
@@ -163,8 +165,11 @@ files:
163
165
  - app/views/api/v1/webhooks/outgoing/events/_event.json.jbuilder
164
166
  - app/views/api/v1/webhooks/outgoing/events/index.json.jbuilder
165
167
  - app/views/api/v1/webhooks/outgoing/events/show.json.jbuilder
168
+ - app/views/webhooks/outgoing/endpoint_mailer/deactivated.html.erb
169
+ - app/views/webhooks/outgoing/endpoint_mailer/deactivation_limit_reached.html.erb
166
170
  - config/locales/en/webhooks/outgoing/deliveries.en.yml
167
171
  - config/locales/en/webhooks/outgoing/delivery_attempts.en.yml
172
+ - config/locales/en/webhooks/outgoing/endpoint_mailer.en.yml
168
173
  - config/locales/en/webhooks/outgoing/endpoints.en.yml
169
174
  - config/locales/en/webhooks/outgoing/events.en.yml
170
175
  - config/routes.rb
@@ -194,6 +199,11 @@ files:
194
199
  - db/migrate/20221230235326_add_api_version_to_webhooks_outgoing_events.rb
195
200
  - db/migrate/20221231003437_remove_default_from_webhooks_outgoing_endpoints.rb
196
201
  - db/migrate/20221231003438_remove_default_from_webhooks_outgoing_events.rb
202
+ - db/migrate/20250522080219_add_deactivation_fields_to_endpoints.rb
203
+ - db/migrate/20250526132225_add_invitations_table.rb
204
+ - db/migrate/20250528082127_update_users_table_for_dummy_app.rb
205
+ - db/migrate/20250528083628_add_locale_to_team.rb
206
+ - db/migrate/20250616143032_add_failed_count_to_webhook_endpoints.rb
197
207
  - lib/bullet_train/outgoing_webhooks.rb
198
208
  - lib/bullet_train/outgoing_webhooks/engine.rb
199
209
  - lib/bullet_train/outgoing_webhooks/signature.rb