fuik 0.10.0 → 0.11.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -0
  3. data/app/helpers/fuik/highlight_helper.rb +1 -1
  4. data/config/routes.rb +1 -1
  5. data/lib/fuik/engine.rb +3 -0
  6. data/lib/fuik/routing/provider_constraint.rb +34 -0
  7. data/lib/fuik/version.rb +1 -1
  8. data/lib/generators/fuik/provider/templates/github/installation_created.rb.tt +2 -2
  9. data/lib/generators/fuik/provider/templates/github/push.rb.tt +2 -4
  10. data/lib/generators/fuik/provider/templates/github/star_created.rb.tt +2 -2
  11. data/lib/generators/fuik/provider/templates/loops/base.rb.tt +17 -0
  12. data/lib/generators/fuik/provider/templates/loops/contact_unsubscribed.rb.tt +12 -0
  13. data/lib/generators/fuik/provider/templates/loops/email_hard_bounced.rb.tt +14 -0
  14. data/lib/generators/fuik/provider/templates/mailgun/base.rb.tt +18 -0
  15. data/lib/generators/fuik/provider/templates/mailgun/bounced.rb.tt +14 -0
  16. data/lib/generators/fuik/provider/templates/mailgun/complained.rb.tt +12 -0
  17. data/lib/generators/fuik/provider/templates/mailpace/email_bounced.rb.tt +2 -1
  18. data/lib/generators/fuik/provider/templates/mailpace/email_spam.rb.tt +3 -0
  19. data/lib/generators/fuik/provider/templates/postmark/base.rb.tt +4 -0
  20. data/lib/generators/fuik/provider/templates/postmark/bounce.rb.tt +17 -0
  21. data/lib/generators/fuik/provider/templates/resend/base.rb.tt +11 -0
  22. data/lib/generators/fuik/provider/templates/resend/email_bounced.rb.tt +14 -0
  23. data/lib/generators/fuik/provider/templates/resend/email_complained.rb.tt +12 -0
  24. data/lib/generators/fuik/provider/templates/stripe/checkout_session_completed.rb.tt +5 -5
  25. data/lib/generators/fuik/provider/templates/stripe/customer_subscription_deleted.rb.tt +2 -1
  26. data/lib/generators/fuik/provider/templates/stripe/customer_subscription_updated.rb.tt +5 -4
  27. data/lib/generators/fuik/provider/templates/stripe/payment_intent_succeeded.rb.tt +5 -4
  28. data/lib/generators/fuik/provider/templates/userlist/base.rb.tt +18 -0
  29. data/lib/generators/fuik/provider/templates/userlist/user_unsubscribed.rb.tt +14 -0
  30. metadata +17 -3
  31. /data/app/helpers/fuik/{icon.rb → icon_helper.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27567e69538dc46d35faddb9c331ccdd3059afbb810f40a84e0c3a1ee0a573bf
4
- data.tar.gz: 23ad7f6fa24a03835fb55f5afcd1157282f60aa600d7e5a8e8084c1c376b110f
3
+ metadata.gz: b15633f53ffe1c5c7dad94fb1d5151a8c2bce010318854cc50a4e3afbe48c828
4
+ data.tar.gz: 0ba925e04a5271e77a8a8ed4c80bbda7a4a00a5d9d87c6b3b304104519988a29
5
5
  SHA512:
6
- metadata.gz: bf626291cd8b4cf4e069b5be95fa58ca45fec06628d0ffe353c9e01af6460537805a7d3b45ad403bf681b26beddfcbb495e3e03c71af880a311c68454064390e
7
- data.tar.gz: 835c24fcb7798aa948d3a6ca545d35cae78a4e83489f6a798b10527d53a8ea836aa17d9495f3ed64d6ec3c16b468254d810249868a3e5101fb7437b980aed759
6
+ metadata.gz: 5c7c0c9ee432bd0702969c6238c0ceea4200da1370350a4d88337f14252bc6f4bce0990a0c80e49dd862de9f25459c4cd1a1f5b914e41de1b021e0ba3b21c255
7
+ data.tar.gz: 93e200fcc81563e5f6a9699067c6543c4e4b26af42f1504dff84aa0a2e59ce89efe2fb2a571dde5fd2bc298bded45b2c9b332d44c1deb79fea22ce4f0130248b
data/README.md CHANGED
@@ -124,6 +124,24 @@ end
124
124
  If `Provider::Base.verify!` exists, Fuik calls it automatically. Invalid signatures return 401 without storing the webhook.
125
125
 
126
126
 
127
+ ### Provider allowlist
128
+
129
+ By default:
130
+ - **Development/test**: all providers are allowed
131
+ - **Production/staging**: only providers in `app/webhooks/` are allowed
132
+
133
+ Configure with `Fuik::Engine.config.providers_allowed`:
134
+ ```ruby
135
+ # Allow all (including production)
136
+ Fuik::Engine.config.providers_allowed = :all
137
+
138
+ # Explicit allowlist (overrides directory scan)
139
+ Fuik::Engine.config.providers_allowed = %w[stripe github shopify]
140
+ ```
141
+
142
+ Unknown providers return `404 Not Found`.
143
+
144
+
127
145
  ### Pre-packaged providers
128
146
 
129
147
  Fuik includes ready-to-use [templates for common providers](https://github.com/Rails-Designer/fuik/tree/main/lib/generators/fuik/provider/templates).
@@ -31,7 +31,7 @@ module Fuik
31
31
 
32
32
  object.each_with_index.map do |(key, value), index|
33
33
  key_path = current_path + [key]
34
- path_string = key_path.map { "[\"#{it}\"]" }.join
34
+ path_string = key_path.map { it.is_a?(String) ? "[\"#{it}\"]" : "[#{it}]" }.join
35
35
 
36
36
  comma = (index == object.size - 1) ? "" : '<span class="json-punctuation">,</span>'
37
37
 
data/config/routes.rb CHANGED
@@ -5,5 +5,5 @@ Fuik::Engine.routes.draw do
5
5
  resources :events, only: %w[show]
6
6
  resources :downloads, only: %w[create]
7
7
 
8
- post ":provider", to: "webhooks#create"
8
+ post ":provider", to: "webhooks#create", constraints: Fuik::Routing::ProviderConstraint.new
9
9
  end
data/lib/fuik/engine.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fuik/routing/provider_constraint"
4
+
3
5
  module Fuik
4
6
  class Engine < ::Rails::Engine
5
7
  isolate_namespace Fuik
6
8
 
7
9
  config.webhooks_controller_parent = "ActionController::Base"
8
10
  config.events_controller_parent = "ActionController::Base"
11
+ config.providers_allowed = Rails.env.local?
9
12
 
10
13
  config.to_prepare do
11
14
  ActiveSupport.on_load(:action_view) do
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fuik
4
+ module Routing
5
+ class ProviderConstraint
6
+ def matches?(request)
7
+ return true if allow_all?
8
+ return explicit_allowlist.include?(request.params[:provider]) if explicit_allowlist?
9
+
10
+ return Rails.env.local? if providers_allowed.nil?
11
+ return Rails.env.local? if providers_allowed == true
12
+
13
+ scanned_allowlist.include?(request.params[:provider])
14
+ end
15
+
16
+ private
17
+
18
+ def allow_all? = Fuik::Engine.config.providers_allowed.in?([:all, "all"])
19
+
20
+ def explicit_allowlist? = Fuik::Engine.config.providers_allowed.is_a?(Array)
21
+
22
+ def explicit_allowlist = Fuik::Engine.config.providers_allowed.to_set
23
+
24
+ def providers_allowed = Fuik::Engine.config.providers_allowed
25
+
26
+ def scanned_allowlist
27
+ @scanned_allowlist ||= Dir["#{Rails.root}/app/webhooks/*"]
28
+ .select { File.directory?(it) }
29
+ .map { File.basename(it) }
30
+ .to_set
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/fuik/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Fuik
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.0"
3
3
  end
@@ -1,8 +1,8 @@
1
1
  module <%= provider_module_name %>
2
2
  class InstallationCreated < Base
3
3
  def process!
4
- installation_id = payload.dig("installation", "id")
5
- account = payload.dig("installation", "account", "login")
4
+ # payload["installation"]["id"] # GitHub app installation ID
5
+ # payload["installation"]["account"] # user/org that installed
6
6
 
7
7
  # TODO: Add business logic
8
8
 
@@ -1,10 +1,8 @@
1
1
  module <%= provider_module_name %>
2
2
  class Push < Base
3
3
  def process!
4
- # This fires when a pull request is merged (default branch push)
5
-
6
- repository = payload.dig("repository", "full_name")
7
- ref = payload["ref"]
4
+ # payload["ref"] # git ref (e.g., "refs/heads/main")
5
+ # payload["repository"]["full_name"] # "owner/repo"
8
6
 
9
7
  # TODO: Add business logic
10
8
 
@@ -1,8 +1,8 @@
1
1
  module <%= provider_module_name %>
2
2
  class StarCreated < Base
3
3
  def process!
4
- repository = payload.dig("repository", "full_name")
5
- stargazer = payload.dig("sender", "login")
4
+ # payload["repository"]["full_name"] # "owner/repo"
5
+ # payload["sender"]["login"] # user who starred
6
6
 
7
7
  # TODO: Add business logic
8
8
 
@@ -0,0 +1,17 @@
1
+ module <%= provider_module_name %>
2
+ class Base < Fuik::Event
3
+ def self.verify!(request)
4
+ secret = Rails.application.credentials.dig(:loops, :signing_secret)
5
+ secret_bytes = Base64.strict_decode64(secret.split("_")[1])
6
+
7
+ webhook_id = request.headers["webhook-id"]
8
+ timestamp = request.headers["webhook-timestamp"]
9
+ signature = request.headers["webhook-signature"]
10
+
11
+ signed_content = "#{webhook_id}.#{timestamp}.#{request.raw_post}"
12
+ expected_signature = OpenSSL::HMAC.digest("SHA256", secret_bytes, signed_content)
13
+
14
+ raise Fuik::InvalidSignature unless signature.split(" ").any? { |sig| sig.include?(",#{expected_signature}") }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module <%= provider_module_name %>
2
+ class ContactUnsubscribed < Base
3
+ def process!
4
+ # payload["contactIdentity"]["email"] # user email
5
+ # payload["contactIdentity"]["id"] # Loops contact ID
6
+
7
+ # TODO: Add business logic
8
+
9
+ @webhook_event.processed!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module <%= provider_module_name %>
2
+ class EmailHardBounced < Base
3
+ def process!
4
+ # payload["contact"]["email"] # recipient email
5
+ # payload["email"]["id"] # Loops email ID
6
+ # payload["email"]["emailMessageId"] # Postmark message ID
7
+ # payload["email"]["subject"] # email subject
8
+
9
+ # TODO: Add business logic
10
+
11
+ @webhook_event.processed!
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module <%= provider_module_name %>
2
+ class Base < Fuik::Event
3
+ def self.verify!(request)
4
+ secret = Rails.application.credentials.dig(:mailgun, :signing_key)
5
+ signature_base64 = request.headers["Signature"]
6
+ timestamp = request.headers["Timestamp"]
7
+ token = request.headers["Token"]
8
+
9
+ signature = Base64.strict_decode64(signature_base64)
10
+ data = "#{timestamp}#{token}"
11
+
12
+ raise Fuik::InvalidSignature unless ActiveSupport::SecurityUtils.secure_compare(
13
+ OpenSSL::HMAC.digest("SHA256", secret, data),
14
+ signature
15
+ )
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ module <%= provider_module_name %>
2
+ class Bounced < Base
3
+ def process!
4
+ # payload["recipient"] # email address that bounced
5
+ # payload["code"] # bounce code (e.g., "MAILBOX_FULL")
6
+ # payload["error"] # SMTP error response
7
+ # payload["reason"] # human-readable reason
8
+
9
+ # TODO: Add business logic
10
+
11
+ @webhook_event.processed!
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module <%= provider_module_name %>
2
+ class Complained < Base
3
+ def process!
4
+ # payload["recipient"] # email address that marked as spam
5
+ # payload["timestamp"] # when the complaint occurred
6
+
7
+ # TODO: Add business logic
8
+
9
+ @webhook_event.processed!
10
+ end
11
+ end
12
+ end
@@ -1,7 +1,8 @@
1
1
  module <%= provider_module_name %>
2
2
  class EmailBounced < Base
3
3
  def process!
4
- bounce_type = payload["bounce_type"]
4
+ # payload["to"] # recipient email
5
+ # payload["bounce_type"] # e.g., "hard", "soft"
5
6
 
6
7
  # TODO: Add business logic
7
8
 
@@ -1,6 +1,9 @@
1
1
  module <%= provider_module_name %>
2
2
  class EmailSpam < Base
3
3
  def process!
4
+ # payload["to"] # recipient email
5
+ # payload["timestamp"] # when marked as spam
6
+
4
7
  # TODO: Add business logic
5
8
 
6
9
  @webhook_event.processed!
@@ -0,0 +1,4 @@
1
+ module <%= provider_module_name %>
2
+ class Base < Fuik::Event
3
+ end
4
+ end
@@ -0,0 +1,17 @@
1
+ module <%= provider_module_name %>
2
+ class Bounce < Base
3
+ def process!
4
+ # payload["Email"] # email address that bounced
5
+ # payload["Type"] # bounce type (e.g., "HardBounce", "SoftBounce")
6
+ # payload["BouncedAt"] # ISO 8601 timestamp
7
+ # payload["Inactive"] # boolean - address is deactivated
8
+ # payload["CanActivate"] # boolean - can be reactivated
9
+ # payload["MessageID"] # Postmark message ID
10
+ # payload["Description"] # human-readable description
11
+
12
+ # TODO: Add business logic
13
+
14
+ @webhook_event.processed!
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module <%= provider_module_name %>
2
+ class Base < Fuik::Event
3
+ def self.verify!(request)
4
+ secret = Rails.application.credentials.dig(:resend, :signing_secret)
5
+ signature = request.headers["Resend-Signature"]
6
+ expected_signature = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post)
7
+
8
+ raise Fuik::InvalidSignature unless Rack::Utils.secure_compare(signature, expected_signature)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module <%= provider_module_name %>
2
+ class EmailBounced < Base
3
+ def process!
4
+ # payload["data"]["emails"][0]["to"] # recipient email
5
+ # payload["data"]["bounce"]["classification"] # bounce type
6
+ # payload["data"]["bounce"]["subtype"] # bounce subtype
7
+ # payload["ts"] # timestamp
8
+
9
+ # TODO: Add business logic
10
+
11
+ @webhook_event.processed!
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module <%= provider_module_name %>
2
+ class EmailComplained < Base
3
+ def process!
4
+ # payload["data"]["email"]["to"] # recipient email
5
+ # payload["ts"] # timestamp
6
+
7
+ # TODO: Add business logic
8
+
9
+ @webhook_event.processed!
10
+ end
11
+ end
12
+ end
@@ -1,14 +1,14 @@
1
1
  module Stripe
2
2
  class CheckoutSessionCompleted < Base
3
3
  def process!
4
+ # payload["data"]["object"]["id"] # session ID
5
+ # payload["data"]["object"]["customer"] # customer ID
6
+ # payload["data"]["object"]["client_reference_id"] # your internal reference
7
+ # payload["data"]["object"]["amount_total"] # amount in cents
8
+
4
9
  # TODO: Add business logic
5
- # session = Stripe::Checkout::Session.retrieve(session_id) # this assumes the Stripe gem is available
6
10
 
7
11
  @webhook_event.processed!
8
12
  end
9
-
10
- private
11
-
12
- def session_id = payload.dig("data", "object", "id")
13
13
  end
14
14
  end
@@ -1,7 +1,8 @@
1
1
  module Stripe
2
2
  class CustomerSubscriptionDeleted < Base
3
3
  def process!
4
- subscription_id = payload.dig("data", "object", "id")
4
+ # payload["data"]["object"]["id"] # subscription ID
5
+ # payload["data"]["object"]["customer"] # customer ID
5
6
 
6
7
  # TODO: Add business logic
7
8
 
@@ -1,13 +1,14 @@
1
1
  module Stripe
2
2
  class CustomerSubscriptionUpdated < Base
3
3
  def process!
4
+ # payload["data"]["object"]["id"] # subscription ID
5
+ # payload["data"]["object"]["customer"] # customer ID
6
+ # payload["data"]["object"]["status"] # e.g., "active", "past_due"
7
+ # payload["data"]["object"]["price"] # price object
8
+
4
9
  # TODO: Add business logic
5
10
 
6
11
  @webhook_event.processed!
7
12
  end
8
-
9
- private
10
-
11
- def subscription_id = payload.dig("data", "object", "id")
12
13
  end
13
14
  end
@@ -1,13 +1,14 @@
1
1
  module Stripe
2
2
  class PaymentIntentSucceeded < Base
3
3
  def process!
4
+ # payload["data"]["object"]["id"] # payment intent ID
5
+ # payload["data"]["object"]["amount"] # amount in cents
6
+ # payload["data"]["object"]["customer"] # customer ID
7
+ # payload["data"]["object"]["currency"] # e.g., "usd"
8
+
4
9
  # TODO: Add business logic
5
10
 
6
11
  @webhook_event.processed!
7
12
  end
8
-
9
- private
10
-
11
- def payment_intent_id = payload["data"]["object"]["id"]
12
13
  end
13
14
  end
@@ -0,0 +1,18 @@
1
+ module <%= provider_module_name %>
2
+ class Base < Fuik::Event
3
+ def self.verify!(request)
4
+ secret = Rails.application.credentials.dig(:userlist, :webhook_secret)
5
+ header = request.headers["Userlist-Signature"]
6
+ body = request.raw_post
7
+
8
+ parts = header.split(",")
9
+ timestamp = parts[0].split("=")[1]
10
+ signature = parts[1].split("=")[1]
11
+
12
+ signed_payload = "#{timestamp}.#{body}"
13
+ expected_signature = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
14
+
15
+ raise Fuik::InvalidSignature unless Rack::Utils.secure_compare(signature, expected_signature)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ module <%= provider_module_name %>
2
+ class UserUnsubscribed < Base
3
+ def process!
4
+ # payload["user"]["id"] # Userlist user ID
5
+ # payload["user"]["identifier"] # your internal user ID
6
+ # payload["user"]["email"] # user email
7
+ # payload["user"]["properties"] # custom user properties
8
+
9
+ # TODO: Add business logic
10
+
11
+ @webhook_event.processed!
12
+ end
13
+ end
14
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fuik
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer
@@ -71,7 +71,7 @@ files:
71
71
  - app/controllers/fuik/events_controller.rb
72
72
  - app/controllers/fuik/webhooks_controller.rb
73
73
  - app/helpers/fuik/highlight_helper.rb
74
- - app/helpers/fuik/icon.rb
74
+ - app/helpers/fuik/icon_helper.rb
75
75
  - app/jobs/fuik/application_job.rb
76
76
  - app/models/fuik/application_record.rb
77
77
  - app/models/fuik/event.rb
@@ -85,6 +85,7 @@ files:
85
85
  - lib/fuik.rb
86
86
  - lib/fuik/dot_access.rb
87
87
  - lib/fuik/engine.rb
88
+ - lib/fuik/routing/provider_constraint.rb
88
89
  - lib/fuik/version.rb
89
90
  - lib/generators/fuik/install/install_generator.rb
90
91
  - lib/generators/fuik/provider/provider_generator.rb
@@ -95,14 +96,27 @@ files:
95
96
  - lib/generators/fuik/provider/templates/github/installation_created.rb.tt
96
97
  - lib/generators/fuik/provider/templates/github/push.rb.tt
97
98
  - lib/generators/fuik/provider/templates/github/star_created.rb.tt
99
+ - lib/generators/fuik/provider/templates/loops/base.rb.tt
100
+ - lib/generators/fuik/provider/templates/loops/contact_unsubscribed.rb.tt
101
+ - lib/generators/fuik/provider/templates/loops/email_hard_bounced.rb.tt
102
+ - lib/generators/fuik/provider/templates/mailgun/base.rb.tt
103
+ - lib/generators/fuik/provider/templates/mailgun/bounced.rb.tt
104
+ - lib/generators/fuik/provider/templates/mailgun/complained.rb.tt
98
105
  - lib/generators/fuik/provider/templates/mailpace/base.rb.tt
99
106
  - lib/generators/fuik/provider/templates/mailpace/email_bounced.rb.tt
100
107
  - lib/generators/fuik/provider/templates/mailpace/email_spam.rb.tt
108
+ - lib/generators/fuik/provider/templates/postmark/base.rb.tt
109
+ - lib/generators/fuik/provider/templates/postmark/bounce.rb.tt
110
+ - lib/generators/fuik/provider/templates/resend/base.rb.tt
111
+ - lib/generators/fuik/provider/templates/resend/email_bounced.rb.tt
112
+ - lib/generators/fuik/provider/templates/resend/email_complained.rb.tt
101
113
  - lib/generators/fuik/provider/templates/stripe/base.rb.tt
102
114
  - lib/generators/fuik/provider/templates/stripe/checkout_session_completed.rb.tt
103
115
  - lib/generators/fuik/provider/templates/stripe/customer_subscription_deleted.rb.tt
104
116
  - lib/generators/fuik/provider/templates/stripe/customer_subscription_updated.rb.tt
105
117
  - lib/generators/fuik/provider/templates/stripe/payment_intent_succeeded.rb.tt
118
+ - lib/generators/fuik/provider/templates/userlist/base.rb.tt
119
+ - lib/generators/fuik/provider/templates/userlist/user_unsubscribed.rb.tt
106
120
  homepage: https://railsdesigner.com/fuik/
107
121
  licenses:
108
122
  - MIT
@@ -123,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
125
139
  requirements: []
126
- rubygems_version: 4.0.8
140
+ rubygems_version: 4.0.9
127
141
  specification_version: 4
128
142
  summary: A fish trap for webhooks
129
143
  test_files: []
File without changes