webhookdb 1.2.2 → 1.3.1

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 (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