action_hooks 0.1.0 → 0.2.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/CHANGELOG.md +5 -0
- data/README.md +49 -29
- data/lib/action_hooks/configuration.rb +2 -0
- data/lib/action_hooks/engine.rb +8 -0
- data/lib/action_hooks/version.rb +1 -1
- data/lib/action_hooks/webhook_controller_behavior.rb +6 -13
- data/lib/generators/action_hooks/install/templates/create_webhook_requests.rb.erb +11 -2
- data/lib/generators/action_hooks/webhook/templates/controller.rb.erb +6 -3
- data/lib/generators/action_hooks/webhook/templates/job.rb.erb +19 -0
- data/lib/generators/action_hooks/webhook/webhook_generator.rb +17 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 284686e350568c9e92b85744b29ad6eb114657ae8872464292020edd97ed32ae
|
|
4
|
+
data.tar.gz: 3f226eff09c69f48a0430bcedd6d8d6583ae4bf0c4cb359de9f13f833d4ce8e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11209c21b103ca1e42b2970dc2d9bed285ac0b9b032824303a418d02da6850c1db36319aaf6f4fa30ee3b6eec7d815a066bd1ae091d9d033ad44dafcefdf88ca
|
|
7
|
+
data.tar.gz: 1efbaf2b0533019d20e3d00f52b348bfa3f11a44b9880fd9676136575ebcaab2ce563a1363b92661552706a1cba88350d6feb14b7d4a11fe97fa5365de7f6bd7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-03-02
|
|
4
|
+
|
|
5
|
+
- Refactored ActionHooks to operate purely as middleware. It now automatically mounts `POST /webhooks/:source` without needing any generated configuration route handling.
|
|
6
|
+
- Re-designed `rails g action_hooks:webhook` generator to create only Jobs by default. A new `--controller` flag is available for customized business logic needs.
|
|
7
|
+
|
|
3
8
|
## [0.1.0] - 2026-03-01
|
|
4
9
|
|
|
5
10
|
- Initial release
|
data/README.md
CHANGED
|
@@ -36,15 +36,15 @@ $ rails db:migrate
|
|
|
36
36
|
|
|
37
37
|
### 1. Configuration
|
|
38
38
|
|
|
39
|
-
Configure your webhook sources in the generated initializer (`config/initializers/action_hooks.rb`). Each source represents a third-party service sending webhooks to your application.
|
|
39
|
+
Configure your webhook sources in the generated initializer (`config/initializers/action_hooks.rb`). Each source represents a third-party service sending webhooks to your application. ActionHooks automatically mounts a catch-all route `POST /webhooks/:source`, so configuring a source is enough to start receiving requests.
|
|
40
40
|
|
|
41
41
|
```ruby
|
|
42
42
|
# config/initializers/action_hooks.rb
|
|
43
43
|
ActionHooks.configure do |config|
|
|
44
44
|
config.add_source(:stripe) do |source|
|
|
45
45
|
# The ActiveJob worker class that will process the webhook
|
|
46
|
-
source.worker = "
|
|
47
|
-
|
|
46
|
+
source.worker = "StripeWebhookJob"
|
|
47
|
+
|
|
48
48
|
# Lambda to verify the signature of the incoming request
|
|
49
49
|
source.verify_signature = ->(request) do
|
|
50
50
|
payload = request.body.read
|
|
@@ -53,60 +53,80 @@ ActionHooks.configure do |config|
|
|
|
53
53
|
# Stripe::Webhook::Signature.verify_header(payload, sig_header, ENV['STRIPE_WEBHOOK_SECRET'])
|
|
54
54
|
true
|
|
55
55
|
end
|
|
56
|
-
|
|
57
|
-
# Optional: Restrict incoming requests to specific IP addresses
|
|
58
|
-
# source.
|
|
56
|
+
|
|
57
|
+
# Optional: Restrict incoming requests to specific IP addresses/hosts
|
|
58
|
+
# source.allowed_hosts = ["127.0.0.1", "10.0.0.1"] # Also aliased as `allowed_ips`
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
### 2. Generating a
|
|
63
|
+
### 2. Generating a Job
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
By default, an incoming webhook is authenticated, persisted, and handed over to a background job. You can easily generate a job template for your source:
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
$ rails generate action_hooks:webhook stripe
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
This will:
|
|
72
|
-
1. Create a controller at `app/controllers/stripe_webhooks_controller.rb`.
|
|
73
|
-
2. Add a route to `config/routes.rb` (e.g., `post "webhooks/stripe", to: "stripe_webhooks#create"`).
|
|
74
|
-
|
|
75
|
-
The generated controller includes `ActionHooks::WebhookControllerBehavior`, which handles everything from skipping CSRF verification, verifying the IP and signature, saving the request to the database, and enqueueing your worker.
|
|
76
|
-
|
|
77
|
-
### 3. Processing the Webhook
|
|
78
|
-
|
|
79
|
-
Create the worker class that you specified in your configuration. The worker will receive the ID of the `ActionHooks::WebhookRequest` record.
|
|
71
|
+
This will create `app/jobs/stripe_webhook_job.rb`. The background job will receive the ID of the saved `ActionHooks::WebhookRequest` record:
|
|
80
72
|
|
|
81
73
|
```ruby
|
|
82
|
-
# app/jobs/
|
|
83
|
-
class
|
|
74
|
+
# app/jobs/stripe_webhook_job.rb
|
|
75
|
+
class StripeWebhookJob < ApplicationJob
|
|
84
76
|
queue_as :default
|
|
85
77
|
|
|
86
78
|
def perform(webhook_request_id)
|
|
87
79
|
webhook_request = ActionHooks::WebhookRequest.find(webhook_request_id)
|
|
88
|
-
|
|
89
|
-
# Access the parsed JSON payload
|
|
90
80
|
payload = webhook_request.payload
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
81
|
+
|
|
82
|
+
case payload["type"]
|
|
83
|
+
when "payment_intent.succeeded"
|
|
84
|
+
# handle payment
|
|
95
85
|
end
|
|
96
|
-
|
|
97
|
-
# Update the state of the webhook request when done
|
|
86
|
+
|
|
98
87
|
webhook_request.processed!
|
|
99
88
|
rescue => e
|
|
100
|
-
# Mark as failed if something goes wrong
|
|
101
89
|
webhook_request.failed!
|
|
102
90
|
raise e
|
|
103
91
|
end
|
|
104
92
|
end
|
|
105
93
|
```
|
|
106
94
|
|
|
95
|
+
### 3. Custom Controller (Optional)
|
|
96
|
+
|
|
97
|
+
If your webhook processing requires complex synchronous logic before placing the job into the queue, you can generate a custom controller using the `--controller` flag:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
$ rails generate action_hooks:webhook stripe --controller
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This generates:
|
|
104
|
+
|
|
105
|
+
- A job: `app/jobs/stripe_webhook_job.rb` (unless `--skip-job` is provided)
|
|
106
|
+
- A controller: `app/controllers/webhooks/stripe_controller.rb`
|
|
107
|
+
- A specific route mapping in `config/routes.rb`
|
|
108
|
+
|
|
109
|
+
Your custom controller will inherit from `ActionHooks::WebhookController`. The parent controller handles persistence, IP checks, and signature verification, while your controller can focus just on the business logic inside the `process_webhook` hook:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# app/controllers/webhooks/stripe_controller.rb
|
|
113
|
+
class Webhooks::StripeController < ActionHooks::WebhookController
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def webhook_source_name
|
|
117
|
+
:stripe
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def process_webhook(webhook_request)
|
|
121
|
+
# Business logic here.
|
|
122
|
+
# The default behavior inside `process_webhook` is to enqueue the configured background job.
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
107
127
|
## Development
|
|
108
128
|
|
|
109
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
129
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
110
130
|
|
|
111
131
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
112
132
|
|
data/lib/action_hooks/engine.rb
CHANGED
|
@@ -5,5 +5,13 @@ require "rails/engine"
|
|
|
5
5
|
module ActionHooks
|
|
6
6
|
class Engine < ::Rails::Engine
|
|
7
7
|
isolate_namespace ActionHooks
|
|
8
|
+
|
|
9
|
+
initializer "action_hooks.routes" do |app|
|
|
10
|
+
app.routes.append do
|
|
11
|
+
post "webhooks/:source",
|
|
12
|
+
to: "action_hooks/webhooks#create",
|
|
13
|
+
constraints: {source: /[a-z0-9_]+/}
|
|
14
|
+
end
|
|
15
|
+
end
|
|
8
16
|
end
|
|
9
17
|
end
|
data/lib/action_hooks/version.rb
CHANGED
|
@@ -10,28 +10,21 @@ module ActionHooks
|
|
|
10
10
|
|
|
11
11
|
before_action :verify_webhook_ip!
|
|
12
12
|
before_action :verify_webhook_signature!
|
|
13
|
+
before_action :persist_webhook_request!
|
|
13
14
|
end
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
payload = parse_webhook_payload
|
|
16
|
+
private
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
def persist_webhook_request!
|
|
19
|
+
@webhook_request = ActionHooks::WebhookRequest.create!(
|
|
19
20
|
source: webhook_source_name.to_s,
|
|
20
|
-
payload:
|
|
21
|
+
payload: parse_webhook_payload,
|
|
21
22
|
state: :pending
|
|
22
23
|
)
|
|
23
|
-
|
|
24
|
-
worker_class = webhook_source_config.worker
|
|
25
|
-
worker_class&.constantize&.perform_later(webhook_request.id)
|
|
26
|
-
|
|
27
|
-
head :ok
|
|
28
24
|
end
|
|
29
25
|
|
|
30
|
-
private
|
|
31
|
-
|
|
32
26
|
def webhook_source_name
|
|
33
|
-
|
|
34
|
-
self.class.name.sub(/WebhooksController$/, "").underscore
|
|
27
|
+
raise NotImplementedError, "You must define `#webhook_source_name` in your webhook controller."
|
|
35
28
|
end
|
|
36
29
|
|
|
37
30
|
def webhook_source_config
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
class CreateWebhookRequests < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
2
|
def change
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
<%-
|
|
4
|
+
json_type = (ActiveRecord::Base.connection.adapter_name.downcase == "postgresql") ? :jsonb : :json
|
|
5
|
+
|
|
6
|
+
supports_uuid = if ActiveRecord::Base.connection.respond_to?(:supports_uuid?)
|
|
7
|
+
ActiveRecord::Base.connection.supports_uuid? # PostgreSQL
|
|
8
|
+
else
|
|
9
|
+
false # Defaults for others like SQLite, typical MySQL setups unless explicitly configured
|
|
10
|
+
end
|
|
11
|
+
-%>
|
|
12
|
+
create_table :webhook_requests<%= supports_uuid ? ", id: :uuid" : "" %> do |t|
|
|
13
|
+
t.<%= json_type %> :payload, default: {}, null: false
|
|
5
14
|
t.string :source, null: false
|
|
6
15
|
t.integer :state, null: false, default: 0
|
|
7
16
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
class
|
|
2
|
-
include ActionHooks::WebhookControllerBehavior
|
|
3
|
-
|
|
1
|
+
class Webhooks::<%= class_name %>Controller < ActionHooks::WebhookController
|
|
4
2
|
private
|
|
5
3
|
|
|
6
4
|
def webhook_source_name
|
|
7
5
|
:<%= file_name %>
|
|
8
6
|
end
|
|
7
|
+
|
|
8
|
+
def process_webhook(webhook_request)
|
|
9
|
+
# Business logic here.
|
|
10
|
+
# @webhook_request is also available as an instance variable.
|
|
11
|
+
end
|
|
9
12
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class <%= class_name %>WebhookJob < ApplicationJob
|
|
2
|
+
queue_as :default
|
|
3
|
+
|
|
4
|
+
def perform(webhook_request_id)
|
|
5
|
+
webhook_request = ActionHooks::WebhookRequest.find(webhook_request_id)
|
|
6
|
+
payload = webhook_request.payload
|
|
7
|
+
|
|
8
|
+
# TODO: handle payload
|
|
9
|
+
# case payload["type"]
|
|
10
|
+
# when "example.event"
|
|
11
|
+
# # handle event
|
|
12
|
+
# end
|
|
13
|
+
|
|
14
|
+
webhook_request.processed!
|
|
15
|
+
rescue => e
|
|
16
|
+
webhook_request.failed!
|
|
17
|
+
raise e
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,17 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "rails/generators"
|
|
2
4
|
|
|
3
5
|
module ActionHooks
|
|
4
6
|
module Generators
|
|
5
7
|
class WebhookGenerator < ::Rails::Generators::NamedBase
|
|
6
8
|
source_root File.expand_path("templates", __dir__)
|
|
7
|
-
desc "Creates a webhook controller for a given source
|
|
9
|
+
desc "Creates a webhook job (and optionally a controller) for a given source."
|
|
10
|
+
|
|
11
|
+
class_option :controller, type: :boolean, default: false,
|
|
12
|
+
desc: "Also generate a custom controller for business logic"
|
|
13
|
+
class_option :skip_job, type: :boolean, default: false,
|
|
14
|
+
desc: "Skip job generation"
|
|
15
|
+
|
|
16
|
+
def create_job_file
|
|
17
|
+
return if options[:skip_job]
|
|
18
|
+
template "job.rb.erb", "app/jobs/#{file_name}_webhook_job.rb"
|
|
19
|
+
end
|
|
8
20
|
|
|
9
21
|
def create_controller_file
|
|
10
|
-
|
|
22
|
+
return unless options[:controller]
|
|
23
|
+
template "controller.rb.erb", "app/controllers/webhooks/#{file_name}_controller.rb"
|
|
11
24
|
end
|
|
12
25
|
|
|
13
26
|
def add_route
|
|
14
|
-
|
|
27
|
+
return unless options[:controller]
|
|
28
|
+
route %(post "webhooks/#{file_name}", to: "webhooks/#{file_name}#create")
|
|
15
29
|
end
|
|
16
30
|
end
|
|
17
31
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: action_hooks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexey Poimtsev
|
|
@@ -127,6 +127,7 @@ files:
|
|
|
127
127
|
- lib/generators/action_hooks/install/templates/action_hooks.rb
|
|
128
128
|
- lib/generators/action_hooks/install/templates/create_webhook_requests.rb.erb
|
|
129
129
|
- lib/generators/action_hooks/webhook/templates/controller.rb.erb
|
|
130
|
+
- lib/generators/action_hooks/webhook/templates/job.rb.erb
|
|
130
131
|
- lib/generators/action_hooks/webhook/webhook_generator.rb
|
|
131
132
|
homepage: https://github.com/alec-c4/action_hooks
|
|
132
133
|
licenses:
|