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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/data/messages/web/install-customer-login.liquid +1 -1
  3. data/data/messages/web/install-success.liquid +2 -2
  4. data/db/migrations/038_webhookdb_api_key.rb +13 -0
  5. data/db/migrations/039_saved_query.rb +17 -0
  6. data/lib/webhookdb/api/db.rb +2 -19
  7. data/lib/webhookdb/api/helpers.rb +27 -0
  8. data/lib/webhookdb/api/install.rb +23 -1
  9. data/lib/webhookdb/api/saved_queries.rb +219 -0
  10. data/lib/webhookdb/api/service_integrations.rb +17 -12
  11. data/lib/webhookdb/api/sync_targets.rb +2 -6
  12. data/lib/webhookdb/api/system.rb +13 -2
  13. data/lib/webhookdb/api/webhook_subscriptions.rb +12 -18
  14. data/lib/webhookdb/api.rb +11 -2
  15. data/lib/webhookdb/apps.rb +2 -0
  16. data/lib/webhookdb/email_octopus.rb +2 -0
  17. data/lib/webhookdb/envfixer.rb +29 -0
  18. data/lib/webhookdb/fixtures/saved_queries.rb +27 -0
  19. data/lib/webhookdb/fixtures/service_integrations.rb +4 -0
  20. data/lib/webhookdb/front.rb +23 -11
  21. data/lib/webhookdb/google_calendar.rb +6 -0
  22. data/lib/webhookdb/icalendar.rb +6 -0
  23. data/lib/webhookdb/idempotency.rb +94 -33
  24. data/lib/webhookdb/jobs/backfill.rb +24 -5
  25. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +7 -1
  26. data/lib/webhookdb/jobs/prepare_database_connections.rb +2 -2
  27. data/lib/webhookdb/jobs/scheduled_backfills.rb +19 -13
  28. data/lib/webhookdb/oauth/{front.rb → front_provider.rb} +21 -4
  29. data/lib/webhookdb/oauth/{intercom.rb → intercom_provider.rb} +1 -1
  30. data/lib/webhookdb/oauth.rb +8 -7
  31. data/lib/webhookdb/organization/alerting.rb +11 -0
  32. data/lib/webhookdb/organization.rb +8 -2
  33. data/lib/webhookdb/postgres/model_utilities.rb +19 -0
  34. data/lib/webhookdb/postgres.rb +3 -0
  35. data/lib/webhookdb/replicator/column.rb +9 -1
  36. data/lib/webhookdb/replicator/front_conversation_v1.rb +5 -1
  37. data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +2 -5
  38. data/lib/webhookdb/replicator/front_message_v1.rb +5 -1
  39. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +325 -0
  40. data/lib/webhookdb/replicator/front_v1_mixin.rb +9 -1
  41. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +6 -3
  42. data/lib/webhookdb/replicator/signalwire_message_v1.rb +28 -14
  43. data/lib/webhookdb/replicator/twilio_sms_v1.rb +3 -1
  44. data/lib/webhookdb/saved_query.rb +28 -0
  45. data/lib/webhookdb/service_integration.rb +36 -3
  46. data/lib/webhookdb/signalwire.rb +40 -0
  47. data/lib/webhookdb/spec_helpers/citest.rb +18 -9
  48. data/lib/webhookdb/spec_helpers/postgres.rb +9 -0
  49. data/lib/webhookdb/spec_helpers/service.rb +5 -0
  50. data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +25 -13
  51. data/lib/webhookdb/spec_helpers/whdb.rb +7 -0
  52. data/lib/webhookdb/sync_target.rb +1 -1
  53. data/lib/webhookdb/tasks/specs.rb +4 -2
  54. data/lib/webhookdb/version.rb +1 -1
  55. data/lib/webhookdb.rb +24 -26
  56. metadata +39 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eaccd930255b228e07aec00b36d43b6bfc47d0d57fca0ff7fe721e2bee3e09d1
4
- data.tar.gz: e51b55c7dd624833ba56d11ecd666e6d3977c8c4e3d92f4b8146c3f8c9c2a47a
3
+ metadata.gz: d2c4da6abd3c5def6f27e180aaf435a4e9293eae4a1f926a065b87c1979372b3
4
+ data.tar.gz: 72de7406adc5049255878c5bf16fe05e92716d18c2f2492d996dbddb6b0aa307
5
5
  SHA512:
6
- metadata.gz: 1b83e8bd2da2e8857e4fbfc4fce5ec2b92ebf22438d047014a337540bfab293412d3327acf13d470cd2063b6f2b712f568e7e225355af8b978a631befc17eea0
7
- data.tar.gz: b5ef0bb3313894aab5b8c939905cbbd0f19196a96255ca3346a139b4f9338cedf55ad94ee55ac418395651c6d9a0d9d6949614de5965e651f1ea232354abd33b
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 Webhookdb! In order to complete this integration, we need your email address so that
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 access the information using the following url:
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
@@ -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
- begin
128
- r = org.execute_readonly_query(params[:query])
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
- id: sint.opaque_id,
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.capitalize, v]
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
- else
374
- confirmation_msg = "Great! We've deleted all secrets for #{sint.service_name} and its dependents. " \
375
- "The following tables have been dropped: \n\n #{sint.table_name} \n #{dependents_lines}"
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
- optional :service_integration_opaque_id,
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] || params[:service_integration_opaque_id]
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])
@@ -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
- get :echo do
38
- pp params.to_h
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 webhook subscriptions for the given org, and all integrations."
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 webhooks create` to set one up."
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
- optional :service_integration_opaque_id,
29
- type: String,
30
- desc: "This is a deprecated parameter. In the future, please use `service_integration_identifier`."
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 = nil
35
- identifier = params[:service_integration_identifier] || params[:service_integration_opaque_id]
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: params[: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 = if sint
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
- def ensure_admin!(org=nil, customer: nil)
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
- permission_error!("You don't have admin privileges with #{org.name}.") if has_no_admin
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
 
@@ -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
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "appydays/configurable"
4
4
 
5
+ require "webhookdb/crypto"
6
+
5
7
  module Webhookdb::EmailOctopus
6
8
  include Appydays::Configurable
7
9
 
@@ -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