webhookdb 1.0.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|