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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -25
  3. data/app/assets/images/fuik/icons/46elks.jpg +0 -0
  4. data/app/assets/images/fuik/icons/lettermint.jpg +0 -0
  5. data/app/assets/images/fuik/icons/smtp2go.jpg +0 -0
  6. data/app/assets/javascript/attractivejs-0.12.0.min.js +1 -0
  7. data/app/assets/stylesheets/fuik/application.css +204 -81
  8. data/app/controllers/concerns/fuik/event_type.rb +32 -7
  9. data/app/controllers/fuik/events_controller.rb +7 -2
  10. data/app/controllers/fuik/retries_controller.rb +12 -0
  11. data/app/controllers/fuik/webhooks_controller.rb +29 -8
  12. data/app/jobs/fuik/webhook_processing_job.rb +21 -0
  13. data/app/models/fuik/event.rb +36 -0
  14. data/app/models/fuik/webhook_event/filterable.rb +25 -0
  15. data/app/models/fuik/webhook_event.rb +34 -1
  16. data/app/views/fuik/events/_filters.html.erb +29 -0
  17. data/app/views/fuik/events/index.html.erb +7 -1
  18. data/app/views/fuik/events/show.html.erb +43 -20
  19. data/app/views/layouts/fuik/application.html.erb +12 -2
  20. data/config/routes.rb +4 -1
  21. data/lib/fuik/configuration.rb +29 -0
  22. data/lib/fuik/engine.rb +0 -4
  23. data/lib/fuik/errors.rb +5 -0
  24. data/lib/fuik/notifications.rb +48 -0
  25. data/lib/fuik/routing/provider_constraint.rb +4 -4
  26. data/lib/fuik/test_helpers.rb +33 -0
  27. data/lib/fuik/version.rb +1 -1
  28. data/lib/fuik.rb +9 -0
  29. data/lib/generators/fuik/provider/templates/base.rb.tt +11 -2
  30. data/lib/generators/fuik/provider/templates/github/base.rb.tt +3 -0
  31. data/lib/generators/fuik/provider/templates/lettermint/base.rb.tt +32 -0
  32. data/lib/generators/fuik/provider/templates/lettermint/message_failed.rb.tt +16 -0
  33. data/lib/generators/fuik/provider/templates/lettermint/message_hard_bounced.rb.tt +16 -0
  34. data/lib/generators/fuik/provider/templates/lettermint/message_soft_bounced.rb.tt +16 -0
  35. data/lib/generators/fuik/provider/templates/lettermint/message_suppressed.rb.tt +15 -0
  36. data/lib/generators/fuik/provider/templates/loops/base.rb.tt +2 -0
  37. data/lib/generators/fuik/provider/templates/postmark/base.rb.tt +1 -0
  38. data/lib/generators/fuik/provider/templates/smtp2go/base.rb.tt +4 -0
  39. data/lib/generators/fuik/provider/templates/smtp2go/bounce.rb.tt +16 -0
  40. data/lib/generators/fuik/provider/templates/smtp2go/reject.rb.tt +14 -0
  41. data/lib/generators/fuik/provider/templates/smtp2go/spam.rb.tt +13 -0
  42. data/lib/generators/fuik/provider/templates/smtp2go/unsubscribe.rb.tt +13 -0
  43. data/lib/generators/fuik/provider/templates/spinal/base.rb.tt +16 -0
  44. data/lib/generators/fuik/provider/templates/spinal/collection_synced.rb.tt +14 -0
  45. data/lib/generators/fuik/provider/templates/spinal/resource_created.rb.tt +14 -0
  46. data/lib/generators/fuik/provider/templates/spinal/resource_pushed.rb.tt +13 -0
  47. data/lib/generators/fuik/provider/templates/spinal/resource_scheduled.rb.tt +13 -0
  48. data/lib/generators/fuik/provider/templates/spinal/resource_unscheduled.rb.tt +12 -0
  49. 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) ? YAML.load_file(config_path) : nil
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::Engine.config.events_controller_parent.constantize
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::Engine.config.webhooks_controller_parent.constantize
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: request.raw_post,
16
+ body: json_body,
17
17
  headers: headers
18
18
  )
19
19
 
20
- process!(webhook_event)
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(request.raw_post)
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 event_class_for(provider, event_type)
64
- "#{provider.camelize}::#{event_type.tr("./:-[]", "_").camelize}".safe_constantize
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
@@ -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
- def failed!(error)
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>Webhooks</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} | Fuik Admin" %>
1
+ <% content_for :title, "#{@webhook_event.provider} / #{@webhook_event.event_type} - #{Fuik.configuration.title}" %>
2
2
 
3
- <h1>
4
- <%= link_to "Webhooks", root_path %> / <%= @webhook_event.event_id %>
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><%= @webhook_event.provider %></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><%= tag.code @webhook_event.event_id, id: "event_id" %> <%= render "copy_button", target: "event_id" %></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><%= @webhook_event.event_type %></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 %>"><%= @webhook_event.status %></dd>
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
- <dt>Created</dt>
21
- <dd><%= @webhook_event.created_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
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
- <dt>Payload</dt>
24
- <dd><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></dd>
30
+ Retry
31
+ <% end if @webhook_event.failed? %>
32
+ </dd>
25
33
 
26
- <dt>Headers</dt>
27
- <dd><pre id="headers" class="json-highlight"><%= highlighted JSON.pretty_generate(@webhook_event.headers) %></pre> <div class="actions"><%= render "copy_button", target: "headers" %></div></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
- <dd><%= @webhook_event.error %></dd>
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) : "Fuik Admin" %></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 defer src="https://cdn.jsdelivr.net/npm/attractivejs@0.12.0"></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
- resources :events, only: %w[show]
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fuik
4
+ class InvalidSignature < StandardError; end
5
+ end
@@ -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::Engine.config.providers_allowed.in?([:all, "all"])
18
+ def allow_all? = Fuik.configuration.providers_allowed.in?([:all, "all"])
19
19
 
20
- def explicit_allowlist? = Fuik::Engine.config.providers_allowed.is_a?(Array)
20
+ def explicit_allowlist? = Fuik.configuration.providers_allowed.is_a?(Array)
21
21
 
22
- def explicit_allowlist = Fuik::Engine.config.providers_allowed.to_set
22
+ def explicit_allowlist = Fuik.configuration.providers_allowed.to_set
23
23
 
24
- def providers_allowed = Fuik::Engine.config.providers_allowed
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
@@ -1,3 +1,3 @@
1
1
  module Fuik
2
- VERSION = "0.11.0"
2
+ VERSION = "0.13.0"
3
3
  end