fuik 0.11.0 → 0.13.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/README.md +162 -25
- data/app/assets/images/fuik/icons/46elks.jpg +0 -0
- data/app/assets/images/fuik/icons/lettermint.jpg +0 -0
- data/app/assets/images/fuik/icons/smtp2go.jpg +0 -0
- data/app/assets/javascript/attractivejs-0.12.0.min.js +1 -0
- data/app/assets/stylesheets/fuik/application.css +204 -81
- data/app/controllers/concerns/fuik/event_type.rb +32 -7
- data/app/controllers/fuik/events_controller.rb +7 -2
- data/app/controllers/fuik/retries_controller.rb +12 -0
- data/app/controllers/fuik/webhooks_controller.rb +29 -8
- data/app/jobs/fuik/webhook_processing_job.rb +21 -0
- data/app/models/fuik/event.rb +36 -0
- data/app/models/fuik/webhook_event/filterable.rb +25 -0
- data/app/models/fuik/webhook_event.rb +34 -1
- data/app/views/fuik/events/_filters.html.erb +29 -0
- data/app/views/fuik/events/index.html.erb +7 -1
- data/app/views/fuik/events/show.html.erb +43 -20
- data/app/views/layouts/fuik/application.html.erb +12 -2
- data/config/routes.rb +4 -1
- data/lib/fuik/configuration.rb +29 -0
- data/lib/fuik/engine.rb +0 -4
- data/lib/fuik/errors.rb +5 -0
- data/lib/fuik/notifications.rb +48 -0
- data/lib/fuik/routing/provider_constraint.rb +4 -4
- data/lib/fuik/test_helpers.rb +33 -0
- data/lib/fuik/version.rb +1 -1
- data/lib/fuik.rb +9 -0
- data/lib/generators/fuik/provider/templates/base.rb.tt +11 -2
- data/lib/generators/fuik/provider/templates/github/base.rb.tt +3 -0
- data/lib/generators/fuik/provider/templates/lettermint/base.rb.tt +32 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_failed.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_hard_bounced.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_soft_bounced.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_suppressed.rb.tt +15 -0
- data/lib/generators/fuik/provider/templates/loops/base.rb.tt +2 -0
- data/lib/generators/fuik/provider/templates/postmark/base.rb.tt +1 -0
- data/lib/generators/fuik/provider/templates/smtp2go/base.rb.tt +4 -0
- data/lib/generators/fuik/provider/templates/smtp2go/bounce.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/smtp2go/reject.rb.tt +14 -0
- data/lib/generators/fuik/provider/templates/smtp2go/spam.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/smtp2go/unsubscribe.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/spinal/base.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/spinal/collection_synced.rb.tt +14 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_created.rb.tt +14 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_pushed.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_scheduled.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_unscheduled.rb.tt +12 -0
- metadata +32 -4
|
@@ -5,23 +5,44 @@ module Fuik
|
|
|
5
5
|
private
|
|
6
6
|
|
|
7
7
|
COMMON_EVENT_TYPE_HEADERS = [
|
|
8
|
-
"X-Github-Event",
|
|
9
8
|
"X-Event-Type",
|
|
10
|
-
"X-Webhook-Event"
|
|
9
|
+
"X-Webhook-Event",
|
|
10
|
+
"Webhook-Event"
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
COMMON_EVENT_ID_HEADERS = [
|
|
14
|
-
"X-GitHub-Delivery",
|
|
15
14
|
"X-Event-Id",
|
|
16
|
-
"X-Webhook-Id"
|
|
15
|
+
"X-Webhook-Id",
|
|
16
|
+
"Webhook-Id",
|
|
17
|
+
"webhook-id"
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
def event_type
|
|
20
|
-
from_config("event_type") || from_event_type_headers || from_payload_type || "unknown"
|
|
21
|
+
from_base_class_event_type || from_config("event_type") || from_event_type_headers || from_payload_type || "unknown"
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def event_id
|
|
24
|
-
from_config("event_id") || from_event_id_headers || payload["id"] || Digest::MD5.hexdigest(request.raw_post)
|
|
25
|
+
from_base_class_event_id || from_config("event_id") || from_event_id_headers || payload["id"] || Digest::MD5.hexdigest(request.raw_post)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def from_base_class_event_type
|
|
29
|
+
config = base_class&.event_type_config
|
|
30
|
+
|
|
31
|
+
return if !config
|
|
32
|
+
return config if config.is_a?(String)
|
|
33
|
+
return request.headers[config[:header]] if config.key?(:header)
|
|
34
|
+
|
|
35
|
+
payload.dig(*config[:payload].to_s.split(".")) if config.key?(:payload)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def from_base_class_event_id
|
|
39
|
+
config = base_class&.event_id_config
|
|
40
|
+
|
|
41
|
+
return if !config
|
|
42
|
+
return config if config.is_a?(String)
|
|
43
|
+
return request.headers[config[:header]] if config.key?(:header)
|
|
44
|
+
|
|
45
|
+
payload.dig(*config[:payload].to_s.split(".")) if config.key?(:payload)
|
|
25
46
|
end
|
|
26
47
|
|
|
27
48
|
def from_config(key)
|
|
@@ -53,7 +74,11 @@ module Fuik
|
|
|
53
74
|
@config ||= begin
|
|
54
75
|
config_path = Rails.root.join("app/webhooks/#{params[:provider]}/config.yml")
|
|
55
76
|
|
|
56
|
-
File.exist?(config_path)
|
|
77
|
+
if File.exist?(config_path)
|
|
78
|
+
Fuik.deprecator.warn("Using config.yml is deprecated. Define event_type/event_id on #{params[:provider].camelize}::Base instead.")
|
|
79
|
+
|
|
80
|
+
YAML.load_file(config_path)
|
|
81
|
+
end
|
|
57
82
|
end
|
|
58
83
|
end
|
|
59
84
|
end
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Fuik
|
|
4
|
-
class EventsController < Fuik
|
|
4
|
+
class EventsController < Fuik.configuration.events_controller_parent.constantize
|
|
5
5
|
layout "fuik/application"
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
|
-
@webhook_events = WebhookEvent.order(created_at: :desc)
|
|
8
|
+
@webhook_events = WebhookEvent.filtered(params).order(created_at: :desc)
|
|
9
|
+
|
|
10
|
+
respond_to do |format|
|
|
11
|
+
format.html
|
|
12
|
+
format.json { render json: @webhook_events }
|
|
13
|
+
end
|
|
9
14
|
end
|
|
10
15
|
|
|
11
16
|
def show
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fuik
|
|
4
|
+
class RetriesController < Fuik.configuration.events_controller_parent.constantize
|
|
5
|
+
def create
|
|
6
|
+
webhook_event = WebhookEvent.find(params[:event_id])
|
|
7
|
+
webhook_event.retry!
|
|
8
|
+
|
|
9
|
+
redirect_to event_path(webhook_event)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Fuik
|
|
4
|
-
class WebhooksController < Fuik
|
|
4
|
+
class WebhooksController < Fuik.configuration.webhooks_controller_parent.constantize
|
|
5
5
|
include EventType
|
|
6
6
|
|
|
7
7
|
skip_before_action :verify_authenticity_token
|
|
@@ -13,23 +13,44 @@ module Fuik
|
|
|
13
13
|
provider: params[:provider],
|
|
14
14
|
event_id: event_id,
|
|
15
15
|
event_type: event_type,
|
|
16
|
-
body:
|
|
16
|
+
body: json_body,
|
|
17
17
|
headers: headers
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Notifications.received(
|
|
21
|
+
provider: params[:provider],
|
|
22
|
+
event_id: event_id,
|
|
23
|
+
event_type: event_type,
|
|
24
|
+
webhook_event: webhook_event
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
webhook_event.process_later!
|
|
21
28
|
|
|
22
29
|
head :ok
|
|
23
30
|
rescue Fuik::InvalidSignature
|
|
31
|
+
Notifications.signature_invalid(
|
|
32
|
+
provider: params[:provider],
|
|
33
|
+
event_id: event_id
|
|
34
|
+
)
|
|
24
35
|
head :unauthorized
|
|
25
36
|
rescue ActiveRecord::RecordNotUnique
|
|
26
37
|
head :ok
|
|
27
|
-
rescue
|
|
38
|
+
rescue => error
|
|
39
|
+
Notifications.receive_error(
|
|
40
|
+
provider: request.path_parameters[:provider],
|
|
41
|
+
error: error
|
|
42
|
+
)
|
|
28
43
|
head :internal_server_error
|
|
29
44
|
end
|
|
30
45
|
|
|
31
46
|
private
|
|
32
47
|
|
|
48
|
+
def json_body
|
|
49
|
+
return request.raw_post unless url_encoded_body?
|
|
50
|
+
|
|
51
|
+
Rack::Utils.parse_nested_query(request.raw_post).to_json
|
|
52
|
+
end
|
|
53
|
+
|
|
33
54
|
def verify_signature!
|
|
34
55
|
return unless should_verify?
|
|
35
56
|
|
|
@@ -47,21 +68,21 @@ module Fuik
|
|
|
47
68
|
@payload ||= begin
|
|
48
69
|
return {} if request.raw_post.blank?
|
|
49
70
|
|
|
50
|
-
JSON.parse(
|
|
71
|
+
JSON.parse(json_body)
|
|
51
72
|
rescue JSON::ParserError
|
|
52
73
|
{}
|
|
53
74
|
end
|
|
54
75
|
end
|
|
55
76
|
|
|
56
77
|
def process!(webhook_event)
|
|
57
|
-
event_class = event_class_for(webhook_event.provider, webhook_event.event_type)
|
|
78
|
+
event_class = WebhookEvent.event_class_for(webhook_event.provider, webhook_event.event_type)
|
|
58
79
|
return unless event_class
|
|
59
80
|
|
|
60
81
|
event_class.new(webhook_event).process!
|
|
61
82
|
end
|
|
62
83
|
|
|
63
|
-
def
|
|
64
|
-
|
|
84
|
+
def url_encoded_body?
|
|
85
|
+
request.media_type == "application/x-www-form-urlencoded"
|
|
65
86
|
end
|
|
66
87
|
|
|
67
88
|
def should_verify? = base_class&.respond_to?(:verify!)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Fuik
|
|
2
|
+
class WebhookProcessingJob < ApplicationJob
|
|
3
|
+
queue_as :default
|
|
4
|
+
|
|
5
|
+
discard_on ActiveJob::DeserializationError
|
|
6
|
+
|
|
7
|
+
def perform(event_class_name, webhook_event)
|
|
8
|
+
Object.const_get(event_class_name).new(webhook_event).process!
|
|
9
|
+
rescue => error
|
|
10
|
+
webhook_event.failed!(error)
|
|
11
|
+
|
|
12
|
+
Notifications.failed(
|
|
13
|
+
webhook_event: webhook_event,
|
|
14
|
+
event_class: event_class_name,
|
|
15
|
+
error: error
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
raise
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/app/models/fuik/event.rb
CHANGED
|
@@ -4,14 +4,50 @@ module Fuik
|
|
|
4
4
|
class Event
|
|
5
5
|
using DotAccess
|
|
6
6
|
|
|
7
|
+
class_attribute :event_type_config, default: nil
|
|
8
|
+
class_attribute :event_id_config, default: nil
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def event_type(key = nil, **options)
|
|
12
|
+
self.event_type_config = key || options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def event_id(key = nil, **options)
|
|
16
|
+
self.event_id_config = key || options
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
7
20
|
def initialize(webhook_event)
|
|
8
21
|
@webhook_event = webhook_event
|
|
9
22
|
end
|
|
10
23
|
|
|
24
|
+
# Called when the webhook event is processed. Override to handle the event
|
|
25
|
+
# payload and transition the event's status to processed or failed.
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
#
|
|
29
|
+
# @example Implementing a checkout.session.completed handler
|
|
30
|
+
# def process!
|
|
31
|
+
# User.find_by(id: payload.client_reference_id).tap do |user|
|
|
32
|
+
# user.activate_subscription!
|
|
33
|
+
# user.send_welcome_email
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# @webhook_event.processed!
|
|
37
|
+
# end
|
|
11
38
|
def process!
|
|
12
39
|
raise NotImplementedError, "#{self.class} must implement #process!"
|
|
13
40
|
end
|
|
14
41
|
|
|
42
|
+
# Access the webhook payload with dot notation or standard hash access.
|
|
43
|
+
#
|
|
44
|
+
# @return [Fuik::DotAccess::AccessPayload] the parsed JSON body wrapped in a dot-accessible object
|
|
45
|
+
#
|
|
46
|
+
# @example Dot notation
|
|
47
|
+
# payload.customer.email
|
|
48
|
+
#
|
|
49
|
+
# @example Hash access
|
|
50
|
+
# payload["line_items"][0]["product_id"]
|
|
15
51
|
def payload
|
|
16
52
|
@webhook_event.payload.to_dot
|
|
17
53
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fuik
|
|
4
|
+
class WebhookEvent
|
|
5
|
+
module Filterable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def filtered(params)
|
|
10
|
+
filters = %w[status provider event_id event_type]
|
|
11
|
+
|
|
12
|
+
where(params.permit(*filters).compact_blank.transform_values { it.split(",") })
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def options_for_select
|
|
16
|
+
by_provider_name.pluck(:provider).map { [it.humanize, it] }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
included do
|
|
21
|
+
scope :by_provider_name, -> { distinct.order(:provider) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -4,6 +4,12 @@ module Fuik
|
|
|
4
4
|
class WebhookEvent < ApplicationRecord
|
|
5
5
|
self.table_name = "fuik_webhook_events"
|
|
6
6
|
|
|
7
|
+
def self.event_class_for(provider, event_type)
|
|
8
|
+
"#{provider.camelize}::#{event_type.tr("./:-[]", "_").camelize}".safe_constantize
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
include Filterable
|
|
12
|
+
|
|
7
13
|
enum :status, %w[pending processed failed].index_by(&:itself), default: "pending"
|
|
8
14
|
|
|
9
15
|
validates :provider, presence: true
|
|
@@ -15,12 +21,39 @@ module Fuik
|
|
|
15
21
|
{}
|
|
16
22
|
end
|
|
17
23
|
|
|
24
|
+
# Mark the event as successfully processed.
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean] true if the update succeeded
|
|
18
27
|
def processed!
|
|
19
28
|
update!(status: "processed")
|
|
20
29
|
end
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
# Mark the event as failed and record the error.
|
|
32
|
+
#
|
|
33
|
+
# @param error [String, nil] optional error message or object
|
|
34
|
+
#
|
|
35
|
+
# @return [Boolean] true if the update succeeded
|
|
36
|
+
def failed!(error = nil)
|
|
23
37
|
update!(status: "failed", error: error.to_s)
|
|
24
38
|
end
|
|
39
|
+
|
|
40
|
+
# Retry a failed event. Resets its status back to pending, clears the error,
|
|
41
|
+
# and enqueues processing via WebhookProcessingJob.
|
|
42
|
+
#
|
|
43
|
+
# @raise [RuntimeError] if the event is not in a failed state
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] true if the update succeeded
|
|
46
|
+
def retry!
|
|
47
|
+
raise "Can only retry failed events" unless failed?
|
|
48
|
+
|
|
49
|
+
update!(status: "pending", error: nil)
|
|
50
|
+
process_later!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def process_later!
|
|
54
|
+
event_class = self.class.event_class_for(provider, event_type)
|
|
55
|
+
|
|
56
|
+
Fuik.webhook_processing_job_class.constantize.perform_later(event_class.name, self) if event_class
|
|
57
|
+
end
|
|
25
58
|
end
|
|
26
59
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<%= form_tag root_path, method: :get, id: :filters do %>
|
|
2
|
+
<div class="filters">
|
|
3
|
+
<fieldset>
|
|
4
|
+
<legend>Provider</legend>
|
|
5
|
+
|
|
6
|
+
<%= select_tag :provider, options_for_select(Fuik::WebhookEvent.options_for_select, params[:provider]), prompt: "All", data: {action: "form#submit", target: "filters"} %>
|
|
7
|
+
</fieldset>
|
|
8
|
+
|
|
9
|
+
<fieldset>
|
|
10
|
+
<legend>Status</legend>
|
|
11
|
+
|
|
12
|
+
<span class="pills">
|
|
13
|
+
<%= label_tag nil, class: "pill" do %>
|
|
14
|
+
<%= radio_button_tag :status, "", params[:status].blank? || params[:status].empty?, data: {action: "form#submit", target: "filters"} %>
|
|
15
|
+
All
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<% Fuik::WebhookEvent.statuses.keys.each do |status| %>
|
|
19
|
+
<%= label_tag nil, class: "pill" do %>
|
|
20
|
+
<%= radio_button_tag :status, status, params[:status] == status, data: {action: "form#submit", target: "filters"} %>
|
|
21
|
+
<%= status.capitalize %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</span>
|
|
25
|
+
</fieldset>
|
|
26
|
+
|
|
27
|
+
<%= submit_tag "Filter" %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
<h1
|
|
1
|
+
<h1 class="heading"><%= Fuik.configuration.title %></h1>
|
|
2
|
+
|
|
3
|
+
<%= render "filters" %>
|
|
2
4
|
|
|
3
5
|
<ul class="events">
|
|
4
6
|
<% @webhook_events.each do |event| %>
|
|
@@ -18,4 +20,8 @@
|
|
|
18
20
|
<% end %>
|
|
19
21
|
</li>
|
|
20
22
|
<% end %>
|
|
23
|
+
|
|
24
|
+
<li class="empty-state">
|
|
25
|
+
<p>No webhook events…</p>
|
|
26
|
+
</li>
|
|
21
27
|
</ul>
|
|
@@ -1,34 +1,57 @@
|
|
|
1
|
-
<% content_for :title, "#{@webhook_event.provider} / #{@webhook_event.event_type}
|
|
1
|
+
<% content_for :title, "#{@webhook_event.provider} / #{@webhook_event.event_type} - #{Fuik.configuration.title}" %>
|
|
2
2
|
|
|
3
|
-
<h1>
|
|
4
|
-
<%= link_to
|
|
3
|
+
<h1 class="heading">
|
|
4
|
+
<%= link_to Fuik.configuration.title, root_path %> <span class="divider">/</span> <%= @webhook_event.event_id %>
|
|
5
5
|
</h1>
|
|
6
6
|
|
|
7
|
-
<dl>
|
|
8
|
-
<dt>Provider</dt>
|
|
9
|
-
<dd
|
|
7
|
+
<dl class="details">
|
|
8
|
+
<dt class="description">Provider</dt>
|
|
9
|
+
<dd class="value">
|
|
10
|
+
<%= fuik_icon @webhook_event.provider %> <%= @webhook_event.provider %>
|
|
11
|
+
</dd>
|
|
10
12
|
|
|
11
|
-
<dt>Event ID</dt>
|
|
12
|
-
<dd
|
|
13
|
+
<dt class="description">Event ID</dt>
|
|
14
|
+
<dd class="value">
|
|
15
|
+
<%= tag.code @webhook_event.event_id, id: "event_id", data: {action: "copy", copy_delay: 5000} %> <%= render "copy_button", target: "event_id" %>
|
|
16
|
+
</dd>
|
|
13
17
|
|
|
14
|
-
<dt>Type</dt>
|
|
15
|
-
<dd
|
|
18
|
+
<dt class="description">Type</dt>
|
|
19
|
+
<dd class="value">
|
|
20
|
+
<%= @webhook_event.event_type %>
|
|
21
|
+
</dd>
|
|
16
22
|
|
|
17
|
-
<dt>Status</dt>
|
|
18
|
-
<dd class="status" data-status="<%= @webhook_event.status %>"
|
|
23
|
+
<dt class="description">Status</dt>
|
|
24
|
+
<dd class="value status" data-status="<%= @webhook_event.status %>">
|
|
25
|
+
<%= tag.span @webhook_event.status %>
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
<%= button_to event_retries_path(@webhook_event), method: :post, class: "retry-button" do %>
|
|
28
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.6a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0v29.8L60.17,60.29h0A96,96,0,0,1,224,128Z"/></svg>
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
Retry
|
|
31
|
+
<% end if @webhook_event.failed? %>
|
|
32
|
+
</dd>
|
|
25
33
|
|
|
26
|
-
<dt>
|
|
27
|
-
<dd
|
|
34
|
+
<dt class="description">Created</dt>
|
|
35
|
+
<dd class="value">
|
|
36
|
+
<%= @webhook_event.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
37
|
+
</dd>
|
|
38
|
+
|
|
39
|
+
<dt class="description">Payload</dt>
|
|
40
|
+
<dd class="value">
|
|
41
|
+
<pre id="payload" data-root="payload" class="json-highlight"><%= highlighted(JSON.pretty_generate(@webhook_event.payload)) %></pre> <div class="actions"><%= button_to downloads_path, params: { event_id: @webhook_event.id }, method: :post, class: "" do %><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34Zm-56,83.32-24,24a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L120,164.69V120a8,8,0,0,1,16,0v44.69l10.34-10.35a8,8,0,0,1,11.32,11.32ZM152,88V44l44,44Z"/></svg><% end %> <%= render "copy_button", target: "payload" %></div>
|
|
42
|
+
</dd>
|
|
43
|
+
|
|
44
|
+
<dt class="description">Headers</dt>
|
|
45
|
+
<dd class="value">
|
|
46
|
+
<pre id="headers" class="json-highlight"><%= highlighted JSON.pretty_generate(@webhook_event.headers) %></pre> <div class="actions"><%= render "copy_button", target: "headers" %></div>
|
|
47
|
+
</dd>
|
|
28
48
|
|
|
29
49
|
<% if @webhook_event.error.present? %>
|
|
30
|
-
<dt>Error</dt>
|
|
31
|
-
|
|
50
|
+
<dt class="description">Error</dt>
|
|
51
|
+
|
|
52
|
+
<dd class="value errors">
|
|
53
|
+
<%= @webhook_event.error %>
|
|
54
|
+
</dd>
|
|
32
55
|
<% end %>
|
|
33
56
|
</dl>
|
|
34
57
|
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
|
-
<title><%= content_for?(:title) ? yield(:title) :
|
|
4
|
+
<title><%= content_for?(:title) ? yield(:title) : Fuik.configuration.title %></title>
|
|
5
5
|
<%= csrf_meta_tags %>
|
|
6
6
|
<%= csp_meta_tag %>
|
|
7
7
|
|
|
8
8
|
<%= yield :head %>
|
|
9
9
|
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
color-scheme: <%= {
|
|
13
|
+
auto: "light dark",
|
|
14
|
+
light: "light",
|
|
15
|
+
dark: "dark"
|
|
16
|
+
}.fetch(Fuik.configuration.color_scheme.to_sym) %>;
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
19
|
+
|
|
10
20
|
<%= stylesheet_link_tag "fuik/application", media: "all" %>
|
|
11
|
-
<script
|
|
21
|
+
<script src="<%= asset_path("attractivejs-0.12.0.min.js") %>"></script>
|
|
12
22
|
</head>
|
|
13
23
|
<body>
|
|
14
24
|
<main>
|
data/config/routes.rb
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Fuik::Engine.routes.draw do
|
|
4
4
|
root to: "events#index"
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
resources :events, only: %w[index show] do
|
|
7
|
+
resources :retries, only: %w[create]
|
|
8
|
+
end
|
|
6
9
|
resources :downloads, only: %w[create]
|
|
7
10
|
|
|
8
11
|
post ":provider", to: "webhooks#create", constraints: Fuik::Routing::ProviderConstraint.new
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fuik
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :events_controller_parent, :webhooks_controller_parent,
|
|
6
|
+
:providers_allowed, :title, :color_scheme, :webhook_processing_job_class
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@events_controller_parent = "ActionController::Base"
|
|
10
|
+
@webhooks_controller_parent = "ActionController::Base"
|
|
11
|
+
@webhook_processing_job_class = "Fuik::WebhookProcessingJob"
|
|
12
|
+
@providers_allowed = nil
|
|
13
|
+
@title = "Webhooks"
|
|
14
|
+
@color_scheme = :light
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def configuration
|
|
20
|
+
@configuration ||= Configuration.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def configure
|
|
24
|
+
yield configuration
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
delegate :webhook_processing_job_class, :webhook_processing_job_class=, to: :configuration
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/fuik/engine.rb
CHANGED
|
@@ -6,10 +6,6 @@ module Fuik
|
|
|
6
6
|
class Engine < ::Rails::Engine
|
|
7
7
|
isolate_namespace Fuik
|
|
8
8
|
|
|
9
|
-
config.webhooks_controller_parent = "ActionController::Base"
|
|
10
|
-
config.events_controller_parent = "ActionController::Base"
|
|
11
|
-
config.providers_allowed = Rails.env.local?
|
|
12
|
-
|
|
13
9
|
config.to_prepare do
|
|
14
10
|
ActiveSupport.on_load(:action_view) do
|
|
15
11
|
include Fuik::IconHelper
|
data/lib/fuik/errors.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fuik
|
|
4
|
+
module Notifications
|
|
5
|
+
class << self
|
|
6
|
+
def received(provider:, event_id:, event_type:, webhook_event:)
|
|
7
|
+
publish("webhook_received",
|
|
8
|
+
provider: provider,
|
|
9
|
+
event_id: event_id,
|
|
10
|
+
event_type: event_type,
|
|
11
|
+
webhook_event: webhook_event)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def processed(webhook_event:, event_class:)
|
|
15
|
+
publish("webhook_processed",
|
|
16
|
+
provider: webhook_event.provider,
|
|
17
|
+
event_id: webhook_event.event_id,
|
|
18
|
+
event_type: webhook_event.event_type,
|
|
19
|
+
event_class: event_class,
|
|
20
|
+
webhook_event: webhook_event)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def failed(webhook_event:, event_class:, error:)
|
|
24
|
+
publish("webhook_failed",
|
|
25
|
+
provider: webhook_event.provider,
|
|
26
|
+
event_id: webhook_event.event_id,
|
|
27
|
+
event_type: webhook_event.event_type,
|
|
28
|
+
event_class: event_class,
|
|
29
|
+
error: error,
|
|
30
|
+
webhook_event: webhook_event)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def signature_invalid(provider:, event_id:)
|
|
34
|
+
publish("webhook_signature_invalid", provider: provider, event_id: event_id)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def receive_error(provider:, error:)
|
|
38
|
+
publish("webhook_receive_error", provider: provider, error: error)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def publish(name, payload = {})
|
|
44
|
+
ActiveSupport::Notifications.publish("#{name}.fuik", payload)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -15,13 +15,13 @@ module Fuik
|
|
|
15
15
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
|
-
def allow_all? = Fuik
|
|
18
|
+
def allow_all? = Fuik.configuration.providers_allowed.in?([:all, "all"])
|
|
19
19
|
|
|
20
|
-
def explicit_allowlist? = Fuik
|
|
20
|
+
def explicit_allowlist? = Fuik.configuration.providers_allowed.is_a?(Array)
|
|
21
21
|
|
|
22
|
-
def explicit_allowlist = Fuik
|
|
22
|
+
def explicit_allowlist = Fuik.configuration.providers_allowed.to_set
|
|
23
23
|
|
|
24
|
-
def providers_allowed = Fuik
|
|
24
|
+
def providers_allowed = Fuik.configuration.providers_allowed
|
|
25
25
|
|
|
26
26
|
def scanned_allowlist
|
|
27
27
|
@scanned_allowlist ||= Dir["#{Rails.root}/app/webhooks/*"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fuik
|
|
4
|
+
module TestHelpers
|
|
5
|
+
def build_webhook_event(provider: "test", event_id: "test_123", event_type: "test", payload: {})
|
|
6
|
+
WebhookEventDouble.new(provider, event_id, event_type, payload)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class WebhookEventDouble
|
|
10
|
+
attr_reader :provider, :event_id, :event_type, :status, :error
|
|
11
|
+
|
|
12
|
+
def initialize(provider, event_id, event_type, payload)
|
|
13
|
+
@provider = provider
|
|
14
|
+
@event_id = event_id
|
|
15
|
+
@event_type = event_type
|
|
16
|
+
@payload = payload
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def payload
|
|
20
|
+
@payload.is_a?(Hash) ? @payload : {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def processed!
|
|
24
|
+
@status = "processed"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def failed!(error)
|
|
28
|
+
@status = "failed"
|
|
29
|
+
@error = error.to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/fuik/version.rb
CHANGED