webhookdb 1.0.2 → 1.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/data/messages/web/install-customer-login.liquid +1 -1
- data/data/messages/web/install-success.liquid +2 -2
- data/db/migrations/038_webhookdb_api_key.rb +13 -0
- data/db/migrations/039_saved_query.rb +17 -0
- data/lib/webhookdb/api/db.rb +2 -19
- data/lib/webhookdb/api/helpers.rb +27 -0
- data/lib/webhookdb/api/install.rb +23 -1
- data/lib/webhookdb/api/saved_queries.rb +219 -0
- data/lib/webhookdb/api/service_integrations.rb +17 -12
- data/lib/webhookdb/api/sync_targets.rb +2 -6
- data/lib/webhookdb/api/system.rb +13 -2
- data/lib/webhookdb/api/webhook_subscriptions.rb +12 -18
- data/lib/webhookdb/api.rb +11 -2
- data/lib/webhookdb/apps.rb +2 -0
- data/lib/webhookdb/email_octopus.rb +2 -0
- data/lib/webhookdb/envfixer.rb +29 -0
- data/lib/webhookdb/fixtures/saved_queries.rb +27 -0
- data/lib/webhookdb/fixtures/service_integrations.rb +4 -0
- data/lib/webhookdb/front.rb +23 -11
- data/lib/webhookdb/google_calendar.rb +6 -0
- data/lib/webhookdb/icalendar.rb +6 -0
- data/lib/webhookdb/idempotency.rb +94 -33
- data/lib/webhookdb/jobs/backfill.rb +24 -5
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +7 -1
- data/lib/webhookdb/jobs/prepare_database_connections.rb +2 -2
- data/lib/webhookdb/jobs/scheduled_backfills.rb +19 -13
- data/lib/webhookdb/oauth/{front.rb → front_provider.rb} +21 -4
- data/lib/webhookdb/oauth/{intercom.rb → intercom_provider.rb} +1 -1
- data/lib/webhookdb/oauth.rb +8 -7
- data/lib/webhookdb/organization/alerting.rb +11 -0
- data/lib/webhookdb/organization.rb +8 -2
- data/lib/webhookdb/postgres/model_utilities.rb +19 -0
- data/lib/webhookdb/postgres.rb +3 -0
- data/lib/webhookdb/replicator/column.rb +9 -1
- data/lib/webhookdb/replicator/front_conversation_v1.rb +5 -1
- data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +2 -5
- data/lib/webhookdb/replicator/front_message_v1.rb +5 -1
- data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +325 -0
- data/lib/webhookdb/replicator/front_v1_mixin.rb +9 -1
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +6 -3
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +28 -14
- data/lib/webhookdb/replicator/twilio_sms_v1.rb +3 -1
- data/lib/webhookdb/saved_query.rb +28 -0
- data/lib/webhookdb/service_integration.rb +36 -3
- data/lib/webhookdb/signalwire.rb +40 -0
- data/lib/webhookdb/spec_helpers/citest.rb +18 -9
- data/lib/webhookdb/spec_helpers/postgres.rb +9 -0
- data/lib/webhookdb/spec_helpers/service.rb +5 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +25 -13
- data/lib/webhookdb/spec_helpers/whdb.rb +7 -0
- data/lib/webhookdb/sync_target.rb +1 -1
- data/lib/webhookdb/tasks/specs.rb +4 -2
- data/lib/webhookdb/version.rb +1 -1
- data/lib/webhookdb.rb +24 -26
- metadata +39 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2c4da6abd3c5def6f27e180aaf435a4e9293eae4a1f926a065b87c1979372b3
|
4
|
+
data.tar.gz: 72de7406adc5049255878c5bf16fe05e92716d18c2f2492d996dbddb6b0aa307
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 725e0d6183cd7bfd259a4202c888e419658f77288de59adf2df1a6e48c36a06dd4f91857d43ac07f54e81aa9ee882a8e65813d3cb92d6cee0576d74a745ed28c
|
7
|
+
data.tar.gz: 6811f58d123c6693758c75c3e5ba4de991c7a5e3d621de581ac32fe76efb63ca8e910757cc3e42d18029fd10389c11d74b8255601781ed5c9267e042e0f7ca96
|
@@ -11,7 +11,7 @@
|
|
11
11
|
<form method="POST" action="{{ action_url }}">
|
12
12
|
{% if view == "email" %}
|
13
13
|
<p>
|
14
|
-
Welcome to
|
14
|
+
Welcome to WebhookDB! In order to complete this integration, we need your email address so that
|
15
15
|
we can associate your information with an organization.
|
16
16
|
</p>
|
17
17
|
<p>Enter your email:</p>
|
@@ -19,9 +19,9 @@
|
|
19
19
|
</p>
|
20
20
|
{% endif %}
|
21
21
|
<p>
|
22
|
-
You can
|
22
|
+
You can connect to your replicated database with the following url:
|
23
23
|
</p>
|
24
|
-
<p>{{ database_url }}</p>
|
24
|
+
<p><code>{{ database_url }}</code></p>
|
25
25
|
<p>
|
26
26
|
To get more out of WebhookDB, like to replicate other APIs or set up Change Data Capture
|
27
27
|
to HTTP endpoints or databases, you can use the WebhookDB CLI.
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Sequel.migration do
|
4
|
+
change do
|
5
|
+
alter_table(:service_integrations) do
|
6
|
+
add_column :webhookdb_api_key, :text, null: true
|
7
|
+
end
|
8
|
+
|
9
|
+
alter_table(:idempotencies) do
|
10
|
+
add_column :stored_result, :jsonb, null: true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Sequel.migration do
|
4
|
+
change do
|
5
|
+
create_table(:saved_queries) do
|
6
|
+
primary_key :id
|
7
|
+
timestamptz :created_at, null: false, default: Sequel.function(:now)
|
8
|
+
timestamptz :updated_at
|
9
|
+
foreign_key :organization_id, :organizations, null: false, unique: true, on_delete: :cascade
|
10
|
+
foreign_key :created_by_id, :customers, on_delete: :set_null
|
11
|
+
text :opaque_id, null: false, unique: true
|
12
|
+
text :description, null: false
|
13
|
+
text :sql, null: false
|
14
|
+
boolean :public, null: false, default: false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/webhookdb/api/db.rb
CHANGED
@@ -124,25 +124,8 @@ class Webhookdb::API::Db < Webhookdb::API::V1
|
|
124
124
|
end
|
125
125
|
post :sql do
|
126
126
|
org = lookup_org!(allow_connstr_auth: true)
|
127
|
-
|
128
|
-
|
129
|
-
rescue Sequel::DatabaseError => e
|
130
|
-
self.logger.error("db_query_database_error", error: e)
|
131
|
-
# We want to handle InsufficientPrivileges and UndefinedTable explicitly
|
132
|
-
# since we can hint the user at what to do.
|
133
|
-
# Otherwise, we should just return the Postgres exception.
|
134
|
-
case e.wrapped_exception
|
135
|
-
when PG::UndefinedTable
|
136
|
-
missing_table = e.wrapped_exception.message.match(/relation (.+) does not/)&.captures&.first
|
137
|
-
msg = "The table #{missing_table} does not exist. Run `webhookdb db tables` to see available tables." if
|
138
|
-
missing_table
|
139
|
-
when PG::InsufficientPrivilege
|
140
|
-
msg = "You do not have permission to perform this query. Queries must be read-only."
|
141
|
-
else
|
142
|
-
msg = e.wrapped_exception.message
|
143
|
-
end
|
144
|
-
merror!(403, msg, code: "invalid_query")
|
145
|
-
end
|
127
|
+
r, msg = execute_readonly_query(org, params[:query])
|
128
|
+
merror!(403, msg, code: "invalid_query") if r.nil?
|
146
129
|
status 200
|
147
130
|
present({rows: r.rows, headers: r.columns, max_rows_reached: r.max_rows_reached})
|
148
131
|
end
|
@@ -250,4 +250,31 @@ module Webhookdb::API::Helpers
|
|
250
250
|
service_integration_opaque_id: opaque_id,
|
251
251
|
)
|
252
252
|
end
|
253
|
+
|
254
|
+
# Run the given SQL inside the org, and use special error handling if it fails.
|
255
|
+
# @return [Array<Webhookdb::Organization::QueryResult,String,nil>] Tuple of query result, and optional message.
|
256
|
+
# On query success, return <QueryResult, nil>.
|
257
|
+
# On DatabaseError, return <nil, message>.
|
258
|
+
# On other types of errors, raise.
|
259
|
+
def execute_readonly_query(org, sql)
|
260
|
+
result = org.execute_readonly_query(sql)
|
261
|
+
return result, nil
|
262
|
+
rescue Sequel::DatabaseError => e
|
263
|
+
self.logger.error("db_query_database_error", error: e)
|
264
|
+
# We want to handle InsufficientPrivileges and UndefinedTable explicitly
|
265
|
+
# since we can hint the user at what to do.
|
266
|
+
# Otherwise, we should just return the Postgres exception.
|
267
|
+
msg = ""
|
268
|
+
case e.wrapped_exception
|
269
|
+
when PG::UndefinedTable
|
270
|
+
missing_table = e.wrapped_exception.message.match(/relation (.+) does not/)&.captures&.first
|
271
|
+
msg = "The table #{missing_table} does not exist. Run `webhookdb db tables` to see available tables." if
|
272
|
+
missing_table
|
273
|
+
when PG::InsufficientPrivilege
|
274
|
+
msg = "You do not have permission to perform this query. Queries must be read-only."
|
275
|
+
else
|
276
|
+
msg = e.wrapped_exception.message
|
277
|
+
end
|
278
|
+
return [nil, msg]
|
279
|
+
end
|
253
280
|
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "webhookdb/api"
|
4
4
|
require "webhookdb/oauth"
|
5
5
|
require "webhookdb/service/view_api"
|
6
|
+
require "webhookdb/front"
|
6
7
|
|
7
8
|
class Webhookdb::API::Install < Webhookdb::API::V1
|
8
9
|
include Webhookdb::Service::ViewApi
|
@@ -37,6 +38,7 @@ class Webhookdb::API::Install < Webhookdb::API::V1
|
|
37
38
|
|
38
39
|
def find_and_verify_user(email:, otp_token:)
|
39
40
|
(me = Webhookdb::Customer.with_email(email)) or forbidden!
|
41
|
+
return me if me.should_skip_authentication?
|
40
42
|
begin
|
41
43
|
Webhookdb::Customer::ResetCode.use_code_with_token(otp_token) do |code|
|
42
44
|
raise Webhookdb::Customer::ResetCode::Unusable unless code.customer === me
|
@@ -189,7 +191,7 @@ class Webhookdb::API::Install < Webhookdb::API::V1
|
|
189
191
|
post :webhook do
|
190
192
|
is_initial_request = request.headers["X-Front-Challenge"].present?
|
191
193
|
if is_initial_request
|
192
|
-
whresp = Webhookdb::Front.initial_verification_request_response(request)
|
194
|
+
whresp = Webhookdb::Front.initial_verification_request_response(request, Webhookdb::Front.app_secret)
|
193
195
|
s_status, s_headers, s_body = whresp.to_rack
|
194
196
|
s_headers.each { |k, v| header k, v }
|
195
197
|
if s_headers["Content-Type"] == "application/json"
|
@@ -236,6 +238,26 @@ class Webhookdb::API::Install < Webhookdb::API::V1
|
|
236
238
|
end
|
237
239
|
end
|
238
240
|
|
241
|
+
resource :front_signalwire do
|
242
|
+
params do
|
243
|
+
requires :type, type: String, values: Webhookdb::Front::CHANNEL_EVENT_TYPES
|
244
|
+
optional :payload, type: JSON
|
245
|
+
end
|
246
|
+
route [:post, :delete], :channel do
|
247
|
+
handle_webhook_request("front-signalwire-channel") do
|
248
|
+
auth_header = request.headers["Authorization"]
|
249
|
+
merror!(401, "Missing Authorization header", code: "unauthenticated") if
|
250
|
+
auth_header.nil?
|
251
|
+
merror!(401, "Expected Bearer authorization", code: "unauthenticated") unless
|
252
|
+
auth_header.start_with?("Bearer ")
|
253
|
+
apikey = auth_header[7..]
|
254
|
+
sint = Webhookdb::ServiceIntegration.for_api_key(apikey)
|
255
|
+
merror!(401, "Invalid API key", code: "unauthenticated") if sint.nil?
|
256
|
+
sint
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
239
261
|
resource :intercom do
|
240
262
|
post :webhook do
|
241
263
|
# Because the `_webhook_response` function is always the same here, I'm wondering if it's even
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webhookdb/api"
|
4
|
+
require "webhookdb/saved_query"
|
5
|
+
|
6
|
+
class Webhookdb::API::SavedQueries < Webhookdb::API::V1
|
7
|
+
resource :organizations do
|
8
|
+
route_param :org_identifier do
|
9
|
+
resource :saved_queries do
|
10
|
+
helpers do
|
11
|
+
def lookup!
|
12
|
+
org = lookup_org!
|
13
|
+
# We can add other identifiers in the future
|
14
|
+
cq = org.saved_queries_dataset[opaque_id: params[:query_identifier]]
|
15
|
+
merror!(403, "There is no saved query with that identifier.") if cq.nil?
|
16
|
+
return cq
|
17
|
+
end
|
18
|
+
|
19
|
+
def guard_editable!(customer, cq)
|
20
|
+
return if customer === cq.created_by
|
21
|
+
return if has_admin?(cq.organization, customer:)
|
22
|
+
permission_error!("You must be the query's creator or an org admin.")
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute_readonly_query_with_suggestion(org, sql)
|
26
|
+
r, message = execute_readonly_query(org, sql)
|
27
|
+
return r, nil unless r.nil?
|
28
|
+
msg = "Something went wrong running your query. Perhaps a table it depends on was deleted. " \
|
29
|
+
"Check out #{Webhookdb::SavedQuery::DOCS_URL} for troubleshooting tips. " \
|
30
|
+
"Here's what went wrong: #{message}"
|
31
|
+
return r, msg
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Returns a list of all custom queries associated with the org."
|
36
|
+
get do
|
37
|
+
queries = lookup_org!.saved_queries
|
38
|
+
message = ""
|
39
|
+
if queries.empty?
|
40
|
+
message = "This organization doesn't have any saved queries yet.\n" \
|
41
|
+
"Use `webhookdb saved-query create` to set one up."
|
42
|
+
end
|
43
|
+
present_collection queries, with: SavedQueryEntity, message:
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "Creates a custom query."
|
47
|
+
params do
|
48
|
+
optional :description, type: String, prompt: "What is the query used for? "
|
49
|
+
optional :sql, type: String, prompt: "Enter the SQL you would like to run: "
|
50
|
+
optional :public, type: Boolean
|
51
|
+
end
|
52
|
+
post :create do
|
53
|
+
cust = current_customer
|
54
|
+
org = lookup_org!
|
55
|
+
_, errmsg = execute_readonly_query_with_suggestion(org, params[:sql])
|
56
|
+
if errmsg
|
57
|
+
Webhookdb::API::Helpers.prompt_for_required_param!(
|
58
|
+
request,
|
59
|
+
:sql,
|
60
|
+
"Enter a new query:",
|
61
|
+
output: "That query was invalid. #{errmsg}\n" \
|
62
|
+
"You can iterate on your query by connecting to your database from any SQL editor.\n" \
|
63
|
+
"Use `webhookdb db connection` to get your query string.",
|
64
|
+
)
|
65
|
+
end
|
66
|
+
cq = Webhookdb::SavedQuery.create(
|
67
|
+
description: params[:description],
|
68
|
+
sql: params[:sql],
|
69
|
+
organization: org,
|
70
|
+
created_by: cust,
|
71
|
+
public: params[:public] || false,
|
72
|
+
)
|
73
|
+
message = "You have created a new saved query with an id of '#{cq.opaque_id}'. " \
|
74
|
+
"You can run it through the CLI, or through the API with or without authentication. " \
|
75
|
+
"See #{Webhookdb::SavedQuery::DOCS_URL} for more information."
|
76
|
+
status 200
|
77
|
+
present cq, with: SavedQueryEntity, message:
|
78
|
+
end
|
79
|
+
|
80
|
+
route_param :query_identifier, type: String do
|
81
|
+
desc "Returns the query with the given identifier."
|
82
|
+
get do
|
83
|
+
cq = lookup!
|
84
|
+
status 200
|
85
|
+
message = "See #{Webhookdb::SavedQuery::DOCS_URL} to see how to run or modify your query."
|
86
|
+
present cq, with: SavedQueryEntity, message:
|
87
|
+
end
|
88
|
+
|
89
|
+
desc "Runs the query with the given identifier."
|
90
|
+
get :run do
|
91
|
+
_customer = current_customer
|
92
|
+
org = lookup_org!
|
93
|
+
cq = lookup!
|
94
|
+
r, msg = execute_readonly_query_with_suggestion(org, cq.sql)
|
95
|
+
merror!(400, msg) if r.nil?
|
96
|
+
status 200
|
97
|
+
present({rows: r.rows, headers: r.columns, max_rows_reached: r.max_rows_reached})
|
98
|
+
end
|
99
|
+
|
100
|
+
desc "Updates the field on a custom query."
|
101
|
+
params do
|
102
|
+
optional :field, type: String, prompt: "What field would you like to update (one of: " \
|
103
|
+
"#{Webhookdb::SavedQuery::CLI_EDITABLE_FIELDS.join(', ')}): "
|
104
|
+
optional :value, type: String, prompt: "What is the new value? "
|
105
|
+
end
|
106
|
+
post :update do
|
107
|
+
customer = current_customer
|
108
|
+
cq = lookup!
|
109
|
+
guard_editable!(customer, cq)
|
110
|
+
# Instead of specifying which values are valid for the optional `field` param in the param declaration,
|
111
|
+
# we do the validation here so that we can provide a more helpful error message
|
112
|
+
unless Webhookdb::SavedQuery::CLI_EDITABLE_FIELDS.include?(params[:field])
|
113
|
+
merror!(400, "That field is not editable.")
|
114
|
+
end
|
115
|
+
value = params[:value]
|
116
|
+
case params[:field]
|
117
|
+
when "public"
|
118
|
+
begin
|
119
|
+
value = Webhookdb.parse_bool(value)
|
120
|
+
rescue ArgumentError => e
|
121
|
+
Webhookdb::API::Helpers.prompt_for_required_param!(
|
122
|
+
request,
|
123
|
+
:value,
|
124
|
+
e.message + "\nAny boolean-like string (true, false, yes, no, etc) will work:",
|
125
|
+
)
|
126
|
+
end
|
127
|
+
cq.public = value
|
128
|
+
when "sql"
|
129
|
+
r, msg = execute_readonly_query_with_suggestion(cq.organization, value)
|
130
|
+
if r.nil?
|
131
|
+
Webhookdb::API::Helpers.prompt_for_required_param!(
|
132
|
+
request,
|
133
|
+
:value,
|
134
|
+
"Enter your query:",
|
135
|
+
output: msg,
|
136
|
+
)
|
137
|
+
end
|
138
|
+
cq.sql = value
|
139
|
+
else
|
140
|
+
cq.send(:"#{params[:field]}=", value)
|
141
|
+
end
|
142
|
+
cq.save_changes
|
143
|
+
status 200
|
144
|
+
# Do not render the value here, it can be very long.
|
145
|
+
message = "You have updated '#{params[:field]}' on saved query '#{cq.opaque_id}'."
|
146
|
+
present cq, with: SavedQueryEntity, message:
|
147
|
+
end
|
148
|
+
|
149
|
+
params do
|
150
|
+
optional :field, type: String, values: Webhookdb::SavedQuery::INFO_FIELDS.keys + [""]
|
151
|
+
end
|
152
|
+
post :info do
|
153
|
+
cq = lookup!
|
154
|
+
data = Webhookdb::SavedQuery::INFO_FIELDS.
|
155
|
+
to_h { |k, v| [k.to_sym, cq.send(v)] }
|
156
|
+
|
157
|
+
field_name = params[:field]
|
158
|
+
blocks = Webhookdb::Formatting.blocks
|
159
|
+
if field_name.present?
|
160
|
+
blocks.line(data.fetch(field_name.to_sym))
|
161
|
+
else
|
162
|
+
rows = data.map do |k, v|
|
163
|
+
[k.to_s.humanize, v.to_s]
|
164
|
+
end
|
165
|
+
blocks.table(["Field", "Value"], rows)
|
166
|
+
end
|
167
|
+
r = {blocks: blocks.as_json}
|
168
|
+
status 200
|
169
|
+
present r
|
170
|
+
end
|
171
|
+
|
172
|
+
post :delete do
|
173
|
+
customer = current_customer
|
174
|
+
cq = lookup!
|
175
|
+
guard_editable!(customer, cq)
|
176
|
+
cq.destroy
|
177
|
+
status 200
|
178
|
+
present cq, with: SavedQueryEntity,
|
179
|
+
message: "You have successfully deleted the saved query '#{cq.description}'."
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
resource :saved_queries do
|
187
|
+
route_param :query_identifier, type: String do
|
188
|
+
get :run do
|
189
|
+
# This endpoint can be used publicly, so should expose as little information as possible.
|
190
|
+
# Do not expose permissions or query details.
|
191
|
+
cq = Webhookdb::SavedQuery[opaque_id: params[:query_identifier]]
|
192
|
+
forbidden! if cq.nil?
|
193
|
+
if cq.private?
|
194
|
+
authed = Webhookdb::API::ConnstrAuth.find_authed([cq.organization], request)
|
195
|
+
if !authed && (cust = current_customer?)
|
196
|
+
authed = !cust.verified_memberships_dataset.where(organization: cq.organization).empty?
|
197
|
+
end
|
198
|
+
forbidden! unless authed
|
199
|
+
end
|
200
|
+
r, _ = execute_readonly_query(cq.organization, cq.sql)
|
201
|
+
merror!(400, "Something went wrong running the query.") if r.nil?
|
202
|
+
status 200
|
203
|
+
present({rows: r.rows, headers: r.columns, max_rows_reached: r.max_rows_reached})
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
class SavedQueryEntity < Webhookdb::API::BaseEntity
|
209
|
+
expose :opaque_id, as: :id
|
210
|
+
expose :description
|
211
|
+
expose :sql
|
212
|
+
expose :public
|
213
|
+
expose :run_url
|
214
|
+
|
215
|
+
def self.display_headers
|
216
|
+
return [[:id, "Id"], [:description, "Description"], [:public, "Public"], [:run_url, "Run URL"]]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -187,19 +187,14 @@ If the list does not look correct, you can contact support at #{Webhookdb.suppor
|
|
187
187
|
|
188
188
|
desc "Returns information about the integration."
|
189
189
|
params do
|
190
|
-
optional :field, type: String, values: Webhookdb::ServiceIntegration::INTEGRATION_INFO_FIELDS
|
190
|
+
optional :field, type: String, values: Webhookdb::ServiceIntegration::INTEGRATION_INFO_FIELDS.keys + [""]
|
191
191
|
end
|
192
192
|
post :info do
|
193
193
|
ensure_plan_supports!
|
194
194
|
org = lookup_org!
|
195
195
|
sint = lookup_service_integration!(org, params[:sint_identifier])
|
196
|
-
data =
|
197
|
-
|
198
|
-
service: sint.service_name,
|
199
|
-
table: sint.table_name,
|
200
|
-
url: sint.replicator.webhook_endpoint,
|
201
|
-
webhook_secret: sint.webhook_secret,
|
202
|
-
}
|
196
|
+
data = Webhookdb::ServiceIntegration::INTEGRATION_INFO_FIELDS.
|
197
|
+
to_h { |k, v| [k.to_sym, sint.send(v)] }
|
203
198
|
|
204
199
|
field_name = params[:field]
|
205
200
|
blocks = Webhookdb::Formatting.blocks
|
@@ -207,7 +202,7 @@ If the list does not look correct, you can contact support at #{Webhookdb.suppor
|
|
207
202
|
blocks.line(data.fetch(field_name.to_sym))
|
208
203
|
else
|
209
204
|
rows = data.map do |k, v|
|
210
|
-
[k.to_s.
|
205
|
+
[k.to_s.humanize, v]
|
211
206
|
end
|
212
207
|
blocks.table(["Field", "Value"], rows)
|
213
208
|
end
|
@@ -216,6 +211,16 @@ If the list does not look correct, you can contact support at #{Webhookdb.suppor
|
|
216
211
|
present r
|
217
212
|
end
|
218
213
|
|
214
|
+
post :roll_api_key do
|
215
|
+
ensure_plan_supports!
|
216
|
+
org = lookup_org!
|
217
|
+
sint = lookup_service_integration!(org, params[:sint_identifier])
|
218
|
+
sint.update(webhookdb_api_key: sint.new_api_key)
|
219
|
+
r = {webhookdb_api_key: sint.webhookdb_api_key}
|
220
|
+
status 200
|
221
|
+
present r
|
222
|
+
end
|
223
|
+
|
219
224
|
resource :backfill do
|
220
225
|
helpers do
|
221
226
|
def lookup_backfillable_replicator(customer:, allow_connstr_auth: false)
|
@@ -370,9 +375,9 @@ The tables and all data for this integration and its dependents will also be rem
|
|
370
375
|
if sint.dependents.empty?
|
371
376
|
confirmation_msg = "Great! We've deleted all secrets for #{sint.service_name}. " \
|
372
377
|
"The table #{sint.table_name} containing its data has been dropped."
|
373
|
-
|
374
|
-
|
375
|
-
|
378
|
+
else
|
379
|
+
confirmation_msg = "Great! We've deleted all secrets for #{sint.service_name} and its dependents. " \
|
380
|
+
"The following tables have been dropped:\n\n#{sint.table_name}\n#{dependents_lines}"
|
376
381
|
end
|
377
382
|
present sint, with: Webhookdb::API::ServiceIntegrationEntity, message: confirmation_msg
|
378
383
|
end
|
@@ -112,18 +112,14 @@ class Webhookdb::API::SyncTargets < Webhookdb::API::V1
|
|
112
112
|
params do
|
113
113
|
use :connection_url
|
114
114
|
use :sync_target_params
|
115
|
-
|
116
|
-
type: String, allow_blank: false,
|
117
|
-
desc: "This is a deprecated parameter. In the future, please use `service_integration_identifier`."
|
118
|
-
optional :service_integration_identifier, type: String, allow_blank: false
|
119
|
-
at_least_one_of :service_integration_opaque_id, :service_integration_identifier
|
115
|
+
requires :service_integration_identifier, type: String, allow_blank: false
|
120
116
|
end
|
121
117
|
route_setting :target_type, target_type_resource
|
122
118
|
post :create do
|
123
119
|
customer = current_customer
|
124
120
|
org = lookup_org!(customer:)
|
125
121
|
ensure_admin!(org, customer:)
|
126
|
-
identifier = params[:service_integration_identifier]
|
122
|
+
identifier = params[:service_integration_identifier]
|
127
123
|
sint = lookup_service_integration!(org, identifier)
|
128
124
|
|
129
125
|
validate_period!(sint.organization, params[:period_seconds])
|
data/lib/webhookdb/api/system.rb
CHANGED
@@ -12,6 +12,10 @@ class Webhookdb::API::System < Webhookdb::Service
|
|
12
12
|
require "webhookdb/service/helpers"
|
13
13
|
helpers Webhookdb::Service::Helpers
|
14
14
|
|
15
|
+
get "/" do
|
16
|
+
redirect "/terminal/"
|
17
|
+
end
|
18
|
+
|
15
19
|
get :healthz do
|
16
20
|
# Do not bother looking at dependencies like databases.
|
17
21
|
# If the primary is down, we can still accept webhooks
|
@@ -34,8 +38,15 @@ class Webhookdb::API::System < Webhookdb::Service
|
|
34
38
|
|
35
39
|
if ["development", "test"].include?(Webhookdb::RACK_ENV)
|
36
40
|
resource :debug do
|
37
|
-
|
38
|
-
|
41
|
+
resource :echo do
|
42
|
+
[:get, :post, :patch, :put, :delete].each do |m|
|
43
|
+
self.send(m) do
|
44
|
+
pp params.to_h
|
45
|
+
pp request.headers
|
46
|
+
status 200
|
47
|
+
present({})
|
48
|
+
end
|
49
|
+
end
|
39
50
|
end
|
40
51
|
end
|
41
52
|
end
|
@@ -6,46 +6,40 @@ class Webhookdb::API::WebhookSubscriptions < Webhookdb::API::V1
|
|
6
6
|
resource :organizations do
|
7
7
|
route_param :org_identifier, type: String do
|
8
8
|
resource :webhook_subscriptions do
|
9
|
-
desc "Return all
|
9
|
+
desc "Return all notifications for the given org and its integrations."
|
10
10
|
get do
|
11
11
|
org = lookup_org!
|
12
12
|
subs = org.all_webhook_subscriptions
|
13
13
|
message = ""
|
14
14
|
if subs.empty?
|
15
15
|
message = "Organization #{org.name} has no webhook subscriptions set up.\n" \
|
16
|
-
"Use `webhookdb
|
16
|
+
"Use `webhookdb notifications create` to set one up."
|
17
17
|
end
|
18
18
|
status 200
|
19
19
|
present_collection subs, with: Webhookdb::API::WebhookSubscriptionEntity, message:
|
20
20
|
end
|
21
21
|
|
22
22
|
params do
|
23
|
-
optional :url, prompt: "Enter the URL that WebhookDB should POST webhooks to:"
|
24
|
-
optional :webhook_secret, prompt: "Enter a random secret used to sign and verify webhooks to the given url:"
|
25
23
|
optional :service_integration_identifier,
|
26
24
|
type: String,
|
27
|
-
desc: "If provided, attach the webhook subscription to this integration rather than the org."
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
desc: "If provided, attach the webhook subscription to this integration rather than the org.",
|
26
|
+
prompt: "Which integration is this for? Use the service name, table name, or opaque id.\n" \
|
27
|
+
"See your integrations with `webhookdb integrations list`:"
|
28
|
+
optional :url, prompt: "Enter the URL that WebhookDB should POST notifications to:"
|
29
|
+
optional :webhook_secret,
|
30
|
+
prompt: "Enter a random secret used to sign and verify notifications to the given url:"
|
31
31
|
end
|
32
32
|
post :create do
|
33
33
|
org = lookup_org!
|
34
|
-
sint =
|
35
|
-
|
36
|
-
sint = lookup_service_integration!(org, identifier) if identifier.present?
|
34
|
+
sint = lookup_service_integration!(org, params[:service_integration_identifier])
|
35
|
+
url = params[:url]
|
37
36
|
webhook_sub = Webhookdb::WebhookSubscription.create(
|
38
37
|
webhook_secret: params[:webhook_secret],
|
39
|
-
deliver_to_url:
|
40
|
-
organization: sint ? nil : org,
|
38
|
+
deliver_to_url: url,
|
41
39
|
service_integration: sint,
|
42
40
|
created_by: current_customer,
|
43
41
|
)
|
44
|
-
message =
|
45
|
-
"All webhooks for this #{sint.service_name} integration will be sent to #{params[:url]}"
|
46
|
-
else
|
47
|
-
"All webhooks for all integrations belonging to organization #{org.name} will be sent to #{params[:url]}."
|
48
|
-
end
|
42
|
+
message = "All notifications for this #{sint.service_name} integration will be sent to #{url}"
|
49
43
|
status 200
|
50
44
|
present webhook_sub, with: Webhookdb::API::WebhookSubscriptionEntity, message:
|
51
45
|
end
|
data/lib/webhookdb/api.rb
CHANGED
@@ -60,13 +60,22 @@ module Webhookdb::API
|
|
60
60
|
return org
|
61
61
|
end
|
62
62
|
|
63
|
-
|
63
|
+
# rubocop:disable Naming/PredicateName
|
64
|
+
def has_admin?(org=nil, customer: nil)
|
65
|
+
# rubocop:enable Naming/PredicateName
|
64
66
|
customer ||= current_customer
|
65
67
|
org ||= lookup_org!
|
66
68
|
has_no_admin = org.verified_memberships_dataset.
|
67
69
|
where(customer:, membership_role: Webhookdb::Role.admin_role).
|
68
70
|
empty?
|
69
|
-
|
71
|
+
return !has_no_admin
|
72
|
+
end
|
73
|
+
|
74
|
+
def ensure_admin!(org=nil, customer: nil)
|
75
|
+
org ||= lookup_org!
|
76
|
+
admin = has_admin?(org, customer:)
|
77
|
+
# noinspection RubyNilAnalysis
|
78
|
+
permission_error!("You don't have admin privileges with #{org.name}.") unless admin
|
70
79
|
end
|
71
80
|
end
|
72
81
|
|
data/lib/webhookdb/apps.rb
CHANGED
@@ -17,6 +17,7 @@ require "webhookdb/api/install"
|
|
17
17
|
require "webhookdb/api/me"
|
18
18
|
require "webhookdb/api/organizations"
|
19
19
|
require "webhookdb/api/replay"
|
20
|
+
require "webhookdb/api/saved_queries"
|
20
21
|
require "webhookdb/api/service_integrations"
|
21
22
|
require "webhookdb/api/services"
|
22
23
|
require "webhookdb/api/stripe"
|
@@ -68,6 +69,7 @@ module Webhookdb::Apps
|
|
68
69
|
mount Webhookdb::API::Me
|
69
70
|
mount Webhookdb::API::Organizations
|
70
71
|
mount Webhookdb::API::Replay
|
72
|
+
mount Webhookdb::API::SavedQueries
|
71
73
|
mount Webhookdb::API::ServiceIntegrations
|
72
74
|
mount Webhookdb::API::Services
|
73
75
|
mount Webhookdb::API::Stripe
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Webhookdb
|
4
|
+
module Envfixer
|
5
|
+
# If DOCKER_DEV is set, replace 'localhost' urls with 'host.docker.internal'.
|
6
|
+
def self.replace_localhost_for_docker(env)
|
7
|
+
return unless env["DOCKER_DEV"]
|
8
|
+
env.each do |k, v|
|
9
|
+
begin
|
10
|
+
localhost = URI(v).host == "localhost"
|
11
|
+
rescue StandardError
|
12
|
+
next
|
13
|
+
end
|
14
|
+
next unless localhost
|
15
|
+
env[k] = v.gsub("localhost", "host.docker.internal")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# If MERGE_HEROKU_ENV, merge all of its environment vars into the current env
|
20
|
+
def self.merge_heroku_env(env)
|
21
|
+
return unless (heroku_app = env.fetch("MERGE_HEROKU_ENV", nil))
|
22
|
+
text = `heroku config -j --app=#{heroku_app}`
|
23
|
+
json = Oj.load(text)
|
24
|
+
json.each do |k, v|
|
25
|
+
env[k] = v
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|