webhookdb 1.0.2 → 1.2.0

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