webhooks-rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +20 -0
- data/app/assets/config/webhooks_manifest.js +1 -0
- data/app/assets/stylesheets/webhooks/application.css +15 -0
- data/app/controllers/webhooks/application_controller.rb +6 -0
- data/app/controllers/webhooks/attempts_controller.rb +25 -0
- data/app/controllers/webhooks/endpoints_controller.rb +65 -0
- data/app/controllers/webhooks/events_controller.rb +37 -0
- data/app/helpers/webhooks/application_helper.rb +6 -0
- data/app/jobs/webhooks/application_job.rb +6 -0
- data/app/jobs/webhooks/deliver_job.rb +11 -0
- data/app/mailers/webhooks/application_mailer.rb +8 -0
- data/app/models/webhooks/application_record.rb +7 -0
- data/app/models/webhooks/attempt.rb +71 -0
- data/app/models/webhooks/endpoint.rb +74 -0
- data/app/models/webhooks/event.rb +50 -0
- data/app/models/webhooks/event_serializer.rb +30 -0
- data/app/models/webhooks/request.rb +37 -0
- data/app/models/webhooks/response.rb +29 -0
- data/app/views/layouts/webhooks/application.html.erb +29 -0
- data/app/views/webhooks/attempts/_attempt.html.erb +32 -0
- data/app/views/webhooks/attempts/show.html.erb +33 -0
- data/app/views/webhooks/endpoints/_endpoint.html.erb +31 -0
- data/app/views/webhooks/endpoints/_form.html.erb +42 -0
- data/app/views/webhooks/endpoints/edit.html.erb +16 -0
- data/app/views/webhooks/endpoints/index.html.erb +41 -0
- data/app/views/webhooks/endpoints/new.html.erb +16 -0
- data/app/views/webhooks/endpoints/show.html.erb +102 -0
- data/app/views/webhooks/events/_event.html.erb +11 -0
- data/app/views/webhooks/events/index.html.erb +38 -0
- data/app/views/webhooks/events/new.html.erb +35 -0
- data/app/views/webhooks/events/show.html.erb +49 -0
- data/app/views/webhooks/requests/_request.html.erb +47 -0
- data/app/views/webhooks/responses/_response.html.erb +55 -0
- data/app/views/webhooks/shared/_navigation.html.erb +100 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20210518022959_create_webhooks_endpoints.rb +17 -0
- data/db/migrate/20210518043350_create_webhooks_events.rb +12 -0
- data/db/migrate/20210518050123_create_webhooks_attempts.rb +36 -0
- data/db/migrate/20210518054916_create_webhooks_requests.rb +14 -0
- data/db/migrate/20210518060614_create_webhooks_responses.rb +15 -0
- data/lib/tasks/webhooks_tasks.rake +5 -0
- data/lib/webhooks-rails.rb +3 -0
- data/lib/webhooks.rb +21 -0
- data/lib/webhooks/engine.rb +31 -0
- data/lib/webhooks/version.rb +5 -0
- metadata +183 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9d3794a579ac593913cc2806f60b0173e2570a42c06e72982f73466418ae08e7
|
4
|
+
data.tar.gz: 324a2208aa9a8550e9ce2dc40325e3b3d97d0339abf109ec618d507c0913ff3b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 784fea97e8b17c1362028554cbfede6490560d9b0369587ba4000f80c7abbd02e89f5669e65915ff49956e81cafee21e56e3c3e9219cdbdcbfef4b65d3203f91
|
7
|
+
data.tar.gz: de690423a67371ab04df5eca3709d6a5a888b21018a1d938909fda84a344ae431659a73bc9b93a2f662119bc7eadc1aa3731507b805a2fde5d2f203c59fd2677
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2021 Garrett Bjerkhoel
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Webhooks
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'webhooks'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install webhooks
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
6
|
+
load 'rails/tasks/engine.rake'
|
7
|
+
|
8
|
+
load 'rails/tasks/statistics.rake'
|
9
|
+
|
10
|
+
require 'bundler/gem_tasks'
|
11
|
+
|
12
|
+
require 'rake/testtask'
|
13
|
+
|
14
|
+
Rake::TestTask.new(:test) do |t|
|
15
|
+
t.libs << 'test'
|
16
|
+
t.pattern = 'test/**/*_test.rb'
|
17
|
+
t.verbose = false
|
18
|
+
end
|
19
|
+
|
20
|
+
task default: :test
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/webhooks .css
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_dependency 'webhooks/application_controller'
|
4
|
+
|
5
|
+
module Webhooks
|
6
|
+
class AttemptsController < ApplicationController
|
7
|
+
def show
|
8
|
+
@attempt = Webhooks::Attempt.find(params[:id])
|
9
|
+
end
|
10
|
+
|
11
|
+
def reattempt
|
12
|
+
@attempt = Webhooks::Attempt.find(params[:id])
|
13
|
+
|
14
|
+
if @attempt.reattempt(manual_reattempt: true)
|
15
|
+
redirect_back fallback_location: attempt_path(@attempt), flash: {
|
16
|
+
success: 'Will attempt to deliver event again',
|
17
|
+
}
|
18
|
+
else
|
19
|
+
redirect_back fallback_location: attempt_path(@attempt), flash: {
|
20
|
+
error: 'Could not attempt delivering event again',
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_dependency 'webhooks/application_controller'
|
4
|
+
|
5
|
+
module Webhooks
|
6
|
+
class EndpointsController < ApplicationController
|
7
|
+
def index
|
8
|
+
@endpoints = Webhooks::Endpoint.order(created_at: :asc)
|
9
|
+
end
|
10
|
+
|
11
|
+
def show
|
12
|
+
@endpoint = Webhooks::Endpoint.find(params[:id])
|
13
|
+
end
|
14
|
+
|
15
|
+
def new
|
16
|
+
@endpoint = Webhooks::Endpoint.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def create
|
20
|
+
@endpoint = Webhooks::Endpoint.new(endpoint_params)
|
21
|
+
|
22
|
+
if @endpoint.save
|
23
|
+
redirect_to endpoint_path(@endpoint), flash: {
|
24
|
+
success: 'Successfully created new webhook endpoint.',
|
25
|
+
}
|
26
|
+
else
|
27
|
+
render :new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def edit
|
32
|
+
@endpoint = Webhooks::Endpoint.find(params[:id])
|
33
|
+
end
|
34
|
+
|
35
|
+
def update
|
36
|
+
@endpoint = Webhooks::Endpoint.find(params[:id])
|
37
|
+
|
38
|
+
if @endpoint.update(endpoint_params)
|
39
|
+
redirect_to edit_endpoint_path(@endpoint), flash: {
|
40
|
+
success: 'Changes saved.',
|
41
|
+
}
|
42
|
+
else
|
43
|
+
render :edit
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def destroy
|
48
|
+
@endpoint = Webhooks::Endpoint.find(params[:id])
|
49
|
+
|
50
|
+
if @endpoint.destroy
|
51
|
+
redirect_to endpoints_path, flash: {
|
52
|
+
success: 'Deleted endpoint.',
|
53
|
+
}
|
54
|
+
else
|
55
|
+
redirect_back fallback_location: @endpoint
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def endpoint_params
|
62
|
+
params.require(:endpoint).permit(:url, event_types: [])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_dependency 'webhooks/application_controller'
|
4
|
+
|
5
|
+
module Webhooks
|
6
|
+
class EventsController < ApplicationController
|
7
|
+
def index
|
8
|
+
@events = Webhooks::Event.order(created_at: :desc)
|
9
|
+
end
|
10
|
+
|
11
|
+
def show
|
12
|
+
@event = Webhooks::Event.find(params[:id])
|
13
|
+
end
|
14
|
+
|
15
|
+
def new
|
16
|
+
@event = Webhooks::Event.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def create
|
20
|
+
@event = Webhooks::Event.new(event_params)
|
21
|
+
|
22
|
+
if @event.save
|
23
|
+
redirect_to event_path(@event), flash: {
|
24
|
+
success: 'Successfully created new webhook event.',
|
25
|
+
}
|
26
|
+
else
|
27
|
+
render :new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def event_params
|
34
|
+
params.require(:event).permit(:event_type, :event)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Webhooks
|
4
|
+
class Attempt < ApplicationRecord
|
5
|
+
belongs_to :endpoint, class_name: 'Webhooks::Endpoint', foreign_key: :webhooks_endpoint_id, inverse_of: :attempts
|
6
|
+
belongs_to :event, class_name: 'Webhooks::Event', foreign_key: :webhooks_event_id, inverse_of: :attempts
|
7
|
+
|
8
|
+
has_one :request, class_name: 'Webhooks::Request', foreign_key: :webhooks_attempt_id, dependent: :destroy, inverse_of: :attempt
|
9
|
+
has_one :response, through: :request
|
10
|
+
|
11
|
+
enum state: {
|
12
|
+
queued: 0,
|
13
|
+
success: 1,
|
14
|
+
error: 2,
|
15
|
+
}
|
16
|
+
|
17
|
+
validates :attempt,
|
18
|
+
numericality: {
|
19
|
+
greater_than: 0,
|
20
|
+
less_than_or_equal_to: Rails.application.config.webhooks.attempts.limit,
|
21
|
+
}
|
22
|
+
|
23
|
+
before_validation :set_retry_at, on: :update, if: %i[state_changed? error?]
|
24
|
+
after_commit :enqueue_webhook_delivery, on: :create
|
25
|
+
after_commit :enqueue_webhook_redelivery, on: :update, if: %i[retry_at_previously_changed? retry_at?]
|
26
|
+
|
27
|
+
def deliver
|
28
|
+
request || create_request!
|
29
|
+
end
|
30
|
+
|
31
|
+
def max_attempts?
|
32
|
+
error? && attempt >= Rails.application.config.webhooks.attempts.limit
|
33
|
+
end
|
34
|
+
|
35
|
+
def reattempt(manual_reattempt: false)
|
36
|
+
return if !manual_reattempt && max_attempts?
|
37
|
+
|
38
|
+
# If it's a manual reattempt we need to set the counter to 1 otherwise
|
39
|
+
# we risk hitting the configured limit.
|
40
|
+
next_attempt = if manual_reattempt
|
41
|
+
1
|
42
|
+
else
|
43
|
+
attempt + 1
|
44
|
+
end
|
45
|
+
|
46
|
+
Webhooks::Attempt.create!(
|
47
|
+
endpoint: endpoint,
|
48
|
+
event: event,
|
49
|
+
attempt: next_attempt,
|
50
|
+
manual: manual_reattempt,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def set_retry_at
|
57
|
+
return if manual?
|
58
|
+
return unless attempt < Rails.application.config.webhooks.attempts.limit
|
59
|
+
|
60
|
+
self.retry_at = (60**attempt).seconds.from_now
|
61
|
+
end
|
62
|
+
|
63
|
+
def enqueue_webhook_redelivery
|
64
|
+
Webhooks::DeliverJob.set(wait_until: retry_at).perform_later(self)
|
65
|
+
end
|
66
|
+
|
67
|
+
def enqueue_webhook_delivery
|
68
|
+
Webhooks::DeliverJob.perform_later(self)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Webhooks
|
4
|
+
class Endpoint < ApplicationRecord
|
5
|
+
URL_REGEX = URI::DEFAULT_PARSER.make_regexp(Rails.application.config.webhooks.endpoints.valid_schemas)
|
6
|
+
|
7
|
+
validates :url, presence: true, format: { with: URL_REGEX }, uniqueness: true
|
8
|
+
validates :secret, presence: true, uniqueness: true
|
9
|
+
validates :state, presence: true
|
10
|
+
|
11
|
+
validates :event_types,
|
12
|
+
inclusion: {
|
13
|
+
in: Rails.application.config.webhooks.events.types,
|
14
|
+
allow_blank: true,
|
15
|
+
}
|
16
|
+
|
17
|
+
enum state: {
|
18
|
+
enabled: 0,
|
19
|
+
disabled: 1,
|
20
|
+
error: 2,
|
21
|
+
}
|
22
|
+
|
23
|
+
has_secure_token :secret, length: 36
|
24
|
+
|
25
|
+
before_validation :normalize_url
|
26
|
+
before_validation :generate_secret, on: :create
|
27
|
+
|
28
|
+
has_many :attempts,
|
29
|
+
foreign_key: :webhooks_endpoint_id,
|
30
|
+
inverse_of: :endpoint,
|
31
|
+
dependent: :destroy
|
32
|
+
|
33
|
+
has_many :events, through: :attempts
|
34
|
+
|
35
|
+
scope :subscribed, -> (event_type) {
|
36
|
+
lhs = Arel::Nodes.build_quoted(event_type)
|
37
|
+
any_event_type = Arel::Nodes::NamedFunction.new('ANY', [arel_table[:event_types]])
|
38
|
+
|
39
|
+
enabled.where(Arel::Nodes::Equality.new(lhs, any_event_type).or(arel_table[:event_types].eq(nil)))
|
40
|
+
}
|
41
|
+
|
42
|
+
def event_types=(new_event_types)
|
43
|
+
prepared_endpoint_types = Array.wrap(new_event_types).reject(&:blank?)
|
44
|
+
|
45
|
+
if prepared_endpoint_types.any?
|
46
|
+
super(prepared_endpoint_types)
|
47
|
+
else
|
48
|
+
super(nil)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
url
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def normalize_url
|
59
|
+
self.url = url&.chomp('/')
|
60
|
+
end
|
61
|
+
|
62
|
+
def generate_secret
|
63
|
+
return if secret?
|
64
|
+
|
65
|
+
loop do
|
66
|
+
secret = self.class.generate_unique_secure_token(length: 36)
|
67
|
+
unless self.class.exists?(secret: secret)
|
68
|
+
self.secret = secret
|
69
|
+
break
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Webhooks
|
4
|
+
class Event < ApplicationRecord
|
5
|
+
validates :event_type,
|
6
|
+
presence: true,
|
7
|
+
inclusion: {
|
8
|
+
in: Rails.application.config.webhooks.events.types,
|
9
|
+
}
|
10
|
+
|
11
|
+
validates :event,
|
12
|
+
presence: true,
|
13
|
+
length: {
|
14
|
+
minimum: 1,
|
15
|
+
}
|
16
|
+
|
17
|
+
has_many :attempts,
|
18
|
+
foreign_key: :webhooks_event_id,
|
19
|
+
inverse_of: :event,
|
20
|
+
dependent: :destroy
|
21
|
+
|
22
|
+
has_many :endpoints, through: :attempts
|
23
|
+
before_validation :enforce_create_without_subscribed_endpoint_setting
|
24
|
+
|
25
|
+
after_create :create_attempts
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
event_type
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def subscribed_endpoint_scope
|
34
|
+
Endpoint.subscribed(event_type)
|
35
|
+
end
|
36
|
+
|
37
|
+
def enforce_create_without_subscribed_endpoint_setting
|
38
|
+
return if Rails.application.config.webhooks.events.create_without_subscribed_endpoint
|
39
|
+
return if subscribed_endpoint_scope.any?
|
40
|
+
|
41
|
+
errors.add(:base, :no_endpoints_subscribed)
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_attempts
|
45
|
+
subscribed_endpoint_scope.find_each do |endpoint|
|
46
|
+
attempts.create!(endpoint: endpoint)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|