From 5ffcee1dde8fb92f82821e71f46c9b89c8c90455 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 3 Jun 2019 17:09:24 +1000 Subject: [PATCH] Add custom wizard integration --- .../components/wizard-custom-action.js.es6 | 21 ++++++++- .../controllers/admin-wizards-api.js.es6 | 1 - .../discourse/routes/admin-wizard.js.es6 | 8 +++- .../components/wizard-custom-action.hbs | 42 +++++++++++++++++- assets/stylesheets/wizard_custom_admin.scss | 17 +++++++ config/locales/client.en.yml | 11 ++++- controllers/api.rb | 4 +- lib/api/authorization.rb | 26 +++++------ lib/api/endpoint.rb | 44 ++++++++++++++++--- lib/builder.rb | 11 +++++ plugin.rb | 1 + serializers/api/api_serializer.rb | 2 +- serializers/api/basic_api_serializer.rb | 13 +++++- serializers/api/basic_endpoint_serializer.rb | 4 ++ 14 files changed, 175 insertions(+), 30 deletions(-) create mode 100644 serializers/api/basic_endpoint_serializer.rb diff --git a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 index 976501e5..75000d8b 100644 --- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 @@ -3,7 +3,8 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor const ACTION_TYPES = [ { id: 'create_topic', name: 'Create Topic' }, { id: 'update_profile', name: 'Update Profile' }, - { id: 'send_message', name: 'Send Message' } + { id: 'send_message', name: 'Send Message' }, + { id: 'send_to_api', name: 'Send to API' } ]; const PROFILE_FIELDS = [ @@ -26,6 +27,8 @@ export default Ember.Component.extend({ createTopic: Ember.computed.equal('action.type', 'create_topic'), updateProfile: Ember.computed.equal('action.type', 'update_profile'), sendMessage: Ember.computed.equal('action.type', 'send_message'), + sendToApi: Ember.computed.equal('action.type', 'send_to_api'), + apiEmpty: Ember.computed.empty('action.api'), disableId: Ember.computed.not('action.isNew'), @computed('currentStepId', 'wizard.save_submissions') @@ -75,5 +78,21 @@ export default Ember.Component.extend({ toggleCustomCategoryWizardField() { const user = this.get('action.custom_category_user_field'); if (user) this.set('action.custom_category_wizard_field', false); + }, + + @computed('wizard.apis') + availableApis(apis) { + return apis.map(a => { + return { + id: a.name, + name: a.title + }; + }); + }, + + @computed('wizard.apis', 'action.api') + availableEndpoints(apis, api) { + if (!api) return []; + return apis.find(a => a.name === api).endpoints; } }); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 926aa751..7306012e 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -78,7 +78,6 @@ export default Ember.Controller.extend({ if (api.title) data['title'] = api.title; const originalTitle = this.get('api.originalTitle'); - console.log(api, originalTitle); if (api.get('isNew') || (originalTitle && (api.title !== originalTitle))) { refreshList = true; } diff --git a/assets/javascripts/discourse/routes/admin-wizard.js.es6 b/assets/javascripts/discourse/routes/admin-wizard.js.es6 index aeba38b0..a10f625e 100644 --- a/assets/javascripts/discourse/routes/admin-wizard.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizard.js.es6 @@ -33,7 +33,8 @@ export default Discourse.Route.extend({ afterModel(model) { return Ember.RSVP.all([ this._getFieldTypes(model), - this._getThemes(model) + this._getThemes(model), + this._getApis(model) ]); }, @@ -48,6 +49,11 @@ export default Discourse.Route.extend({ }); }, + _getApis(model) { + return ajax('/admin/wizards/apis') + .then((result) => model.set('apis', result)); + }, + setupController(controller, model) { const newWizard = this.get('newWizard'); const steps = model.get('steps') || []; diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs index 7a7f3cfc..95384f64 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs @@ -96,7 +96,7 @@ {{d-editor value=action.post_template - placeholder='admin.wizard.action.post_builder.placeholder' + placeholder='admin.wizard.action.interpolate_fields' classNames='post-builder-editor'}} @@ -157,7 +157,7 @@ {{d-editor value=action.post_template - placeholder='admin.wizard.action.post_builder.placeholder' + placeholder='admin.wizard.action.interpolate_fields' classNames='post-builder-editor'}} @@ -204,3 +204,41 @@ allowUserField=true}} {{/if}} + +{{#if sendToApi}} +
+
+

{{i18n "admin.wizard.action.send_to_api.api"}}

+
+
+ {{combo-box value=action.api + content=availableApis + none='admin.wizard.action.send_to_api.select_an_api' + isDisabled=action.custom_title_enabled}} +
+
+ +
+
+

{{i18n "admin.wizard.action.send_to_api.endpoint"}}

+
+
+ {{combo-box value=action.api_endpoint + content=availableEndpoints + none='admin.wizard.action.send_to_api.select_an_endpoint' + isDisabled=apiEmpty}} +
+
+ +
+
+

{{i18n "admin.wizard.action.send_to_api.body"}}

+
+
+ + + {{textarea value=action.api_body + placeholder=(i18n 'admin.wizard.action.interpolate_fields')}} +
+
+{{/if}} diff --git a/assets/stylesheets/wizard_custom_admin.scss b/assets/stylesheets/wizard_custom_admin.scss index f97a4500..2ebbb253 100644 --- a/assets/stylesheets/wizard_custom_admin.scss +++ b/assets/stylesheets/wizard_custom_admin.scss @@ -117,6 +117,23 @@ margin-top: 5px; padding: 5px; } + + .api-body { + width: 100%; + + .setting-label { + max-width: 70px; + } + + .setting-value { + width: calc(100% - 180px); + } + + textarea { + width: 100%; + min-height: 150px; + } + } } .wizard-links { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 700f50ca..e3388f57 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -103,6 +103,8 @@ en: add_fields: "{{type}} Fields" available_fields: "* If 'Save wizard submissions' is disabled, only the fields of the current step are available to the current step's actions." topic_attr: "Topic Attribute" + interpolate_fields: "Insert wizard fields using the field_id in w{}. Insert user fields using field key in u{}." + skip_redirect: label: "Skip Redirect" description: "Don't redirect the user to this {{type}} after the wizard completes" @@ -120,12 +122,19 @@ en: label: "Builder" user_fields: "User Fields: " wizard_fields: "Wizard Fields: " - placeholder: "Insert wizard fields using the field_id in w{}. Insert user fields using field key in u{}." custom_title: "Custom Title" custom_category: label: "Custom Category" wizard_field: "Wizard Field" user_field: "User Field" + send_to_api: + label: "Send to API" + api: "API" + endpoint: "Endpoint" + select_an_api: "Select an API" + select_an_endpoint: "Select an endpoint" + body: "Request body JSON" + api: label: "API" nav_label: 'APIs' diff --git a/controllers/api.rb b/controllers/api.rb index 86fc6841..524f4e84 100644 --- a/controllers/api.rb +++ b/controllers/api.rb @@ -106,10 +106,10 @@ class CustomWizard::ApiController < ::ApplicationController :client_secret, :username, :password, - :params + :auth_params ) - auth_data[:params] = JSON.parse(auth_data[:params]) if auth_data[:params].present? + auth_data[:auth_params] = JSON.parse(auth_data[:auth_params]) if auth_data[:auth_params].present? @auth_data ||= auth_data end diff --git a/lib/api/authorization.rb b/lib/api/authorization.rb index ac625475..7f702f6b 100644 --- a/lib/api/authorization.rb +++ b/lib/api/authorization.rb @@ -32,6 +32,8 @@ class CustomWizard::Api::Authorization end def self.set(api_name, new_data = {}) + api_name = api_name.underscore + data = self.get(api_name, data_only: true) || {} new_data.each do |k, v| @@ -44,6 +46,8 @@ class CustomWizard::Api::Authorization end def self.get(api_name, opts = {}) + api_name = api_name.underscore + if data = PluginStore.get("custom_wizard_api_#{api_name}", 'authorization') if opts[:data_only] data @@ -60,22 +64,16 @@ class CustomWizard::Api::Authorization end def self.get_header_authorization_string(name) - protocol = authentication_protocol(name) - raise Discourse::InvalidParameters.new(:name) unless protocol.present? - raise Discourse::InvalidParameters.new(:protocol) unless [BASIC_AUTH, OAUTH2_AUTH].include? protocol + auth = CustomWizard::Api::Authorization.get(name) + raise Discourse::InvalidParameters.new(:name) unless auth.present? - if protocol = BASIC_AUTH - username = username(name) - raise Discourse::InvalidParameters.new(:username) unless username.present? - password = password(name) - raise Discourse::InvalidParameters.new(:password) unless password.present? - authorization_string = (username + ":" + password).chomp - "Basic #{Base64.strict_encode64(authorization_string)}" + if auth.auth_type === "basic" + raise Discourse::InvalidParameters.new(:username) unless auth.username.present? + raise Discourse::InvalidParameters.new(:password) unless auth.password.present? + "Basic #{Base64.strict_encode64((auth.username + ":" + auth.password).chomp)}" else - # must be OAUTH2 - access_token = access_token(name) - raise Discourse::InvalidParameters.new(access_token) unless access_token.present? - "Bearer #{access_token}" + raise Discourse::InvalidParameters.new(auth.access_token) unless auth.access_token.present? + "Bearer #{auth.access_token}" end end diff --git a/lib/api/endpoint.rb b/lib/api/endpoint.rb index 1d14d6ba..3f475301 100644 --- a/lib/api/endpoint.rb +++ b/lib/api/endpoint.rb @@ -16,8 +16,13 @@ class CustomWizard::Api::Endpoint end def self.set(api_name, new_data) - data = new_data[:endpoint_id] ? self.get(api_name, new_data[:endpoint_id], data_only: true) : {} - endpoint_id = new_data[:endpoint_id] || SecureRandom.hex(3) + if new_data['id'] + data = self.get(api_name, new_data['id'], data_only: true) + endpoint_id = new_data['id'] + else + data = {} + endpoint_id = SecureRandom.hex(3) + end new_data.each do |k, v| data[k.to_sym] = v @@ -32,11 +37,10 @@ class CustomWizard::Api::Endpoint return nil if !endpoint_id if data = PluginStore.get("custom_wizard_api_#{api_name}", "endpoint_#{endpoint_id}") - data[:id] = endpoint_id - if opts[:data_only] data else + data[:id] = endpoint_id self.new(api_name, data) end else @@ -48,8 +52,8 @@ class CustomWizard::Api::Endpoint PluginStoreRow.where("plugin_name = 'custom_wizard_api_#{api_name}' AND key LIKE 'endpoint_%'").destroy_all end - def self.list - PluginStoreRow.where("plugin_name LIKE 'custom_wizard_api_%' AND key LIKE 'endpoint_%'") + def self.list(api_name) + PluginStoreRow.where("plugin_name LIKE 'custom_wizard_api_#{api_name}' AND key LIKE 'endpoint_%'") .map do |record| api_name = record['plugin_name'].sub("custom_wizard_api_", "") data = ::JSON.parse(record['value']) @@ -57,4 +61,32 @@ class CustomWizard::Api::Endpoint self.new(api_name, data) end end + + def self.request(api_name, endpoint_id, body) + endpoint = self.get(api_name, endpoint_id) + auth = CustomWizard::Api::Authorization.get_header_authorization_string(api_name) + + connection = Excon.new( + URI.parse(URI.encode(endpoint.url)).to_s, + :headers => { + "Authorization" => auth, + "Accept" => "application/json, */*", + "Content-Type" => "application/json" + } + ) + + params = { + method: endpoint.method + } + + if body + body = JSON.generate(body) + body.delete! '\\' + params[:body] = body + end + + response = connection.request(params) + + JSON.parse(response.body) + end end diff --git a/lib/builder.rb b/lib/builder.rb index ad4ef53a..bc873261 100644 --- a/lib/builder.rb +++ b/lib/builder.rb @@ -393,6 +393,17 @@ class CustomWizard::Builder end end + def send_to_api(user, action, data) + api_body = CustomWizard::Builder.fill_placeholders(JSON.generate(JSON.parse(action['api_body'])), user, data) + result = CustomWizard::Api::Endpoint.request(action['api'], action['api_endpoint'], api_body) + + if result['error'] + updater.errors.add(:send_message, result['error']) + else + ## add validation callback + end + end + def save_submissions(data, final_step) if final_step data['submitted_at'] = Time.now.iso8601 diff --git a/plugin.rb b/plugin.rb index f0d7cfc6..da45683d 100644 --- a/plugin.rb +++ b/plugin.rb @@ -97,6 +97,7 @@ after_initialize do load File.expand_path('../serializers/api/authorization_serializer.rb', __FILE__) load File.expand_path('../serializers/api/basic_api_serializer.rb', __FILE__) load File.expand_path('../serializers/api/endpoint_serializer.rb', __FILE__) + load File.expand_path('../serializers/api/basic_endpoint_serializer.rb', __FILE__) ::UsersController.class_eval do def wizard_path diff --git a/serializers/api/api_serializer.rb b/serializers/api/api_serializer.rb index 0856b461..651e29fd 100644 --- a/serializers/api/api_serializer.rb +++ b/serializers/api/api_serializer.rb @@ -14,7 +14,7 @@ class CustomWizard::ApiSerializer < ApplicationSerializer end def endpoints - if endpoints = CustomWizard::Api::Endpoint.list + if endpoints = CustomWizard::Api::Endpoint.list(object.name) ActiveModel::ArraySerializer.new( endpoints, each_serializer: CustomWizard::Api::EndpointSerializer diff --git a/serializers/api/basic_api_serializer.rb b/serializers/api/basic_api_serializer.rb index 6f583f7d..d0214d65 100644 --- a/serializers/api/basic_api_serializer.rb +++ b/serializers/api/basic_api_serializer.rb @@ -1,3 +1,14 @@ class CustomWizard::BasicApiSerializer < ApplicationSerializer - attributes :name, :title + attributes :name, + :title, + :endpoints + + def endpoints + if endpoints = CustomWizard::Api::Endpoint.list(object.name) + ActiveModel::ArraySerializer.new( + endpoints, + each_serializer: CustomWizard::Api::BasicEndpointSerializer + ) + end + end end diff --git a/serializers/api/basic_endpoint_serializer.rb b/serializers/api/basic_endpoint_serializer.rb new file mode 100644 index 00000000..e2cc2262 --- /dev/null +++ b/serializers/api/basic_endpoint_serializer.rb @@ -0,0 +1,4 @@ +class CustomWizard::Api::BasicEndpointSerializer < ApplicationSerializer + attributes :id, + :name +end