webhookdb 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/admin-dist/assets/index-6aebf805.js +264 -0
  3. data/admin-dist/favicon.ico +0 -0
  4. data/admin-dist/index.html +130 -0
  5. data/admin-dist/manifest.json +15 -0
  6. data/data/messages/replicators/url-recorder.liquid +20 -0
  7. data/data/messages/templates/errors/signalwire_send_sms.email.liquid +31 -0
  8. data/data/messages/web/install-customer-login.liquid +6 -5
  9. data/data/messages/web/install-error.liquid +1 -1
  10. data/data/messages/web/install-forbidden.liquid +25 -0
  11. data/data/messages/web/install-org-chooser.liquid +40 -0
  12. data/data/messages/web/install-success.liquid +2 -1
  13. data/data/messages/web/install.liquid +2 -1
  14. data/data/messages/web/partials/head.liquid +2 -0
  15. data/data/messages/web/styles.liquid +24 -0
  16. data/db/migrations/041_views.rb +20 -0
  17. data/db/migrations/042_sint_lock.rb +10 -0
  18. data/db/migrations/043_text_search.rb +28 -0
  19. data/db/migrations/044_oauth_session_token_cache.rb +21 -0
  20. data/integration/auth_spec.rb +2 -2
  21. data/lib/sequel/plugins/text_searchable.rb +165 -0
  22. data/lib/sequel/text_searchable.rb +42 -0
  23. data/lib/webhookdb/admin_api/auth.rb +24 -3
  24. data/lib/webhookdb/admin_api/data_provider.rb +196 -0
  25. data/lib/webhookdb/admin_api/entities.rb +143 -28
  26. data/lib/webhookdb/admin_api.rb +0 -2
  27. data/lib/webhookdb/api/auth.rb +5 -6
  28. data/lib/webhookdb/api/db.rb +31 -6
  29. data/lib/webhookdb/api/entities.rb +7 -1
  30. data/lib/webhookdb/api/helpers.rb +6 -25
  31. data/lib/webhookdb/api/install.rb +204 -79
  32. data/lib/webhookdb/api/organizations.rb +14 -12
  33. data/lib/webhookdb/api/saved_queries.rb +9 -3
  34. data/lib/webhookdb/api/saved_views.rb +99 -0
  35. data/lib/webhookdb/api/service_integrations.rb +15 -9
  36. data/lib/webhookdb/api/subscriptions.rb +3 -1
  37. data/lib/webhookdb/api/sync_targets.rb +9 -7
  38. data/lib/webhookdb/api/system.rb +1 -0
  39. data/lib/webhookdb/api/webhook_subscriptions.rb +3 -1
  40. data/lib/webhookdb/apps.rb +30 -7
  41. data/lib/webhookdb/async/audit_logger.rb +2 -0
  42. data/lib/webhookdb/async.rb +5 -0
  43. data/lib/webhookdb/backfill_job/service_integration_lock.rb +22 -0
  44. data/lib/webhookdb/backfill_job.rb +9 -0
  45. data/lib/webhookdb/customer.rb +5 -0
  46. data/lib/webhookdb/database_document.rb +1 -1
  47. data/lib/webhookdb/db_adapter/default_sql.rb +1 -1
  48. data/lib/webhookdb/db_adapter.rb +20 -4
  49. data/lib/webhookdb/fixtures/message_bodies.rb +34 -0
  50. data/lib/webhookdb/fixtures/organizations.rb +5 -0
  51. data/lib/webhookdb/fixtures/roles.rb +14 -0
  52. data/lib/webhookdb/fixtures/saved_views.rb +25 -0
  53. data/lib/webhookdb/fixtures/webhook_subscription_deliveries.rb +18 -0
  54. data/lib/webhookdb/http.rb +8 -2
  55. data/lib/webhookdb/icalendar.rb +3 -0
  56. data/lib/webhookdb/idempotency.rb +69 -22
  57. data/lib/webhookdb/increase.rb +69 -21
  58. data/lib/webhookdb/intercom.rb +10 -3
  59. data/lib/webhookdb/jobs/backfill.rb +3 -1
  60. data/lib/webhookdb/jobs/emailer.rb +0 -1
  61. data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +19 -0
  62. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +1 -1
  63. data/lib/webhookdb/jobs/icalendar_sync.rb +1 -1
  64. data/lib/webhookdb/jobs/increase_event_handler.rb +20 -0
  65. data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -1
  66. data/lib/webhookdb/jobs/sync_target_run_sync.rb +3 -1
  67. data/lib/webhookdb/message/body.rb +6 -4
  68. data/lib/webhookdb/message/delivery.rb +2 -0
  69. data/lib/webhookdb/messages/error_icalendar_fetch.rb +1 -2
  70. data/lib/webhookdb/messages/error_signalwire_send_sms.rb +48 -0
  71. data/lib/webhookdb/oauth/fake_provider.rb +44 -0
  72. data/lib/webhookdb/oauth/front_provider.rb +1 -2
  73. data/lib/webhookdb/oauth/increase_provider.rb +80 -0
  74. data/lib/webhookdb/oauth/intercom_provider.rb +3 -11
  75. data/lib/webhookdb/oauth/session.rb +20 -0
  76. data/lib/webhookdb/oauth.rb +7 -21
  77. data/lib/webhookdb/organization/alerting.rb +2 -0
  78. data/lib/webhookdb/organization/database_migration.rb +3 -0
  79. data/lib/webhookdb/organization.rb +37 -6
  80. data/lib/webhookdb/organization_membership.rb +14 -7
  81. data/lib/webhookdb/postgres.rb +2 -0
  82. data/lib/webhookdb/replicator/base.rb +1 -0
  83. data/lib/webhookdb/replicator/docgen.rb +9 -1
  84. data/lib/webhookdb/replicator/fake.rb +2 -3
  85. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +49 -14
  86. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +97 -17
  87. data/lib/webhookdb/replicator/icalendar_event_v1.rb +104 -2
  88. data/lib/webhookdb/replicator/increase_account_number_v1.rb +6 -43
  89. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +7 -24
  90. data/lib/webhookdb/replicator/increase_account_v1.rb +7 -31
  91. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +5 -43
  92. data/lib/webhookdb/replicator/increase_app_v1.rb +78 -0
  93. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +23 -29
  94. data/lib/webhookdb/replicator/increase_event_v1.rb +41 -0
  95. data/lib/webhookdb/replicator/increase_limit_v1.rb +9 -34
  96. data/lib/webhookdb/replicator/increase_transaction_v1.rb +5 -30
  97. data/lib/webhookdb/replicator/increase_v1_mixin.rb +58 -78
  98. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +5 -24
  99. data/lib/webhookdb/replicator/intercom_contact_v1.rb +51 -4
  100. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +42 -6
  101. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +2 -13
  102. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +20 -16
  103. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +1 -1
  104. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
  105. data/lib/webhookdb/replicator/transistor_episode_v1.rb +17 -0
  106. data/lib/webhookdb/replicator/url_recorder_v1.rb +137 -0
  107. data/lib/webhookdb/replicator/webhook_request.rb +4 -0
  108. data/lib/webhookdb/replicator.rb +8 -0
  109. data/lib/webhookdb/role.rb +5 -2
  110. data/lib/webhookdb/saved_query.rb +23 -0
  111. data/lib/webhookdb/saved_view.rb +73 -0
  112. data/lib/webhookdb/sentry.rb +2 -0
  113. data/lib/webhookdb/service/entities.rb +0 -4
  114. data/lib/webhookdb/service/helpers.rb +5 -0
  115. data/lib/webhookdb/service/middleware.rb +9 -0
  116. data/lib/webhookdb/service/types.rb +10 -8
  117. data/lib/webhookdb/service/validators.rb +1 -2
  118. data/lib/webhookdb/service/view_api.rb +1 -1
  119. data/lib/webhookdb/service_integration.rb +17 -15
  120. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +8 -8
  121. data/lib/webhookdb/spec_helpers/whdb.rb +3 -2
  122. data/lib/webhookdb/subscription.rb +2 -0
  123. data/lib/webhookdb/sync_target.rb +10 -2
  124. data/lib/webhookdb/tasks/message.rb +3 -1
  125. data/lib/webhookdb/version.rb +1 -1
  126. data/lib/webhookdb/webhook_subscription/delivery.rb +2 -0
  127. data/lib/webhookdb/webhook_subscription.rb +2 -0
  128. metadata +57 -9
  129. data/lib/webhookdb/admin_api/customers.rb +0 -63
  130. data/lib/webhookdb/admin_api/message_deliveries.rb +0 -61
  131. data/lib/webhookdb/admin_api/roles.rb +0 -15
@@ -12,12 +12,16 @@ class Webhookdb::API::Install < Webhookdb::API::V1
12
12
  helpers do
13
13
  def lookup_session!
14
14
  session = Webhookdb::Oauth::Session.usable.where(oauth_state: params[:state]).first
15
- forbidden! unless session
15
+ error!("Forbidden", 302, {"Location" => "/v1/install/#{oauth_provider.key}/forbidden"}) if session.nil?
16
16
  return session
17
17
  end
18
18
 
19
19
  def handle_login(email:, session:, action_url:)
20
- new_customer, me = Webhookdb::Customer.find_or_create_for_email(email)
20
+ begin
21
+ new_customer, me = Webhookdb::Customer.find_or_create_for_email(email)
22
+ rescue Sequel::ValidationFailed => e
23
+ raise FormError.new(e.message.capitalize, 400)
24
+ end
21
25
  me.reset_codes_dataset.usable.each(&:expire!)
22
26
  me.add_reset_code(transport: "email")
23
27
  session.update(customer: me)
@@ -25,6 +29,7 @@ class Webhookdb::API::Install < Webhookdb::API::V1
25
29
  "messages/web/install-customer-login.liquid",
26
30
  serialize_view_params: true,
27
31
  vars: {
32
+ app_name: oauth_provider.app_name,
28
33
  view: "otp",
29
34
  action_url:,
30
35
  oauth_state: session.oauth_state,
@@ -50,47 +55,32 @@ class Webhookdb::API::Install < Webhookdb::API::V1
50
55
  end
51
56
  end
52
57
 
58
+ params do
59
+ requires :state, type: String
60
+ end
61
+ get :fake_oauth_authorization do
62
+ redirect "/v1/install/fake/callback?code=fakecode&state=#{params[:state]}"
63
+ end
64
+
53
65
  route_param :oauth_provider, type: String, values: Webhookdb::Oauth.registry.keys do
54
66
  helpers do
55
67
  def oauth_provider
56
- return @oauth_provider ||= Webhookdb::Oauth.provider(params[:oauth_provider])
57
- end
58
-
59
- def finish_org_setup(organization:, tokens:, scope:)
60
- organization.prepare_database_connections?
61
- oauth_provider.build_marketplace_integrations(organization:, tokens:, scope:)
62
- rendered = render_liquid(
63
- "messages/web/install-success.liquid",
64
- serialize_view_params: true,
65
- vars: {
66
- app_name: oauth_provider.app_name,
67
- database_url: organization.readonly_connection_url,
68
- supports_webhooks: oauth_provider.supports_webhooks?,
69
- },
70
- )
71
- status 200
72
- body rendered
68
+ @oauth_provider ||= Webhookdb::Oauth.provider(params[:oauth_provider])
69
+ rescue KeyError
70
+ forbidden!
73
71
  end
74
72
 
75
73
  def exchange_authorization_code(code)
76
74
  return oauth_provider.exchange_authorization_code(code:)
77
75
  rescue Webhookdb::Http::Error => e
78
76
  logger.warn "oauth_exchange_error", exception: e
77
+ url = "#{Webhookdb.api_url}/v1/install/#{oauth_provider.key}"
79
78
  raise FormError.new(
80
- "Something went wrong getting your access token from #{oauth_provider.app_name}. Please start over",
79
+ "Something went wrong getting your access token from #{oauth_provider.app_name}. " \
80
+ "Please start over by going to <a href=\"#{url}\">#{url}</a>.",
81
81
  400,
82
82
  )
83
83
  end
84
-
85
- def find_admin_membership(customer)
86
- _created, membership = Webhookdb::Customer.find_or_create_default_organization(customer)
87
- membership = customer.verified_memberships.find(&:admin?) unless membership.admin?
88
- return membership if membership
89
- raise FormError.new(
90
- "You must be an administrator of your WebhookDB organization to set up this app.",
91
- 403,
92
- )
93
- end
94
84
  end
95
85
 
96
86
  get do
@@ -120,24 +110,18 @@ class Webhookdb::API::Install < Webhookdb::API::V1
120
110
  get :callback do
121
111
  session = lookup_session!
122
112
  code = params[:code]
123
- if oauth_provider.requires_webhookdb_auth?
124
- session.update(authorization_code: code)
125
- redirect "/v1/install/#{oauth_provider.key}/login?state=#{session.oauth_state}"
126
- else
127
- scope = {}
128
- # Order of operations here is:
129
- # - Exchange token. We need the token to create the customer so it comes first.
130
- # - Create the customer, find the membership, and create replicators.
131
- # - If this last step fails, the code becomes invalid, which is annoying,
132
- # but should be rare and not a big deal to start over.
133
- tokens = exchange_authorization_code(code)
134
- session.db.transaction do
135
- _created, customer = oauth_provider.find_or_create_customer(tokens:, scope:)
136
- membership = find_admin_membership(customer)
137
- finish_org_setup(organization: membership.organization, tokens:, scope:)
138
- session.update(customer:, used_at: Time.now, authorization_code: code)
139
- end
140
- end
113
+ # Exchange the token now, in case it's invalid we don't want to find out at the end.
114
+ tokens = exchange_authorization_code(code)
115
+ session.update(token_json: tokens.as_json)
116
+ # Send the user to auth. We could (and did) use a "/me" endpoint here
117
+ # to get an email, but that pushes the trust someone is who they say they are
118
+ # to the oauth provider. We don't feel comfortable doing that in all cases,
119
+ # so we ask them to auth with WebhookDB.
120
+ #
121
+ # On top of that, many setups don't have a way to know who did the connection,
122
+ # nor can we be sure what org to install the replicators into,
123
+ # so may as well put everyone on the same path.
124
+ redirect "/v1/install/#{oauth_provider.key}/login?state=#{session.oauth_state}"
141
125
  end
142
126
 
143
127
  params do
@@ -149,6 +133,7 @@ class Webhookdb::API::Install < Webhookdb::API::V1
149
133
  "messages/web/install-customer-login.liquid",
150
134
  serialize_view_params: true,
151
135
  vars: {
136
+ app_name: oauth_provider.app_name,
152
137
  view: "email",
153
138
  action_url: "/v1/install/#{oauth_provider.key}/login",
154
139
  oauth_state: session.oauth_state,
@@ -167,23 +152,117 @@ class Webhookdb::API::Install < Webhookdb::API::V1
167
152
  session = lookup_session!
168
153
  email = params[:email]
169
154
  raise FormError.new("Email is required", 400) unless email.present?
170
- otp_token = params[:otp_token]
171
- if otp_token
155
+ if (otp_token = params[:otp_token]).nil?
156
+ # This is the first submit, asking for email. Prompt them for an OTP.
157
+ handle_login(email:, session:, action_url: "/v1/install/#{oauth_provider.key}/login")
158
+ else
159
+ # This is the 'second' submit, asking for OTP.
160
+ # Verify it, ensure the customer has a default org, and then send them to the 'org chooser'.
172
161
  # Order of operations here is:
173
162
  # - Verify the OTP
174
163
  # - Make sure we can find a valid admin membership
175
164
  # - Only then do we exchange the token.
176
165
  # - Setup replicators.
177
- customer = find_and_verify_user(email:, otp_token:)
178
- membership = find_admin_membership(customer)
179
- tokens = exchange_authorization_code(session.authorization_code)
180
166
  session.db.transaction do
181
- finish_org_setup(organization: membership.organization, tokens:, scope: {})
182
- session.update(used_at: Time.now)
167
+ customer = find_and_verify_user(email:, otp_token:)
168
+ Webhookdb::Customer.find_or_create_default_organization(customer)
169
+ session.update(customer:)
183
170
  end
171
+ redirect "/v1/install/#{oauth_provider.key}/org?state=#{session.oauth_state}"
172
+ end
173
+ end
174
+
175
+ params do
176
+ requires :state, type: String
177
+ end
178
+ get :org do
179
+ session = lookup_session!
180
+ organizations = session.customer.verified_memberships.select(&:admin?).map do |m|
181
+ {name: m.organization.name, key: m.organization.key, checked: m.default? ? "true" : ""}
182
+ end
183
+ rendered = render_liquid(
184
+ "messages/web/install-org-chooser.liquid",
185
+ serialize_view_params: true,
186
+ vars: {
187
+ app_name: oauth_provider.app_name,
188
+ action_url: "/v1/install/#{oauth_provider.key}/org",
189
+ oauth_state: session.oauth_state,
190
+ organizations:,
191
+ },
192
+ )
193
+ status 200
194
+ body rendered
195
+ end
196
+
197
+ params do
198
+ requires :state, type: String, desc: "the user session info string that we provided to Front"
199
+ optional :existing_org_key, type: String
200
+ optional :new_org_name, type: String
201
+ end
202
+ post :org do
203
+ session = lookup_session!
204
+ if (key = params[:existing_org_key]).present?
205
+ membership = session.customer.verified_memberships_dataset.
206
+ admin.
207
+ where(organization: Webhookdb::Organization.where(key:)).
208
+ first
209
+ raise FormError.new("You are not an administrator of that org or it does not exist.", 400) if
210
+ membership.nil?
211
+ elsif (name = params[:new_org_name])
212
+ org = Webhookdb::Organization.create_if_unique(name:)
213
+ raise FormError.new("Sorry, an organization with that name already exists.", 400) if
214
+ org.nil?
215
+ membership = session.customer.add_membership(
216
+ organization: org,
217
+ membership_role: Webhookdb::Role.admin_role,
218
+ verified: true,
219
+ )
184
220
  else
185
- handle_login(email:, session:, action_url: "/v1/install/#{oauth_provider.key}/login")
221
+ raise FormError.new("Existing organization key or a new organization name are required", 400)
186
222
  end
223
+
224
+ tokens = Webhookdb::Oauth::Tokens.new(**session.token_json)
225
+ session.db.transaction do
226
+ session.customer.replace_default_membership(membership)
227
+ membership.organization.prepare_database_connections?
228
+ oauth_provider.build_marketplace_integrations(organization: membership.organization, tokens:)
229
+ session.update(organization: membership.organization, token_json: nil)
230
+ redirect "/v1/install/#{oauth_provider.key}/success?state=#{params[:state]}"
231
+ end
232
+ end
233
+
234
+ params do
235
+ requires :state, type: String
236
+ end
237
+ get :success do
238
+ session = lookup_session!
239
+ # Mark the session used on GET, since we use the state to look up the session.
240
+ # It does mean that refreshing the page will error, though.
241
+ session.update(used_at: Time.now)
242
+ rendered = render_liquid(
243
+ "messages/web/install-success.liquid",
244
+ serialize_view_params: true,
245
+ vars: {
246
+ app_name: oauth_provider.app_name,
247
+ database_url: session.organization.readonly_connection_url,
248
+ supports_webhooks: oauth_provider.supports_webhooks?,
249
+ },
250
+ )
251
+ status 200
252
+ body rendered
253
+ end
254
+
255
+ get :forbidden do
256
+ rendered = render_liquid(
257
+ "messages/web/install-forbidden.liquid",
258
+ vars: {
259
+ app_name: oauth_provider.app_name,
260
+ terminal_url: "#{Webhookdb.api_url}/terminal",
261
+ install_url: "#{Webhookdb.api_url}/v1/install/#{oauth_provider.key}",
262
+ },
263
+ )
264
+ status 403
265
+ body rendered
187
266
  end
188
267
  end
189
268
 
@@ -194,12 +273,7 @@ class Webhookdb::API::Install < Webhookdb::API::V1
194
273
  whresp = Webhookdb::Front.initial_verification_request_response(request, Webhookdb::Front.app_secret)
195
274
  s_status, s_headers, s_body = whresp.to_rack
196
275
  s_headers.each { |k, v| header k, v }
197
- if s_headers["Content-Type"] == "application/json"
198
- body Oj.load(s_body)
199
- else
200
- env["api.format"] = :binary
201
- body s_body
202
- end
276
+ body Oj.load(s_body)
203
277
  status s_status
204
278
  break
205
279
  end
@@ -258,15 +332,52 @@ class Webhookdb::API::Install < Webhookdb::API::V1
258
332
  end
259
333
  end
260
334
 
335
+ resource :increase do
336
+ params do
337
+ requires :id, type: String
338
+ requires :created_at, type: Time
339
+ requires :category, type: String
340
+ requires :associated_object_type, type: String
341
+ requires :associated_object_id, type: String
342
+ requires :type, type: String
343
+ end
344
+ post :webhook do
345
+ group_id = env["HTTP_INCREASE_GROUP_ID"]
346
+ handle_webhook_request("increase-group-#{group_id || '?'}") do
347
+ if group_id.nil?
348
+ # No group ID is one of our own events.
349
+ # Run the job to handle it as a platform event (usually this is the oauth disconnect)
350
+ Amigo.publish("increase.#{params[:category]}", declared(params).as_json)
351
+ status 202
352
+ present({message: "ok"})
353
+ next :pass
354
+ end
355
+ root_sint = Webhookdb::ServiceIntegration[service_name: "increase_app_v1", api_url: group_id]
356
+ if root_sint.nil?
357
+ logger.error "increase_unregistered_group", increase_group_id: group_id
358
+ status 202
359
+ present({message: "unregistered group"})
360
+ next :pass
361
+ end
362
+ next root_sint
363
+ end
364
+ end
365
+ end
366
+
261
367
  resource :intercom do
368
+ helpers do
369
+ def find_root(app_id)
370
+ return Webhookdb::ServiceIntegration[service_name: "intercom_marketplace_root_v1", api_url: app_id]
371
+ end
372
+ end
262
373
  post :webhook do
263
374
  # Because the `_webhook_response` function is always the same here, I'm wondering if it's even
264
375
  # advisable to do the integration lookup before performing a webhook verification when we don't
265
376
  # need that info. Something to consider upon refactor
266
-
267
- handle_webhook_request("intercom_marketplace_appid-#{params[:app_id] || '?'}") do
268
- app_id = params[:app_id]
269
- root_sint = Webhookdb::ServiceIntegration[service_name: "intercom_marketplace_root_v1", api_url: app_id]
377
+ app_id = params[:app_id]
378
+ root_sint = find_root(app_id)
379
+ opaque_id = root_sint&.opaque_id || "intercom_marketplace_appid-#{app_id}"
380
+ handle_webhook_request(opaque_id) do
270
381
  if root_sint.nil?
271
382
  logger.warn "intercom_webhook_unregistered_app", intercom_app_id: app_id
272
383
  status 200
@@ -275,6 +386,8 @@ class Webhookdb::API::Install < Webhookdb::API::V1
275
386
  end
276
387
  # Notification topics are formatted like "{model}.{thing that happened}" (e.g. "contact.created")
277
388
  # to get the model type of the notification, for our purposes we can just grab that first chunk
389
+ # This should probably move to the marketplace replicator itself,
390
+ # rather than being done in the endpoint (see /v1/install/increase/webhook).
278
391
  type = params[:topic].split(".")[0]
279
392
  handling_type = "intercom_#{type}_v1"
280
393
  unless (handling_sint = root_sint.recursive_dependents.find { |d| d.service_name == handling_type })
@@ -291,27 +404,39 @@ class Webhookdb::API::Install < Webhookdb::API::V1
291
404
  requires :app_id
292
405
  end
293
406
  post :uninstall do
294
- # TODO: Verify the headers are valid
295
- # We want to delete all the integrations associated with the app_id.
296
- root_sint = Webhookdb::ServiceIntegration[service_name: "intercom_marketplace_root_v1",
297
- api_url: params["app_id"]]
298
- root_sint.destroy_self_and_all_dependents
299
- status 200
300
- present({o: "k"})
407
+ app_id = params[:app_id]
408
+ root_sint = find_root(app_id)
409
+ # Intercom uses X-Body-Signature rather than X-Hub-Signature here,
410
+ # unlike the normal /webhook request.
411
+ # I've asked Intercom if they can support X-Hub-Signature here as well.
412
+ # If they cannot, we need to add support for the alternative signature validation.
413
+ opaque_id = root_sint&.opaque_id || "intercom_marketplace_appid-#{app_id}"
414
+ handle_webhook_request(opaque_id) do
415
+ root_sint&.destroy_self_and_all_dependents
416
+ status 200
417
+ present({o: "k"})
418
+ next :pass
419
+ end
301
420
  end
302
421
 
303
422
  params do
423
+ # This endpoint recieves a value called "workspace_id" but it is
424
+ # identical to the "app_id" value we get from the `/me` endpoint.
425
+ # It just has a different name here for some reason.
304
426
  requires :workspace_id
305
427
  end
306
428
  post :health do
307
- # TODO: Verify the headers are valid
308
- # For now we are just returning "OK" per the specification:
309
429
  # https://developers.intercom.com/docs/build-an-integration/learn-more/installation-health-check
310
- # An interesting point is that this endpoint recieves a value called "workspace_id" but it is
311
- # identical to the "app_id" value we get from the `/me` endpoint. It just has a different name here, for
312
- # some reason.
430
+ result = {}
431
+ if find_root(params[:workspace_id]).nil?
432
+ result[:state] = "UNHEALTHY"
433
+ result[:cta_type] = "REINSTALL_CTA"
434
+ result[:message] = "You need to reinstall this app to sync your data to WebhookDB."
435
+ else
436
+ result[:state] = "OK"
437
+ end
313
438
  status 200
314
- present({state: "OK"})
439
+ present result
315
440
  end
316
441
  end
317
442
  end
@@ -54,7 +54,7 @@ class Webhookdb::API::Organizations < Webhookdb::API::V1
54
54
  get do
55
55
  _customer = current_customer
56
56
  org = lookup_org!
57
- fake_entities = org.available_replicator_names.sort.map { |name| {name:} }
57
+ fake_entities = org.available_replicators.map(&:name).sort.map { |name| {name:} }
58
58
  message = "Run `webhookdb integrations create [service name]` to start replicating data to your database."
59
59
  present_collection fake_entities, with: Webhookdb::API::ServiceEntity, message:
60
60
  end
@@ -62,11 +62,11 @@ class Webhookdb::API::Organizations < Webhookdb::API::V1
62
62
 
63
63
  desc "Generates an invitation code for a user, adds pending membership in the organization."
64
64
  params do
65
- optional :email, type: String, coerce_with: NormalizedEmail,
65
+ optional :email, type: NormalizedEmail,
66
66
  prompt: "Enter the email to send the invitation to:"
67
67
  optional :role_name,
68
- type: String,
69
- values: Webhookdb::OrganizationMembership::VALID_ROLE_NAMES,
68
+ type: TrimmedString,
69
+ values: TrimmedString.map(Webhookdb::OrganizationMembership::VALID_ROLE_NAMES),
70
70
  default: "member"
71
71
  end
72
72
  post :invite do
@@ -102,7 +102,7 @@ class Webhookdb::API::Organizations < Webhookdb::API::V1
102
102
 
103
103
  desc "Allows organization admin to remove customer from an organization"
104
104
  params do
105
- optional :email, type: String, coerce_with: NormalizedEmail,
105
+ optional :email, type: NormalizedEmail,
106
106
  prompt: "Enter the email of the member you are removing permissions from:"
107
107
  optional :guard_confirm
108
108
  end
@@ -118,7 +118,7 @@ class Webhookdb::API::Organizations < Webhookdb::API::V1
118
118
  to_delete.delete
119
119
  roll_back_if_no_admins!(org)
120
120
  status 200
121
- present({}, with: Webhookdb::AdminAPI::BaseEntity,
121
+ present({}, with: Webhookdb::API::BaseEntity,
122
122
  message: "#{email} is no longer a part of #{org.name}.",)
123
123
  end
124
124
  end
@@ -149,9 +149,11 @@ class Webhookdb::API::Organizations < Webhookdb::API::V1
149
149
  params do
150
150
  optional :emails, type: [String], coerce_with: CommaSepArray,
151
151
  prompt: "Enter the emails to modify the roles of as a comma-separated list:"
152
- optional :role_name, type: String, values: Webhookdb::OrganizationMembership::VALID_ROLE_NAMES,
153
- prompt: "Enter the name of the role to assign " \
154
- "(#{Webhookdb::OrganizationMembership::VALID_ROLE_NAMES.join(', ')}): "
152
+ optional :role_name,
153
+ type: TrimmedString,
154
+ values: TrimmedString.map(Webhookdb::OrganizationMembership::VALID_ROLE_NAMES),
155
+ prompt: "Enter the name of the role to assign " \
156
+ "(#{Webhookdb::OrganizationMembership::VALID_ROLE_NAMES.join(', ')}): "
155
157
  optional :guard_confirm
156
158
  end
157
159
  post :change_roles do
@@ -173,7 +175,7 @@ class Webhookdb::API::Organizations < Webhookdb::API::V1
173
175
 
174
176
  desc "Allow organization admin to change the name of the organization"
175
177
  params do
176
- optional :name, type: String, prompt: "Enter the new organization name:"
178
+ optional :name, type: TrimmedString, prompt: "Enter the new organization name:"
177
179
  end
178
180
  post :rename do
179
181
  customer = current_customer
@@ -214,7 +216,7 @@ class Webhookdb::API::Organizations < Webhookdb::API::V1
214
216
 
215
217
  desc "Creates a new organization and adds current customer as a member."
216
218
  params do
217
- optional :name, type: String, prompt: "Enter the name of the organization:"
219
+ optional :name, type: TrimmedString, prompt: "Enter the name of the organization:"
218
220
  end
219
221
  post :create do
220
222
  customer = current_customer
@@ -234,7 +236,7 @@ class Webhookdb::API::Organizations < Webhookdb::API::V1
234
236
 
235
237
  desc "Allows user to verify membership in an organization with an invitation code."
236
238
  params do
237
- optional :invitation_code, type: String, prompt: "Enter the invitation code:"
239
+ optional :invitation_code, type: TrimmedString, prompt: "Enter the invitation code:"
238
240
  end
239
241
  post :join do
240
242
  customer = current_customer
@@ -4,6 +4,8 @@ require "webhookdb/api"
4
4
  require "webhookdb/saved_query"
5
5
 
6
6
  class Webhookdb::API::SavedQueries < Webhookdb::API::V1
7
+ include Webhookdb::Service::Types
8
+
7
9
  resource :organizations do
8
10
  route_param :org_identifier do
9
11
  resource :saved_queries do
@@ -99,8 +101,10 @@ class Webhookdb::API::SavedQueries < Webhookdb::API::V1
99
101
 
100
102
  desc "Updates the field on a custom query."
101
103
  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 :field,
105
+ type: TrimmedString,
106
+ prompt: "What field would you like to update (one of: " \
107
+ "#{Webhookdb::SavedQuery::CLI_EDITABLE_FIELDS.join(', ')}): "
104
108
  optional :value, type: String, prompt: "What is the new value? "
105
109
  end
106
110
  post :update do
@@ -147,7 +151,9 @@ class Webhookdb::API::SavedQueries < Webhookdb::API::V1
147
151
  end
148
152
 
149
153
  params do
150
- optional :field, type: String, values: Webhookdb::SavedQuery::INFO_FIELDS.keys + [""]
154
+ optional :field,
155
+ type: TrimmedString,
156
+ values: TrimmedString.map(Webhookdb::SavedQuery::INFO_FIELDS.keys + [""])
151
157
  end
152
158
  post :info do
153
159
  cq = lookup!
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/api"
4
+
5
+ class Webhookdb::API::SavedViews < Webhookdb::API::V1
6
+ include Webhookdb::Service::Types
7
+
8
+ resource :organizations do
9
+ route_param :org_identifier do
10
+ resource :saved_views do
11
+ helpers do
12
+ def lookup_view!
13
+ org = lookup_org!
14
+ cq = org.saved_views_dataset[name: params[:name].strip]
15
+ merror!(403, "There is no view with that name.") if cq.nil?
16
+ return cq
17
+ end
18
+
19
+ def guard_editable!(customer, org)
20
+ return if has_admin?(org, customer:)
21
+ permission_error!("You must be an org admin to modify views.")
22
+ end
23
+ end
24
+
25
+ desc "Returns a list of all saved views associated with the org."
26
+ get do
27
+ views = lookup_org!.saved_views
28
+ message = ""
29
+ if views.empty?
30
+ message = "This organization doesn't have any saved views yet.\n" \
31
+ "Use `webhookdb saved-view create` to set one up."
32
+ end
33
+ present_collection views, with: SavedViewEntity, message:
34
+ end
35
+
36
+ desc "Creates or replaces the view with the given name."
37
+ params do
38
+ optional :name,
39
+ type: TrimmedString,
40
+ prompt: "Enter the view name (alphanumeric, spaces, underscores):"
41
+ optional :sql, type: String, prompt: "Enter the SQL you would like to run:"
42
+ end
43
+ post :create_or_replace do
44
+ cust = current_customer
45
+ org = lookup_org!
46
+ check_feature_access!(org, Webhookdb::SavedView.feature_role)
47
+ guard_editable!(cust, org)
48
+ begin
49
+ sv = Webhookdb::SavedView.create_or_replace(
50
+ organization: org,
51
+ sql: params[:sql],
52
+ name: params[:name].strip,
53
+ created_by: cust,
54
+ )
55
+ rescue Webhookdb::SavedView::InvalidQuery => e
56
+ Webhookdb::API::Helpers.prompt_for_required_param!(
57
+ request,
58
+ :sql,
59
+ "Enter a new query:",
60
+ output: "That query was invalid. #{e.message}\n" \
61
+ "You can iterate on your query by connecting to your database from any SQL editor.\n" \
62
+ "Use `webhookdb db connection` to get your query string.",
63
+ )
64
+ rescue Webhookdb::DBAdapter::InvalidIdentifier => e
65
+ Webhookdb::API::Helpers.prompt_for_required_param!(
66
+ request,
67
+ :name,
68
+ "Enter a new name:",
69
+ output: e.message,
70
+ )
71
+ end
72
+ message = "You have created or replaced the view with the name '#{sv.name}'. " \
73
+ "You can now use it in any query with your database connection string. " \
74
+ "Run `webhookdb db connection` to retrieve your connection string if you need it."
75
+ status 200
76
+ present sv, with: SavedViewEntity, message:
77
+ end
78
+
79
+ post :delete do
80
+ customer = current_customer
81
+ cq = lookup_view!
82
+ guard_editable!(customer, cq.organization)
83
+ cq.destroy
84
+ status 200
85
+ present cq, with: SavedViewEntity,
86
+ message: "You have successfully deleted the saved view '#{cq.name}'."
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ class SavedViewEntity < Webhookdb::API::BaseEntity
93
+ expose :name
94
+
95
+ def self.display_headers
96
+ return [[:name, "Name"]]
97
+ end
98
+ end
99
+ end