bullet_train-outgoing_webhooks 1.25.0 → 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.
- checksums.yaml +4 -4
- data/app/controllers/account/webhooks/outgoing/endpoints_controller.rb +30 -1
- data/app/controllers/concerns/api/v1/webhooks/outgoing/endpoints/controller_base.rb +1 -0
- data/app/mailers/webhooks/outgoing/endpoint_mailer.rb +35 -0
- data/app/models/concerns/webhooks/outgoing/delivery_support.rb +35 -3
- data/app/models/concerns/webhooks/outgoing/endpoint_deactivatable.rb +78 -0
- data/app/models/webhooks/outgoing/endpoint.rb +1 -0
- data/app/views/account/webhooks/outgoing/deliveries/_index.html.erb +1 -1
- data/app/views/account/webhooks/outgoing/deliveries/show.html.erb +1 -1
- data/app/views/account/webhooks/outgoing/endpoints/_index.html.erb +19 -0
- data/app/views/api/v1/webhooks/outgoing/endpoints/_endpoint.json.jbuilder +3 -0
- data/app/views/webhooks/outgoing/endpoint_mailer/deactivated.html.erb +49 -0
- data/app/views/webhooks/outgoing/endpoint_mailer/deactivation_limit_reached.html.erb +45 -0
- data/config/locales/en/webhooks/outgoing/endpoint_mailer.en.yml +55 -0
- data/config/locales/en/webhooks/outgoing/endpoints.en.yml +37 -1
- data/config/routes.rb +4 -0
- data/db/migrate/20250522080219_add_deactivation_fields_to_endpoints.rb +10 -0
- data/db/migrate/20250526132225_add_invitations_table.rb +16 -0
- data/db/migrate/20250528082127_update_users_table_for_dummy_app.rb +34 -0
- data/db/migrate/20250528083628_add_locale_to_team.rb +5 -0
- data/db/migrate/20250616143032_add_failed_count_to_webhook_endpoints.rb +5 -0
- data/lib/bullet_train/outgoing_webhooks/engine.rb +6 -1
- data/lib/bullet_train/outgoing_webhooks/version.rb +1 -1
- metadata +11 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36479ad4e919366dc9e89a68a38682e16806e5fc13871afca777677fe78e03c6
|
4
|
+
data.tar.gz: ee4b1266f2988a289b295708d90b45ede44e0e9dde8a0e890e992a0b41275611
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
!
|
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
|
@@ -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: :
|
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: :
|
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 %>
|
@@ -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:
|
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
|
@@ -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
|
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.
|
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
|