From 221c233e6d8c9da12b3b9c6356be54a21b01ec25 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Sat, 11 May 2019 16:53:37 +0100 Subject: [PATCH 01/31] first commit --- jobs/refresh_api_access_token.rb | 7 +++ lib/authorization.rb | 91 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 jobs/refresh_api_access_token.rb create mode 100644 lib/authorization.rb diff --git a/jobs/refresh_api_access_token.rb b/jobs/refresh_api_access_token.rb new file mode 100644 index 00000000..08503361 --- /dev/null +++ b/jobs/refresh_api_access_token.rb @@ -0,0 +1,7 @@ +module Jobs + class RefreshAPIAccessToken < Jobs::Base + def execute(args) + CustomWizard::Authorization.refresh_access_token + end + end +end diff --git a/lib/authorization.rb b/lib/authorization.rb new file mode 100644 index 00000000..e96df6f9 --- /dev/null +++ b/lib/authorization.rb @@ -0,0 +1,91 @@ +require 'excon' + +class CustomWizard::Authorization + def self.access_token + PluginStore.get('custom_wizard', 'access_token') || {} + end + + def self.set_access_token(data) + PluginStore.set('custom_wizard', 'access_token', data) + end + + def self.refresh_token + PluginStore.get('custom_wizard', 'refresh_token') + end + + def self.set_refresh_token(token) + PluginStore.set('custom_wizard', 'refresh_token', token) + end + + def self.code + PluginStore.get('custom_wizard', 'code') + end + + def self.set_code(code) + PluginStore.set('custom_wizard', 'code', code) + end + + def self.get_access_token + body = { + client_id: SiteSetting.custom_wizard_client_id, + client_secret: SiteSetting.custom_wizard_client_secret, + code: CustomWizard::Authorization.code, + grant_type: 'authorization_code', + redirect_uri: (Rails.env.development? ? CustomWizard::NGROK_URL : Discourse.base_url) + '/custom_wizard/authorization/callback' + } + + result = Excon.post( + "https://api.custom_wizard.com/token", + :headers => { + "Content-Type" => "application/x-www-form-urlencoded" + }, + :body => URI.encode_www_form(body) + ) + + self.handle_token_result(result) + end + + def self.refresh_access_token + body = { + grant_type: 'refresh_token', + refresh_token: CustomWizard::Authorization.refresh_token + } + + authorization_string = SiteSetting.custom_wizard_client_id + ':' + SiteSetting.custom_wizard_client_secret + + result = Excon.post( + "https://api.custom_wizard.com/token", + :headers => { + "Content-Type" => "application/x-www-form-urlencoded", + "Authorization" => "Basic #{Base64.strict_encode64(authorization_string)}" + }, + :body => URI.encode_www_form(body) + ) + + self.handle_token_result(result) + end + + def self.handle_token_result(result) + data = JSON.parse(result.body) + return false if (data['error']) + + token = data['access_token'] + expires_at = Time.now + data['expires_in'].seconds + refresh_at = expires_at.to_time - 2.hours + + Jobs.enqueue_at(refresh_at, :refresh_custom_wizard_access_token) + + CustomWizard::Authorization.set_access_token( + token: token, + expires_at: expires_at, + refresh_at: refresh_at + ) + + CustomWizard::Authorization.set_refresh_token(data['refresh_token']) + end + + def self.authorized + CustomWizard::Authorization.access_token[:token] && + CustomWizard::Authorization.access_token[:expires_at].to_datetime > Time.now + end +end From 839f085500f1c5ba7af11f59258bd7310394ef11 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Sun, 12 May 2019 15:08:59 +0100 Subject: [PATCH 02/31] add file reference to plugin dot rb --- plugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin.rb b/plugin.rb index 231bb7e1..c7268648 100644 --- a/plugin.rb +++ b/plugin.rb @@ -77,6 +77,7 @@ after_initialize do load File.expand_path('../lib/template.rb', __FILE__) load File.expand_path('../lib/wizard.rb', __FILE__) load File.expand_path('../lib/wizard_edits.rb', __FILE__) + load File.expand_path('../lib/authorization.rb', __FILE__) load File.expand_path('../controllers/wizard.rb', __FILE__) load File.expand_path('../controllers/steps.rb', __FILE__) load File.expand_path('../controllers/admin.rb', __FILE__) From 19a9497d74c9ca8ea1a983570bd43462f0b23b74 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Tue, 14 May 2019 14:54:33 +0100 Subject: [PATCH 03/31] added authentication protocol type --- lib/authorization.rb | 94 +++++++++++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 28 deletions(-) diff --git a/lib/authorization.rb b/lib/authorization.rb index e96df6f9..4a6e8642 100644 --- a/lib/authorization.rb +++ b/lib/authorization.rb @@ -1,60 +1,97 @@ require 'excon' class CustomWizard::Authorization - def self.access_token - PluginStore.get('custom_wizard', 'access_token') || {} + + BASIC_AUTH = 'basic_authentication' + OAUTH2_AUTH = 'OAuth2_authentication' + + def self.authentication_protocol(service) + PluginStore.get(service, 'authentication_protocol') || {} end - def self.set_access_token(data) - PluginStore.set('custom_wizard', 'access_token', data) + def self.set_authentication_protocol(service, protocol) + raise Discourse::InvalidParameters unless [BASIC_AUTH, OAUTH2_AUTH].include? protocol + PluginStore.set(service, 'authentication_protocol', protocol) end - def self.refresh_token - PluginStore.get('custom_wizard', 'refresh_token') + def self.access_token(service) + PluginStore.get(service, 'access_token') || {} end - def self.set_refresh_token(token) - PluginStore.set('custom_wizard', 'refresh_token', token) + def self.set_access_token(service, data) + PluginStore.set(service, 'access_token', data) end - def self.code - PluginStore.get('custom_wizard', 'code') + def self.refresh_token (service) + PluginStore.get(service, 'refresh_token') end - def self.set_code(code) - PluginStore.set('custom_wizard', 'code', code) + def self.set_refresh_token(service, token) + PluginStore.set(service, 'refresh_token', token) end - def self.get_access_token + def self.code(service) + PluginStore.get(service,'code') + end + + def self.set_code(service, code) + PluginStore.set(service, 'code', code) + end + + def self.client_id(service) + PluginStore.get(service,'client_id') + end + + def self.set_client_id(service, client_id) + PluginStore.set(service, 'client_id', client_id) + end + + def self.client_secret(service) + PluginStore.get(service,'client_secret') + end + + def self.set_client_secret(service, client_secret) + PluginStore.set(service, 'client_secret', client_secret) + end + + def self.url(service) + PluginStore.get(service,'url') + end + + def self.set_url(service, url) + PluginStore.set(service, 'url', url) + end + + def self.get_access_token(service) body = { - client_id: SiteSetting.custom_wizard_client_id, - client_secret: SiteSetting.custom_wizard_client_secret, - code: CustomWizard::Authorization.code, + client_id: CustomWizard::Authorization.client_id(service), + client_secret: CustomWizard::Authorization.client_secret(service), + code: CustomWizard::Authorization.code(service), grant_type: 'authorization_code', redirect_uri: (Rails.env.development? ? CustomWizard::NGROK_URL : Discourse.base_url) + '/custom_wizard/authorization/callback' } result = Excon.post( - "https://api.custom_wizard.com/token", + CustomWizard::Authorization.url(service), :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, :body => URI.encode_www_form(body) ) - self.handle_token_result(result) + self.handle_token_result(service, result) end - def self.refresh_access_token + def self.refresh_access_token(service) body = { grant_type: 'refresh_token', refresh_token: CustomWizard::Authorization.refresh_token } - authorization_string = SiteSetting.custom_wizard_client_id + ':' + SiteSetting.custom_wizard_client_secret + authorization_string = CustomWizard::Authorization.client_id(service) + ':' + CustomWizard::Authorization.client_secret(service) result = Excon.post( - "https://api.custom_wizard.com/token", + CustomWizard::Authorization.url(service), :headers => { "Content-Type" => "application/x-www-form-urlencoded", "Authorization" => "Basic #{Base64.strict_encode64(authorization_string)}" @@ -62,10 +99,10 @@ class CustomWizard::Authorization :body => URI.encode_www_form(body) ) - self.handle_token_result(result) + self.handle_token_result(service, result) end - def self.handle_token_result(result) + def self.handle_token_result(service, result) data = JSON.parse(result.body) return false if (data['error']) @@ -73,19 +110,20 @@ class CustomWizard::Authorization expires_at = Time.now + data['expires_in'].seconds refresh_at = expires_at.to_time - 2.hours - Jobs.enqueue_at(refresh_at, :refresh_custom_wizard_access_token) + Jobs.enqueue_at(refresh_at, :refresh_api_access_token) CustomWizard::Authorization.set_access_token( + service: service, token: token, expires_at: expires_at, refresh_at: refresh_at ) - CustomWizard::Authorization.set_refresh_token(data['refresh_token']) + CustomWizard::Authorization.set_refresh_token(service, data['refresh_token']) end - def self.authorized - CustomWizard::Authorization.access_token[:token] && - CustomWizard::Authorization.access_token[:expires_at].to_datetime > Time.now + def self.authorized(service) + CustomWizard::Authorization.access_token[service, :token] && + CustomWizard::Authorization.access_token[service, :expires_at].to_datetime > Time.now end end From 08172d7d3531b1dbe37e92028eac9762f1963ecd Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Wed, 15 May 2019 07:54:32 +0100 Subject: [PATCH 04/31] added basic authentication and error handling --- lib/authorization.rb | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/authorization.rb b/lib/authorization.rb index 4a6e8642..8b5093d6 100644 --- a/lib/authorization.rb +++ b/lib/authorization.rb @@ -10,7 +10,8 @@ class CustomWizard::Authorization end def self.set_authentication_protocol(service, protocol) - raise Discourse::InvalidParameters unless [BASIC_AUTH, OAUTH2_AUTH].include? protocol + # TODO: make error more informative + raise Discourse::InvalidParameters.new(:protocol) unless [BASIC_AUTH, OAUTH2_AUTH].include? protocol PluginStore.set(service, 'authentication_protocol', protocol) end @@ -38,6 +39,22 @@ class CustomWizard::Authorization PluginStore.set(service, 'code', code) end + def self.username(service) + PluginStore.get(service,'username') + end + + def self.set_username(service, username) + PluginStore.set(service, 'username', username) + end + + def self.password(service) + PluginStore.get(service,'password') + end + + def self.set_password(service, password) + PluginStore.set(service, 'password', password) + end + def self.client_id(service) PluginStore.get(service,'client_id') end @@ -62,6 +79,28 @@ class CustomWizard::Authorization PluginStore.set(service, 'url', url) end + def self.get_header_authorization_string(service) + # TODO: make error more informative, raise error if service not defined + protocol = authentication_protocol(service) + raise Discourse::InvalidParameters.new(:service) unless protocol.present? + raise Discourse::InvalidParameters.new(:protocol) unless [BASIC_AUTH, OAUTH2_AUTH].include? protocol + + if protocol = BASIC_AUTH + # TODO: improve error reporting + username = username(service) + raise Discourse::InvalidParameters.new(:username) unless username.present? + password = password(service) + raise Discourse::InvalidParameters.new(:password) unless password.present? + authorization_string = (username + ":" + password).chomp + "Basic #{Base64.strict_encode64(authorization_string)}" + else + # must be OAUTH2 + # TODO: make error more informative, raise error if there is no recorded access token + raise Discourse::InvalidParameters unless access_token[:token].present? + "Bearer #{access_token[:token]}" + end + end + def self.get_access_token(service) body = { client_id: CustomWizard::Authorization.client_id(service), From 0ca8758f6ceec87872fcf187cd1a4a763d481e03 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Wed, 15 May 2019 08:09:48 +0100 Subject: [PATCH 05/31] comments cleanup and added better paramater check for oauth2 --- lib/authorization.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/authorization.rb b/lib/authorization.rb index 8b5093d6..c18785ab 100644 --- a/lib/authorization.rb +++ b/lib/authorization.rb @@ -10,7 +10,6 @@ class CustomWizard::Authorization end def self.set_authentication_protocol(service, protocol) - # TODO: make error more informative raise Discourse::InvalidParameters.new(:protocol) unless [BASIC_AUTH, OAUTH2_AUTH].include? protocol PluginStore.set(service, 'authentication_protocol', protocol) end @@ -80,13 +79,11 @@ class CustomWizard::Authorization end def self.get_header_authorization_string(service) - # TODO: make error more informative, raise error if service not defined protocol = authentication_protocol(service) raise Discourse::InvalidParameters.new(:service) unless protocol.present? raise Discourse::InvalidParameters.new(:protocol) unless [BASIC_AUTH, OAUTH2_AUTH].include? protocol if protocol = BASIC_AUTH - # TODO: improve error reporting username = username(service) raise Discourse::InvalidParameters.new(:username) unless username.present? password = password(service) @@ -94,10 +91,10 @@ class CustomWizard::Authorization authorization_string = (username + ":" + password).chomp "Basic #{Base64.strict_encode64(authorization_string)}" else - # must be OAUTH2 - # TODO: make error more informative, raise error if there is no recorded access token - raise Discourse::InvalidParameters unless access_token[:token].present? - "Bearer #{access_token[:token]}" + # must be OAUTH2 + access_token = access_token(service) + raise Discourse::InvalidParameters.new(access_token) unless access_token.present? + "Bearer #{access_token}" end end From 52b8c229bac35cbd4b4a44c1102d13645d2de4bd Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Sun, 19 May 2019 14:56:17 +0100 Subject: [PATCH 06/31] added code to handle token refresh jobs specific to services --- jobs/refresh_api_access_token.rb | 4 ++-- lib/authorization.rb | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/jobs/refresh_api_access_token.rb b/jobs/refresh_api_access_token.rb index 08503361..6e517743 100644 --- a/jobs/refresh_api_access_token.rb +++ b/jobs/refresh_api_access_token.rb @@ -1,7 +1,7 @@ module Jobs - class RefreshAPIAccessToken < Jobs::Base + class RefreshApiAccessToken < Jobs::Base def execute(args) - CustomWizard::Authorization.refresh_access_token + CustomWizard::Authorization.refresh_access_token(args[:service]) end end end diff --git a/lib/authorization.rb b/lib/authorization.rb index c18785ab..eac5a77a 100644 --- a/lib/authorization.rb +++ b/lib/authorization.rb @@ -121,7 +121,7 @@ class CustomWizard::Authorization def self.refresh_access_token(service) body = { grant_type: 'refresh_token', - refresh_token: CustomWizard::Authorization.refresh_token + refresh_token: CustomWizard::Authorization.refresh_token(service) } authorization_string = CustomWizard::Authorization.client_id(service) + ':' + CustomWizard::Authorization.client_secret(service) @@ -146,7 +146,11 @@ class CustomWizard::Authorization expires_at = Time.now + data['expires_in'].seconds refresh_at = expires_at.to_time - 2.hours - Jobs.enqueue_at(refresh_at, :refresh_api_access_token) + opts = { + service: service + } + + Jobs.enqueue_at(refresh_at, :refresh_api_access_token, opts) CustomWizard::Authorization.set_access_token( service: service, From fdecbe155ec28c0d83e04e89eff27e550989b9f0 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Wed, 22 May 2019 11:46:34 +0100 Subject: [PATCH 07/31] added controller to handle callbacks --- controllers/authorization.rb | 12 ++++++++++++ plugin.rb | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 controllers/authorization.rb diff --git a/controllers/authorization.rb b/controllers/authorization.rb new file mode 100644 index 00000000..45b93863 --- /dev/null +++ b/controllers/authorization.rb @@ -0,0 +1,12 @@ +class CustomWizard::AuthorizationController < ::ApplicationController + skip_before_action :check_xhr, + :preload_json, + :redirect_to_login_if_required, + :verify_authenticity_token + + def callback + # TODO: work out which service it relates to! + CustomWizard::Authorization.set_code(service, params[:code]) + CustomWizard::Authorization.get_access_token(service) + end +end diff --git a/plugin.rb b/plugin.rb index c7268648..067cb0b4 100644 --- a/plugin.rb +++ b/plugin.rb @@ -49,6 +49,7 @@ after_initialize do get ':wizard_id/steps' => 'wizard#index' get ':wizard_id/steps/:step_id' => 'wizard#index' put ':wizard_id/steps/:step_id' => 'steps#update' + get 'authorization/callback' => "authorization#callback" end require_dependency 'admin_constraint' @@ -81,6 +82,7 @@ after_initialize do load File.expand_path('../controllers/wizard.rb', __FILE__) load File.expand_path('../controllers/steps.rb', __FILE__) load File.expand_path('../controllers/admin.rb', __FILE__) + load File.expand_path('../controllers/authorization.rb', __FILE__) ::UsersController.class_eval do def wizard_path From 3da8833410a4e2d5a33f7cc1c3a46f786071fbd8 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Wed, 29 May 2019 23:18:38 +0100 Subject: [PATCH 08/31] add parameter constraints for callback controller --- controllers/authorization.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/controllers/authorization.rb b/controllers/authorization.rb index 45b93863..c016476d 100644 --- a/controllers/authorization.rb +++ b/controllers/authorization.rb @@ -5,7 +5,10 @@ class CustomWizard::AuthorizationController < ::ApplicationController :verify_authenticity_token def callback - # TODO: work out which service it relates to! + + params.require(:service) + params.require(:code) + CustomWizard::Authorization.set_code(service, params[:code]) CustomWizard::Authorization.get_access_token(service) end From 01a9e7f148eee5e41c8f085cf03b2c7a9335e72e Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 30 May 2019 15:04:34 +1000 Subject: [PATCH 09/31] complete OAuth authorization && Start API Admin UI --- .../controllers/admin-wizards-api.js.es6 | 78 +++++++++ .../custom-wizard-admin-route-map.js.es6 | 3 + .../discourse/models/custom-wizard-api.js.es6 | 50 ++++++ .../discourse/routes/admin-wizards-api.js.es6 | 15 ++ .../routes/admin-wizards-apis.js.es6 | 11 ++ .../templates/admin-wizard-submissions.hbs | 16 -- .../discourse/templates/admin-wizards-api.hbs | 124 ++++++++++++++ .../templates/admin-wizards-apis.hbs | 20 +++ .../discourse/templates/admin-wizards.hbs | 1 + config/locales/client.en.yml | 19 +++ controllers/admin_api.rb | 55 +++++++ controllers/authorization.rb | 15 -- jobs/refresh_api_access_token.rb | 2 +- lib/authorization.rb | 153 ++++++++---------- plugin.rb | 12 +- serializers/api_list_item_serializer.rb | 3 + serializers/api_serializer.rb | 17 ++ 17 files changed, 472 insertions(+), 122 deletions(-) create mode 100644 assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 create mode 100644 assets/javascripts/discourse/models/custom-wizard-api.js.es6 create mode 100644 assets/javascripts/discourse/routes/admin-wizards-api.js.es6 create mode 100644 assets/javascripts/discourse/routes/admin-wizards-apis.js.es6 delete mode 100644 assets/javascripts/discourse/templates/admin-wizard-submissions.hbs create mode 100644 assets/javascripts/discourse/templates/admin-wizards-api.hbs create mode 100644 assets/javascripts/discourse/templates/admin-wizards-apis.hbs create mode 100644 controllers/admin_api.rb delete mode 100644 controllers/authorization.rb create mode 100644 serializers/api_list_item_serializer.rb create mode 100644 serializers/api_serializer.rb diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 new file mode 100644 index 00000000..270dac4f --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -0,0 +1,78 @@ +import { ajax } from 'discourse/lib/ajax'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import CustomWizardApi from '../models/custom-wizard-api'; + +export default Ember.Controller.extend({ + loadingSubscriptions: false, + notAuthorized: Ember.computed.not('api.authorized'), + authorizationTypes: ['oauth', 'basic'], + isOauth: Ember.computed.equal('api.authType', 'oauth'), + + actions: { + addParam() { + this.get('api.authParams').pushObject({}); + }, + + removeParam(param) { + this.get('api.authParams').removeObject(param); + }, + + authorize() { + const api = this.get('api'); + const { authType, authUrl, authParams } = api; + let query = '?'; + + if (authType === 'oauth') { + query += `client_id=${api.get('clientId')}&redirect_uri=${encodeURIComponent(api.get('redirectUri'))}&response_type=code`; + + if (authParams) { + authParams.forEach(p => { + query += `&${p.key}=${encodeURIComponent(p.value)}`; + }); + } + } else { + // basic auth + } + + window.location.href = authUrl + query; + }, + + save() { + const api = this.get('api'); + const service = api.get('service'); + + let data = {}; + + data['auth_type'] = api.get('authType'); + data['auth_url'] = api.get('authUrl'); + + if (data.auth_type === 'oauth') { + data['client_id'] = api.get('clientId'); + data['client_secret'] = api.get('clientSecret'); + + let params = api.get('authParams'); + + if (params) { + data['auth_params'] = JSON.stringify(params); + } + + data['token_url'] = api.get('tokenUrl'); + } else { + data['username'] = api.get('username'); + data['password'] = api.get('password'); + } + + this.set('savingApi', true); + + ajax(`/admin/wizards/apis/${service}/save`, { + type: 'PUT', + data + }).catch(popupAjaxError) + .then(result => { + if (result.success) { + this.set('api', CustomWizardApi.create(result.api)); + } + }).finally(() => this.set('savingApi', false)); + } + } +}); diff --git a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 index 46d0e411..dfe53a82 100644 --- a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 +++ b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 @@ -8,6 +8,9 @@ export default { this.route('adminWizardsSubmissions', { path: '/submissions', resetNamespace: true }, function() { this.route('adminWizardSubmissions', { path: '/:wizard_id', resetNamespace: true }); }); + this.route('adminWizardsApis', { path: '/apis', resetNamespace: true }, function() { + this.route('adminWizardsApi', { path: '/:service', resetNamespace: true }); + }); }); } }; diff --git a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 new file mode 100644 index 00000000..0bdec66a --- /dev/null +++ b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 @@ -0,0 +1,50 @@ +import { ajax } from 'discourse/lib/ajax'; +import { default as computed } from 'ember-addons/ember-computed-decorators'; + +const CustomWizardApi = Discourse.Model.extend({ + @computed('service') + redirectUri(service) { + const baseUrl = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: ''); + return baseUrl + `/admin/wizards/apis/${service}/redirect`; + } +}); + +CustomWizardApi.reopenClass({ + create(params) { + const api = this._super.apply(this); + api.setProperties({ + service: params.service, + authType: params.auth_type, + authUrl: params.auth_url, + tokenUrl: params.token_url, + clientId: params.client_id, + clientSecret: params.client_secret, + authParams: Ember.A(params.auth_params), + authorized: params.authorized, + accessToken: params.access_token, + refreshToken: params.refresh_token, + code: params.code, + tokenExpiresAt: params.token_expires_at, + tokenRefreshAt: params.token_refresh_at + }); + return api; + }, + + find(service) { + return ajax(`/admin/wizards/apis/${service}`, { + type: 'GET' + }).then(result => { + return result; + }); + }, + + list() { + return ajax("/admin/wizards/apis", { + type: 'GET' + }).then(result => { + return result; + }); + } +}); + +export default CustomWizardApi; diff --git a/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 new file mode 100644 index 00000000..72f4ad2a --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 @@ -0,0 +1,15 @@ +import CustomWizardApi from '../models/custom-wizard-api'; + +export default Discourse.Route.extend({ + model(params) { + if (params.service === 'new') { + return {}; + } else { + return CustomWizardApi.find(params.service); + } + }, + + setupController(controller, model){ + controller.set("api", CustomWizardApi.create(model)); + } +}); diff --git a/assets/javascripts/discourse/routes/admin-wizards-apis.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-apis.js.es6 new file mode 100644 index 00000000..b7e89e10 --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-wizards-apis.js.es6 @@ -0,0 +1,11 @@ +import CustomWizardApi from '../models/custom-wizard-api'; + +export default Discourse.Route.extend({ + model() { + return CustomWizardApi.list(); + }, + + setupController(controller, model){ + controller.set("model", model); + } +}); diff --git a/assets/javascripts/discourse/templates/admin-wizard-submissions.hbs b/assets/javascripts/discourse/templates/admin-wizard-submissions.hbs deleted file mode 100644 index ec8a55ff..00000000 --- a/assets/javascripts/discourse/templates/admin-wizard-submissions.hbs +++ /dev/null @@ -1,16 +0,0 @@ -
- - - {{#each fields as |f|}} - - {{/each}} - - {{#each submissions as |s|}} - - {{#each-in s as |k v|}} - - {{/each-in}} - - {{/each}} -
{{f}}
{{v}}
-
diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs new file mode 100644 index 00000000..9ab94f03 --- /dev/null +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -0,0 +1,124 @@ +
+ +
+
+ +
+ {{input value=api.service}} +
+
+ {{i18n 'admin.wizard.api.redirect_uri'}} + {{api.redirectUri}} +
+
+ +
+ +
+ {{combo-box value=api.authType content=authorizationTypes none='admin.wizard.api.auth_type_none'}} +
+
+ +
+ +
+ {{input value=api.authUrl}} +
+
+ +
+ +
+ {{input value=api.tokenUrl}} +
+
+ + {{#if isOauth}} +
+ +
+ {{input value=api.clientId}} +
+
+ +
+ +
+ {{input value=api.clientSecret}} +
+
+ +
+ +
+ {{#each api.authParams as |param|}} +
+ {{input value=param.key placeholder=(i18n 'admin.wizard.api.param_key')}} + {{input value=param.value placeholder=(i18n 'admin.wizard.api.param_value')}} + {{d-button action='removeParam' actionParam=param icon='times'}} +
+ {{/each}} + {{d-button label='admin.wizard.api.param_new' icon='plus' action='addParam'}} +
+
+ {{/if}} +
+ +
+ {{d-button label="admin.wizard.api.save" action="save"}} + {{#if savingApi}} + {{loading-spinner size="small"}} + {{/if}} +
+ +
+ {{d-button label="admin.wizard.api.authorize" action="authorize"}} +
+ +
+ {{#if api.authorized}} + + {{i18n "admin.wizard.api.authorized"}} + {{else}} + + {{i18n "admin.wizard.api.not_authorized"}} + {{/if}} +
+ +
+
+ +
+ {{api.accessToken}} +
+
+ +
+ +
+ {{api.tokenExpiresAt}} +
+
+ +
+ +
+ {{api.tokenRefreshAt}} +
+
+ +
+ +
+ {{api.refreshToken}} +
+
+ +
+ +
+ {{api.code}} +
+
+
+
diff --git a/assets/javascripts/discourse/templates/admin-wizards-apis.hbs b/assets/javascripts/discourse/templates/admin-wizards-apis.hbs new file mode 100644 index 00000000..78a5c954 --- /dev/null +++ b/assets/javascripts/discourse/templates/admin-wizards-apis.hbs @@ -0,0 +1,20 @@ +
+
+
    + {{#each model as |api|}} +
  • + {{#link-to "adminWizardsApi" api.service}}{{api.service}}{{/link-to}} +
  • + {{/each}} +
+
+ {{#link-to 'adminWizardsApi' 'new' class="btn"}} + {{d-icon "plus"}} {{i18n 'admin.wizard.api.new'}} + {{/link-to}} +
+
+ +
+ {{outlet}} +
+
diff --git a/assets/javascripts/discourse/templates/admin-wizards.hbs b/assets/javascripts/discourse/templates/admin-wizards.hbs index f855b33d..3c26e24a 100644 --- a/assets/javascripts/discourse/templates/admin-wizards.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards.hbs @@ -1,6 +1,7 @@ {{#admin-nav}} {{nav-item route='adminWizardsCustom' label='admin.wizard.custom_label'}} {{nav-item route='adminWizardsSubmissions' label='admin.wizard.submissions_label'}} + {{nav-item route='adminWizardsApis' label='admin.wizard.api.nav_label'}} {{/admin-nav}}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 653be688..33bfbc47 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -126,6 +126,25 @@ en: label: "Custom Category" wizard_field: "Wizard Field" user_field: "User Field" + api: + nav_label: 'APIs' + new: 'New Api' + service: 'Api Service' + redirect_uri: "Redirect Uri" + auth_type: 'Authorization Type' + auth_type_none: 'Select a type' + token_url: "Token Url" + auth_url: 'Authorization Url' + client_id: 'Client Id' + client_secret: 'Client Secret' + save: "Save" + authorize: 'Authorize' + authorized: 'Authorized' + not_authorized: "Not Authorized" + params: 'Params' + param_new: 'New Param' + param_key: 'Param Key' + param_value: 'Param Value' wizard_js: location: diff --git a/controllers/admin_api.rb b/controllers/admin_api.rb new file mode 100644 index 00000000..04f5a95f --- /dev/null +++ b/controllers/admin_api.rb @@ -0,0 +1,55 @@ +class CustomWizard::AdminApiController < ::ApplicationController + before_action :ensure_logged_in + before_action :ensure_admin + skip_before_action :check_xhr, only: [:redirect] + + def index + end + + def list + serializer = ActiveModel::ArraySerializer.new( + CustomWizard::Authorization.list, + each_serializer: CustomWizard::ApiListItemSerializer + ) + + render json: MultiJson.dump(serializer) + end + + def find + params.require(:service) + render_serialized(CustomWizard::Authorization.get(params[:service]), CustomWizard::ApiSerializer, root: false) + end + + def save + params.require(:service) + + data = params.permit( + :service, + :auth_type, + :auth_url, + :token_url, + :client_id, + :client_secret, + :username, + :password, + :auth_params + ).to_h + + data[:auth_params] = JSON.parse(data[:auth_params]) if data[:auth_params] + + result = CustomWizard::Authorization.set(data[:service], data.except!(:service)) + + render json: success_json.merge(api: CustomWizard::ApiSerializer.new(result, root: false)) + end + + def redirect + params.require(:service) + params.require(:code) + + CustomWizard::Authorization.set(params[:service], code: params[:code]) + + CustomWizard::Authorization.get_token(params[:service]) + + return redirect_to path('/admin/wizards/apis/' + params[:service]) + end +end diff --git a/controllers/authorization.rb b/controllers/authorization.rb deleted file mode 100644 index c016476d..00000000 --- a/controllers/authorization.rb +++ /dev/null @@ -1,15 +0,0 @@ -class CustomWizard::AuthorizationController < ::ApplicationController - skip_before_action :check_xhr, - :preload_json, - :redirect_to_login_if_required, - :verify_authenticity_token - - def callback - - params.require(:service) - params.require(:code) - - CustomWizard::Authorization.set_code(service, params[:code]) - CustomWizard::Authorization.get_access_token(service) - end -end diff --git a/jobs/refresh_api_access_token.rb b/jobs/refresh_api_access_token.rb index 6e517743..1b3221e8 100644 --- a/jobs/refresh_api_access_token.rb +++ b/jobs/refresh_api_access_token.rb @@ -1,7 +1,7 @@ module Jobs class RefreshApiAccessToken < Jobs::Base def execute(args) - CustomWizard::Authorization.refresh_access_token(args[:service]) + CustomWizard::Authorization.refresh_token(args[:service]) end end end diff --git a/lib/authorization.rb b/lib/authorization.rb index eac5a77a..a9b27142 100644 --- a/lib/authorization.rb +++ b/lib/authorization.rb @@ -1,81 +1,59 @@ require 'excon' class CustomWizard::Authorization + include ActiveModel::SerializerSupport - BASIC_AUTH = 'basic_authentication' - OAUTH2_AUTH = 'OAuth2_authentication' + NGROK_URL = '' - def self.authentication_protocol(service) - PluginStore.get(service, 'authentication_protocol') || {} + attr_accessor :authorized, + :service, + :auth_type, + :auth_url, + :token_url, + :client_id, + :client_secret, + :auth_params, + :access_token, + :refresh_token, + :token_expires_at, + :token_refresh_at, + :code, + :username, + :password + + def initialize(service, params) + @service = service + data = params.is_a?(String) ? ::JSON.parse(params) : params + + data.each do |k, v| + self.send "#{k}=", v if self.respond_to?(k) + end end - def self.set_authentication_protocol(service, protocol) - raise Discourse::InvalidParameters.new(:protocol) unless [BASIC_AUTH, OAUTH2_AUTH].include? protocol - PluginStore.set(service, 'authentication_protocol', protocol) + def authorized + @authorized ||= @access_token && @token_expires_at.to_datetime > Time.now end - def self.access_token(service) - PluginStore.get(service, 'access_token') || {} + def self.set(service, data) + model = self.get(service) || {} + + data.each do |k, v| + model.send "#{k}=", v if model.respond_to?(k) + end + + PluginStore.set("custom_wizard_#{service}", 'authorization', model.as_json) + + self.get(service) end - def self.set_access_token(service, data) - PluginStore.set(service, 'access_token', data) + def self.get(service) + data = PluginStore.get("custom_wizard_#{service}", 'authorization') + self.new(service, data) end - def self.refresh_token (service) - PluginStore.get(service, 'refresh_token') - end - - def self.set_refresh_token(service, token) - PluginStore.set(service, 'refresh_token', token) - end - - def self.code(service) - PluginStore.get(service,'code') - end - - def self.set_code(service, code) - PluginStore.set(service, 'code', code) - end - - def self.username(service) - PluginStore.get(service,'username') - end - - def self.set_username(service, username) - PluginStore.set(service, 'username', username) - end - - def self.password(service) - PluginStore.get(service,'password') - end - - def self.set_password(service, password) - PluginStore.set(service, 'password', password) - end - - def self.client_id(service) - PluginStore.get(service,'client_id') - end - - def self.set_client_id(service, client_id) - PluginStore.set(service, 'client_id', client_id) - end - - def self.client_secret(service) - PluginStore.get(service,'client_secret') - end - - def self.set_client_secret(service, client_secret) - PluginStore.set(service, 'client_secret', client_secret) - end - - def self.url(service) - PluginStore.get(service,'url') - end - - def self.set_url(service, url) - PluginStore.set(service, 'url', url) + def self.list + PluginStoreRow.where("plugin_name LIKE 'custom_wizard_%' AND key = 'authorization'") + .map { |record| self.new(record['plugin_name'].split('_').last, record['value']) } end def self.get_header_authorization_string(service) @@ -98,17 +76,19 @@ class CustomWizard::Authorization end end - def self.get_access_token(service) + def self.get_token(service) + authorization = CustomWizard::Authorization.get(service) + body = { - client_id: CustomWizard::Authorization.client_id(service), - client_secret: CustomWizard::Authorization.client_secret(service), - code: CustomWizard::Authorization.code(service), + client_id: authorization.client_id, + client_secret: authorization.client_secret, + code: authorization.code, grant_type: 'authorization_code', - redirect_uri: (Rails.env.development? ? CustomWizard::NGROK_URL : Discourse.base_url) + '/custom_wizard/authorization/callback' + redirect_uri: Discourse.base_url + "/admin/wizards/apis/#{service}/redirect" } result = Excon.post( - CustomWizard::Authorization.url(service), + authorization.token_url, :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, @@ -118,16 +98,18 @@ class CustomWizard::Authorization self.handle_token_result(service, result) end - def self.refresh_access_token(service) + def self.refresh_token(service) + authorization = CustomWizard::Authorization.get(service) + body = { grant_type: 'refresh_token', - refresh_token: CustomWizard::Authorization.refresh_token(service) + refresh_token: authorization.refresh_token } - authorization_string = CustomWizard::Authorization.client_id(service) + ':' + CustomWizard::Authorization.client_secret(service) + authorization_string = authorization.client_id + ':' + authorization.client_secret result = Excon.post( - CustomWizard::Authorization.url(service), + authorization.token_url, :headers => { "Content-Type" => "application/x-www-form-urlencoded", "Authorization" => "Basic #{Base64.strict_encode64(authorization_string)}" @@ -140,9 +122,11 @@ class CustomWizard::Authorization def self.handle_token_result(service, result) data = JSON.parse(result.body) + return false if (data['error']) - token = data['access_token'] + access_token = data['access_token'] + refresh_token = data['refresh_token'] expires_at = Time.now + data['expires_in'].seconds refresh_at = expires_at.to_time - 2.hours @@ -152,18 +136,11 @@ class CustomWizard::Authorization Jobs.enqueue_at(refresh_at, :refresh_api_access_token, opts) - CustomWizard::Authorization.set_access_token( - service: service, - token: token, - expires_at: expires_at, - refresh_at: refresh_at + CustomWizard::Authorization.set(service, + access_token: access_token, + refresh_token: refresh_token, + token_expires_at: expires_at, + token_refresh_at: refresh_at ) - - CustomWizard::Authorization.set_refresh_token(service, data['refresh_token']) - end - - def self.authorized(service) - CustomWizard::Authorization.access_token[service, :token] && - CustomWizard::Authorization.access_token[service, :expires_at].to_datetime > Time.now end end diff --git a/plugin.rb b/plugin.rb index 067cb0b4..45725879 100644 --- a/plugin.rb +++ b/plugin.rb @@ -49,12 +49,12 @@ after_initialize do get ':wizard_id/steps' => 'wizard#index' get ':wizard_id/steps/:step_id' => 'wizard#index' put ':wizard_id/steps/:step_id' => 'steps#update' - get 'authorization/callback' => "authorization#callback" end require_dependency 'admin_constraint' Discourse::Application.routes.append do mount ::CustomWizard::Engine, at: 'w' + post 'wizard/authorization/callback' => "custom_wizard/authorization#callback" scope module: 'custom_wizard', constraints: AdminConstraint.new do get 'admin/wizards' => 'admin#index' @@ -67,11 +67,17 @@ after_initialize do delete 'admin/wizards/custom/remove' => 'admin#remove' get 'admin/wizards/submissions' => 'admin#index' get 'admin/wizards/submissions/:wizard_id' => 'admin#submissions' + get 'admin/wizards/apis' => 'admin_api#list' + get 'admin/wizards/apis/new' => 'admin_api#index' + get 'admin/wizards/apis/:service' => 'admin_api#find' + put 'admin/wizards/apis/:service/save' => 'admin_api#save' + get 'admin/wizards/apis/:service/redirect' => 'admin_api#redirect' end end load File.expand_path('../jobs/clear_after_time_wizard.rb', __FILE__) load File.expand_path('../jobs/set_after_time_wizard.rb', __FILE__) + load File.expand_path('../jobs/refresh_api_access_token.rb', __FILE__) load File.expand_path('../lib/builder.rb', __FILE__) load File.expand_path('../lib/field.rb', __FILE__) load File.expand_path('../lib/step_updater.rb', __FILE__) @@ -82,7 +88,9 @@ after_initialize do load File.expand_path('../controllers/wizard.rb', __FILE__) load File.expand_path('../controllers/steps.rb', __FILE__) load File.expand_path('../controllers/admin.rb', __FILE__) - load File.expand_path('../controllers/authorization.rb', __FILE__) + load File.expand_path('../controllers/admin_api.rb', __FILE__) + load File.expand_path('../serializers/api_serializer.rb', __FILE__) + load File.expand_path('../serializers/api_list_item_serializer.rb', __FILE__) ::UsersController.class_eval do def wizard_path diff --git a/serializers/api_list_item_serializer.rb b/serializers/api_list_item_serializer.rb new file mode 100644 index 00000000..854349c8 --- /dev/null +++ b/serializers/api_list_item_serializer.rb @@ -0,0 +1,3 @@ +class CustomWizard::ApiListItemSerializer < ApplicationSerializer + attributes :service +end diff --git a/serializers/api_serializer.rb b/serializers/api_serializer.rb new file mode 100644 index 00000000..3bf86348 --- /dev/null +++ b/serializers/api_serializer.rb @@ -0,0 +1,17 @@ +class CustomWizard::ApiSerializer < ApplicationSerializer + attributes :service, + :auth_type, + :auth_url, + :token_url, + :client_id, + :client_secret, + :authorized, + :auth_params, + :access_token, + :refresh_token, + :token_expires_at, + :token_refresh_at, + :code, + :username, + :password +end From 9ff904d0fa695433088801c96fd95f150b3bded6 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Fri, 31 May 2019 17:54:11 +1000 Subject: [PATCH 10/31] WIP: Add Endpoint Administration --- .../controllers/admin-wizards-api.js.es6 | 16 +++- .../discourse/models/custom-wizard-api.js.es6 | 31 ++++--- .../discourse/routes/admin-wizards-api.js.es6 | 2 +- .../discourse/templates/admin-wizards-api.hbs | 90 +++++++++++++------ assets/stylesheets/wizard_custom_admin.scss | 63 ++++++++++++- config/locales/client.en.yml | 18 +++- controllers/admin_api.rb | 55 ------------ controllers/api.rb | 69 ++++++++++++++ jobs/refresh_api_access_token.rb | 2 +- lib/api/api.rb | 16 ++++ lib/{ => api}/authorization.rb | 19 ++-- lib/api/endpoint.rb | 46 ++++++++++ plugin.rb | 25 +++--- serializers/api/api_serializer.rb | 19 ++++ .../authorization_serializer.rb} | 5 +- serializers/api/basic_api_serializer.rb | 3 + serializers/api/endpoint_serializer.rb | 5 ++ serializers/api_list_item_serializer.rb | 3 - 18 files changed, 356 insertions(+), 131 deletions(-) delete mode 100644 controllers/admin_api.rb create mode 100644 controllers/api.rb create mode 100644 lib/api/api.rb rename lib/{ => api}/authorization.rb (86%) create mode 100644 lib/api/endpoint.rb create mode 100644 serializers/api/api_serializer.rb rename serializers/{api_serializer.rb => api/authorization_serializer.rb} (76%) create mode 100644 serializers/api/basic_api_serializer.rb create mode 100644 serializers/api/endpoint_serializer.rb delete mode 100644 serializers/api_list_item_serializer.rb diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 270dac4f..642eb9e7 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -7,6 +7,7 @@ export default Ember.Controller.extend({ notAuthorized: Ember.computed.not('api.authorized'), authorizationTypes: ['oauth', 'basic'], isOauth: Ember.computed.equal('api.authType', 'oauth'), + endpointMethods: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'], actions: { addParam() { @@ -17,6 +18,14 @@ export default Ember.Controller.extend({ this.get('api.authParams').removeObject(param); }, + addEndpoint() { + this.get('api.endpoints').pushObject({}); + }, + + removeEndpoint(endpoint) { + this.get('api.endpoints').removeObject(endpoint); + }, + authorize() { const api = this.get('api'); const { authType, authUrl, authParams } = api; @@ -62,9 +71,14 @@ export default Ember.Controller.extend({ data['password'] = api.get('password'); } + const endpoints = api.get('endpoints'); + if (endpoints.length) { + data['endpoints'] = JSON.stringify(endpoints); + } + this.set('savingApi', true); - ajax(`/admin/wizards/apis/${service}/save`, { + ajax(`/admin/wizards/apis/${service}`, { type: 'PUT', data }).catch(popupAjaxError) diff --git a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 index 0bdec66a..6cfbae06 100644 --- a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 @@ -12,21 +12,26 @@ const CustomWizardApi = Discourse.Model.extend({ CustomWizardApi.reopenClass({ create(params) { const api = this._super.apply(this); + const authorization = params.authorization; + const endpoints = params.endpoints; + api.setProperties({ service: params.service, - authType: params.auth_type, - authUrl: params.auth_url, - tokenUrl: params.token_url, - clientId: params.client_id, - clientSecret: params.client_secret, - authParams: Ember.A(params.auth_params), - authorized: params.authorized, - accessToken: params.access_token, - refreshToken: params.refresh_token, - code: params.code, - tokenExpiresAt: params.token_expires_at, - tokenRefreshAt: params.token_refresh_at + authType: authorization.auth_type, + authUrl: authorization.auth_url, + tokenUrl: authorization.token_url, + clientId: authorization.client_id, + clientSecret: authorization.client_secret, + authParams: Ember.A(authorization.auth_params), + authorized: authorization.authorized, + accessToken: authorization.access_token, + refreshToken: authorization.refresh_token, + code: authorization.code, + tokenExpiresAt: authorization.token_expires_at, + tokenRefreshAt: authorization.token_refresh_at, + endpoints: Ember.A(endpoints) }); + return api; }, @@ -34,7 +39,7 @@ CustomWizardApi.reopenClass({ return ajax(`/admin/wizards/apis/${service}`, { type: 'GET' }).then(result => { - return result; + return CustomWizardApi.create(result); }); }, diff --git a/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 index 72f4ad2a..34320924 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 @@ -10,6 +10,6 @@ export default Discourse.Route.extend({ }, setupController(controller, model){ - controller.set("api", CustomWizardApi.create(model)); + controller.set("api", model); } }); diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index 9ab94f03..f91a4c3f 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -1,11 +1,29 @@ -
+
+
+ {{input value=api.service placeholder=(i18n 'admin.wizard.api.service')}} +
+ +
+ {{#if savingApi}} + {{loading-spinner size="small"}} + {{/if}} + {{d-button label="admin.wizard.api.save" action="save" class="btn-primary"}} + {{d-button action="removeApi" label="admin.wizard.api.remove"}} +
+
+ +
+ {{i18n 'admin.wizard.api.auth'}} +
+ +
+
+ +
+ {{i18n 'admin.wizard.api.auth_settings'}} +
-
- -
- {{input value=api.service}} -
{{i18n 'admin.wizard.api.redirect_uri'}} {{api.redirectUri}} @@ -62,30 +80,26 @@
{{/if}} + +
+ {{d-button label="admin.wizard.api.authorize" action="authorize"}} +
-
- {{d-button label="admin.wizard.api.save" action="save"}} - {{#if savingApi}} - {{loading-spinner size="small"}} - {{/if}} -
+
+
+ {{i18n 'admin.wizard.api.auth_status'}} +
-
- {{d-button label="admin.wizard.api.authorize" action="authorize"}} -
- -
- {{#if api.authorized}} - - {{i18n "admin.wizard.api.authorized"}} - {{else}} - - {{i18n "admin.wizard.api.not_authorized"}} - {{/if}} -
- -
+
+ {{#if api.authorized}} + + {{i18n "admin.wizard.api.authorized"}} + {{else}} + + {{i18n "admin.wizard.api.not_authorized"}} + {{/if}} +
@@ -122,3 +136,25 @@
+ +
+ {{i18n 'admin.wizard.api.endpoint.label'}} +
+ +
+ {{d-button action='addEndpoint' label='admin.wizard.api.endpoint.add' icon='plus'}} + +
+
    + {{#each api.endpoints as |endpoint|}} +
  • +
    + {{combo-box content=endpointMethods value=endpoint.method none="admin.wizard.api.endpoint.method"}} + {{input value=endpoint.url placeholder=(i18n 'admin.wizard.api.endpoint.url') class='endpoint-url'}} + {{d-button action='removeEndpoint' actionParam=endpoint icon='times' class='remove-endpoint'}} +
    +
  • + {{/each}} +
+
+
diff --git a/assets/stylesheets/wizard_custom_admin.scss b/assets/stylesheets/wizard_custom_admin.scss index 10efb43a..7bec453f 100644 --- a/assets/stylesheets/wizard_custom_admin.scss +++ b/assets/stylesheets/wizard_custom_admin.scss @@ -298,6 +298,65 @@ } } -.wizard-step-contents{ - height: unset !important; +.wizard-step-contents { + height: unset !important; +} + +.admin-wizards-api { + margin-bottom: 40px; + + .content-list { + margin-right: 20px; + } +} + +.wizard-api-header { + display: flex; + justify-content: space-between; + margin-bottom: 20px; +} + +.wizard-api-authentication { + display: flex; + background-color: $primary-very-low; + padding: 20px; + margin-bottom: 20px; + + .settings { + border-right: 1px solid #333; + margin-right: 10px; + padding-right: 20px; + } +} + +.wizard-api-endpoints { + background-color: $primary-very-low; + padding: 20px; + + .endpoint-list { + margin-top: 20px; + + ul { + margin: 0; + list-style: none; + } + } + + .endpoint { + display: flex; + + .combo-box { + width: 200px; + margin-right: 20px; + } + + .endpoint-url { + margin: 0; + width: 450px; + } + + .remove-endpoint { + margin-left: auto; + } + } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 33bfbc47..0df2b330 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -129,6 +129,10 @@ en: api: nav_label: 'APIs' new: 'New Api' + + auth: "Authentication" + auth_settings: "Settings" + auth_status: "Status" service: 'Api Service' redirect_uri: "Redirect Uri" auth_type: 'Authorization Type' @@ -137,8 +141,8 @@ en: auth_url: 'Authorization Url' client_id: 'Client Id' client_secret: 'Client Secret' - save: "Save" - authorize: 'Authorize' + + status: "Status" authorized: 'Authorized' not_authorized: "Not Authorized" params: 'Params' @@ -146,6 +150,16 @@ en: param_key: 'Param Key' param_value: 'Param Value' + remove: 'Delete' + authorize: 'Authorize' + save: "Save" + + endpoint: + label: "Endpoints" + add: "Add Endpoint" + method: "Select a method" + url: "Enter a url" + wizard_js: location: name: diff --git a/controllers/admin_api.rb b/controllers/admin_api.rb deleted file mode 100644 index 04f5a95f..00000000 --- a/controllers/admin_api.rb +++ /dev/null @@ -1,55 +0,0 @@ -class CustomWizard::AdminApiController < ::ApplicationController - before_action :ensure_logged_in - before_action :ensure_admin - skip_before_action :check_xhr, only: [:redirect] - - def index - end - - def list - serializer = ActiveModel::ArraySerializer.new( - CustomWizard::Authorization.list, - each_serializer: CustomWizard::ApiListItemSerializer - ) - - render json: MultiJson.dump(serializer) - end - - def find - params.require(:service) - render_serialized(CustomWizard::Authorization.get(params[:service]), CustomWizard::ApiSerializer, root: false) - end - - def save - params.require(:service) - - data = params.permit( - :service, - :auth_type, - :auth_url, - :token_url, - :client_id, - :client_secret, - :username, - :password, - :auth_params - ).to_h - - data[:auth_params] = JSON.parse(data[:auth_params]) if data[:auth_params] - - result = CustomWizard::Authorization.set(data[:service], data.except!(:service)) - - render json: success_json.merge(api: CustomWizard::ApiSerializer.new(result, root: false)) - end - - def redirect - params.require(:service) - params.require(:code) - - CustomWizard::Authorization.set(params[:service], code: params[:code]) - - CustomWizard::Authorization.get_token(params[:service]) - - return redirect_to path('/admin/wizards/apis/' + params[:service]) - end -end diff --git a/controllers/api.rb b/controllers/api.rb new file mode 100644 index 00000000..f16cba56 --- /dev/null +++ b/controllers/api.rb @@ -0,0 +1,69 @@ +class CustomWizard::ApiController < ::ApplicationController + before_action :ensure_logged_in + before_action :ensure_admin + skip_before_action :check_xhr, only: [:redirect] + + def index + end + + def list + serializer = ActiveModel::ArraySerializer.new( + CustomWizard::Api.list, + each_serializer: CustomWizard::BasicApiSerializer + ) + + render json: MultiJson.dump(serializer) + end + + def find + params.require(:service) + render_serialized(CustomWizard::Api.new(params[:service]), CustomWizard::ApiSerializer, root: false) + end + + def save + params.require(:service) + service = params.permit(:service) + + data[:auth_params] = JSON.parse(@auth_data[:auth_params]) if @auth_data[:auth_params] + + CustomWizard::Api::Authorization.set(service, @auth_data) + + @endpoint_data.each do |endpoint| + CustomWizard::Api::Endpoint.set(service, endpoint) + end + + render json: success_json.merge( + api: CustomWizard::ApiSerializer.new(params[:service], root: false) + ) + end + + def redirect + params.require(:service) + params.require(:code) + + CustomWizard::Api::Authorization.set(params[:service], code: params[:code]) + + CustomWizard::Api::Authorization.get_token(params[:service]) + + return redirect_to path('/admin/wizards/apis/' + params[:service]) + end + + private + + def auth_data + @auth_data ||= params.permit( + :auth_type, + :auth_url, + :token_url, + :client_id, + :client_secret, + :username, + :password, + :auth_params + ).to_h + end + + def endpoint_data + @endpoint_data ||= JSON.parse(params.permit(endpoints: [:id, :type, :url])) + end +end diff --git a/jobs/refresh_api_access_token.rb b/jobs/refresh_api_access_token.rb index 1b3221e8..c7fb94e5 100644 --- a/jobs/refresh_api_access_token.rb +++ b/jobs/refresh_api_access_token.rb @@ -1,7 +1,7 @@ module Jobs class RefreshApiAccessToken < Jobs::Base def execute(args) - CustomWizard::Authorization.refresh_token(args[:service]) + CustomWizard::Api::Authorization.refresh_token(args[:service]) end end end diff --git a/lib/api/api.rb b/lib/api/api.rb new file mode 100644 index 00000000..dde491d4 --- /dev/null +++ b/lib/api/api.rb @@ -0,0 +1,16 @@ +class CustomWizard::Api + include ActiveModel::SerializerSupport + + attr_accessor :service + + def initialize(service) + @service = service + end + + def self.list + PluginStoreRow.where("plugin_name LIKE 'custom_wizard_api_%' AND key = 'authorization'") + .map do |record| + self.new(record['plugin_name'].split('_').last) + end + end +end diff --git a/lib/authorization.rb b/lib/api/authorization.rb similarity index 86% rename from lib/authorization.rb rename to lib/api/authorization.rb index a9b27142..9ae5a211 100644 --- a/lib/authorization.rb +++ b/lib/api/authorization.rb @@ -1,10 +1,8 @@ require 'excon' -class CustomWizard::Authorization +class CustomWizard::Api::Authorization include ActiveModel::SerializerSupport - NGROK_URL = '' - attr_accessor :authorized, :service, :auth_type, @@ -41,21 +39,16 @@ class CustomWizard::Authorization model.send "#{k}=", v if model.respond_to?(k) end - PluginStore.set("custom_wizard_#{service}", 'authorization', model.as_json) + PluginStore.set("custom_wizard_api_#{service}", 'authorization', model.as_json) self.get(service) end def self.get(service) - data = PluginStore.get("custom_wizard_#{service}", 'authorization') + data = PluginStore.get("custom_wizard_api_#{service}", 'authorization') self.new(service, data) end - def self.list - PluginStoreRow.where("plugin_name LIKE 'custom_wizard_%' AND key = 'authorization'") - .map { |record| self.new(record['plugin_name'].split('_').last, record['value']) } - end - def self.get_header_authorization_string(service) protocol = authentication_protocol(service) raise Discourse::InvalidParameters.new(:service) unless protocol.present? @@ -77,7 +70,7 @@ class CustomWizard::Authorization end def self.get_token(service) - authorization = CustomWizard::Authorization.get(service) + authorization = CustomWizard::Api::Authorization.get(service) body = { client_id: authorization.client_id, @@ -99,7 +92,7 @@ class CustomWizard::Authorization end def self.refresh_token(service) - authorization = CustomWizard::Authorization.get(service) + authorization = CustomWizard::Api::Authorization.get(service) body = { grant_type: 'refresh_token', @@ -136,7 +129,7 @@ class CustomWizard::Authorization Jobs.enqueue_at(refresh_at, :refresh_api_access_token, opts) - CustomWizard::Authorization.set(service, + CustomWizard::Api::Authorization.set(service, access_token: access_token, refresh_token: refresh_token, token_expires_at: expires_at, diff --git a/lib/api/endpoint.rb b/lib/api/endpoint.rb new file mode 100644 index 00000000..b1e40e69 --- /dev/null +++ b/lib/api/endpoint.rb @@ -0,0 +1,46 @@ +class CustomWizard::Api::Endpoint + include ActiveModel::SerializerSupport + + attr_accessor :id, + :method, + :url + + def initialize(service, params) + @service = service + data = params.is_a?(String) ? ::JSON.parse(params) : params + + data.each do |k, v| + self.send "#{k}=", v if self.respond_to?(k) + end + end + + def self.set(service, data) + model = data[:endpoint_id] ? self.get(service, data[:endpoint_id]) : {} + endpoint_id = model[:endpoint_id] || SecureRandom.hex(8) + + data.each do |k, v| + model.send "#{k}=", v if model.respond_to?(k) + end + + PluginStore.set("custom_wizard_api_#{service}", "endpoint_#{endpoint_id}", model.as_json) + + self.get(service) + end + + def self.get(service, endpoint_id) + return nil if !endpoint_id + data = PluginStore.get("custom_wizard_api_#{service}", "endpoint_#{endpoint_id}") + data[:id] = endpoint_id + self.new(service, data) + end + + def self.list + PluginStoreRow.where("plugin_name LIKE 'custom_wizard_api_%' AND key LIKE 'endpoint_%'") + .map do |record| + service = record['plugin_name'].split('_').last + data = ::JSON.parse(record['value']) + data[:id] = record['key'].split('_').last + self.new(service, data) + end + end +end diff --git a/plugin.rb b/plugin.rb index 45725879..d35d929f 100644 --- a/plugin.rb +++ b/plugin.rb @@ -67,30 +67,35 @@ after_initialize do delete 'admin/wizards/custom/remove' => 'admin#remove' get 'admin/wizards/submissions' => 'admin#index' get 'admin/wizards/submissions/:wizard_id' => 'admin#submissions' - get 'admin/wizards/apis' => 'admin_api#list' - get 'admin/wizards/apis/new' => 'admin_api#index' - get 'admin/wizards/apis/:service' => 'admin_api#find' - put 'admin/wizards/apis/:service/save' => 'admin_api#save' - get 'admin/wizards/apis/:service/redirect' => 'admin_api#redirect' + get 'admin/wizards/apis' => 'api#list' + get 'admin/wizards/apis/new' => 'api#index' + get 'admin/wizards/apis/:service' => 'api#find' + put 'admin/wizards/apis/:service' => 'api#save' + get 'admin/wizards/apis/:service/redirect' => 'api#redirect' end end load File.expand_path('../jobs/clear_after_time_wizard.rb', __FILE__) load File.expand_path('../jobs/set_after_time_wizard.rb', __FILE__) - load File.expand_path('../jobs/refresh_api_access_token.rb', __FILE__) load File.expand_path('../lib/builder.rb', __FILE__) load File.expand_path('../lib/field.rb', __FILE__) load File.expand_path('../lib/step_updater.rb', __FILE__) load File.expand_path('../lib/template.rb', __FILE__) load File.expand_path('../lib/wizard.rb', __FILE__) load File.expand_path('../lib/wizard_edits.rb', __FILE__) - load File.expand_path('../lib/authorization.rb', __FILE__) load File.expand_path('../controllers/wizard.rb', __FILE__) load File.expand_path('../controllers/steps.rb', __FILE__) load File.expand_path('../controllers/admin.rb', __FILE__) - load File.expand_path('../controllers/admin_api.rb', __FILE__) - load File.expand_path('../serializers/api_serializer.rb', __FILE__) - load File.expand_path('../serializers/api_list_item_serializer.rb', __FILE__) + + load File.expand_path('../jobs/refresh_api_access_token.rb', __FILE__) + load File.expand_path('../lib/api/api.rb', __FILE__) + load File.expand_path('../lib/api/authorization.rb', __FILE__) + load File.expand_path('../lib/api/endpoint.rb', __FILE__) + load File.expand_path('../controllers/api.rb', __FILE__) + load File.expand_path('../serializers/api/api_serializer.rb', __FILE__) + 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__) ::UsersController.class_eval do def wizard_path diff --git a/serializers/api/api_serializer.rb b/serializers/api/api_serializer.rb new file mode 100644 index 00000000..aae825c2 --- /dev/null +++ b/serializers/api/api_serializer.rb @@ -0,0 +1,19 @@ +class CustomWizard::ApiSerializer < ApplicationSerializer + attributes :service, + :authorization, + :endpoints + + def authorization + CustomWizard::Api::AuthorizationSerializer.new( + CustomWizard::Api::Authorization.get(object.service), + root: false + ) + end + + def endpoints + ActiveModel::ArraySerializer.new( + CustomWizard::Api::Endpoint.list, + each_serializer: CustomWizard::Api::EndpointSerializer + ) + end +end diff --git a/serializers/api_serializer.rb b/serializers/api/authorization_serializer.rb similarity index 76% rename from serializers/api_serializer.rb rename to serializers/api/authorization_serializer.rb index 3bf86348..2ca347b5 100644 --- a/serializers/api_serializer.rb +++ b/serializers/api/authorization_serializer.rb @@ -1,6 +1,5 @@ -class CustomWizard::ApiSerializer < ApplicationSerializer - attributes :service, - :auth_type, +class CustomWizard::Api::AuthorizationSerializer < ApplicationSerializer + attributes :auth_type, :auth_url, :token_url, :client_id, diff --git a/serializers/api/basic_api_serializer.rb b/serializers/api/basic_api_serializer.rb new file mode 100644 index 00000000..b5430897 --- /dev/null +++ b/serializers/api/basic_api_serializer.rb @@ -0,0 +1,3 @@ +class CustomWizard::BasicApiSerializer < ApplicationSerializer + attributes :service +end diff --git a/serializers/api/endpoint_serializer.rb b/serializers/api/endpoint_serializer.rb new file mode 100644 index 00000000..7ca802c4 --- /dev/null +++ b/serializers/api/endpoint_serializer.rb @@ -0,0 +1,5 @@ +class CustomWizard::Api::EndpointSerializer < ApplicationSerializer + attributes :id, + :type, + :url +end diff --git a/serializers/api_list_item_serializer.rb b/serializers/api_list_item_serializer.rb deleted file mode 100644 index 854349c8..00000000 --- a/serializers/api_list_item_serializer.rb +++ /dev/null @@ -1,3 +0,0 @@ -class CustomWizard::ApiListItemSerializer < ApplicationSerializer - attributes :service -end From 6afdeb74083d0e8c1e821e34e6b96a97ba113863 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Fri, 31 May 2019 22:32:24 +0100 Subject: [PATCH 11/31] resolved type error for api.get not a function and for error on save when no endpoints --- .../controllers/admin-wizards-api.js.es6 | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 642eb9e7..e230ce37 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -32,7 +32,7 @@ export default Ember.Controller.extend({ let query = '?'; if (authType === 'oauth') { - query += `client_id=${api.get('clientId')}&redirect_uri=${encodeURIComponent(api.get('redirectUri'))}&response_type=code`; + query += `client_id=${api.clientId}&redirect_uri=${encodeURIComponent(api.redirectUri)}&response_type=code`; if (authParams) { authParams.forEach(p => { @@ -47,33 +47,36 @@ export default Ember.Controller.extend({ }, save() { + debugger; const api = this.get('api'); - const service = api.get('service'); + const service = api.service; let data = {}; - data['auth_type'] = api.get('authType'); - data['auth_url'] = api.get('authUrl'); + data['auth_type'] = api.authType; + data['auth_url'] = api.authUrl; if (data.auth_type === 'oauth') { - data['client_id'] = api.get('clientId'); - data['client_secret'] = api.get('clientSecret'); + data['client_id'] = api.clientId; + data['client_secret'] = api.clientSecret; - let params = api.get('authParams'); + let params = api.authParams; if (params) { data['auth_params'] = JSON.stringify(params); } - data['token_url'] = api.get('tokenUrl'); + data['token_url'] = api.tokenUrl; } else { - data['username'] = api.get('username'); - data['password'] = api.get('password'); + data['username'] = api.username; + data['password'] = api.password; } - const endpoints = api.get('endpoints'); - if (endpoints.length) { - data['endpoints'] = JSON.stringify(endpoints); + const endpoints = api.endpoints; + if (endpoints != undefined) { + if (endpoints.length) { + data['endpoints'] = JSON.stringify(endpoints); + } } this.set('savingApi', true); From 06b5e28ced5ae30ce86067b6371f70176c825d8e Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Fri, 31 May 2019 22:37:17 +0100 Subject: [PATCH 12/31] removed debug statement from javascript --- .../javascripts/discourse/controllers/admin-wizards-api.js.es6 | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index e230ce37..ec4b5170 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -47,7 +47,6 @@ export default Ember.Controller.extend({ }, save() { - debugger; const api = this.get('api'); const service = api.service; From 582e2cca42ee69e6da8408e82709afe491d61139 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Fri, 31 May 2019 23:25:22 +0100 Subject: [PATCH 13/31] fixed issue that prevented you adding params or endpoints when there were none to begin with --- .../discourse/controllers/admin-wizards-api.js.es6 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index ec4b5170..3787c60e 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -11,6 +11,9 @@ export default Ember.Controller.extend({ actions: { addParam() { + if (this.get('api.authParams') == undefined) { + this.set('api.authParams',[]); + }; this.get('api.authParams').pushObject({}); }, @@ -19,6 +22,9 @@ export default Ember.Controller.extend({ }, addEndpoint() { + if (this.get('api.endpoints') == undefined) { + this.set('api.endpoints',[]); + }; this.get('api.endpoints').pushObject({}); }, From 65979e1987be7af12baf152bb86eb0ca1d95b477 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Sat, 1 Jun 2019 00:06:30 +0100 Subject: [PATCH 14/31] non working controller updates --- controllers/api.rb | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/controllers/api.rb b/controllers/api.rb index f16cba56..059063ca 100644 --- a/controllers/api.rb +++ b/controllers/api.rb @@ -21,15 +21,23 @@ class CustomWizard::ApiController < ::ApplicationController end def save + byebug params.require(:service) service = params.permit(:service) + auth_data = params[:auth_params] + endpoints_data = params[:endpoints] - data[:auth_params] = JSON.parse(@auth_data[:auth_params]) if @auth_data[:auth_params] + service_auth_data = JSON.parse(auth_data) if !auth_data.nil? + service_endpoints = JSON.parse(endpoints_data) if !endpoints_data.nil? - CustomWizard::Api::Authorization.set(service, @auth_data) + if !service_auth_data.nil? + CustomWizard::Api::Authorization.set(service, service_auth_data) + end - @endpoint_data.each do |endpoint| - CustomWizard::Api::Endpoint.set(service, endpoint) + if !service_endpoints.nil? + service_endpoints.each do |endpoint| + CustomWizard::Api::Endpoint.set(service, endpoint) + end end render json: success_json.merge( From 6345083c4d7041a96dd3a6889acad9b778e3632b Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Sat, 1 Jun 2019 12:25:09 +1000 Subject: [PATCH 15/31] initialize auth params and endpoints in the route & model --- .../discourse/controllers/admin-wizards-api.js.es6 | 6 ------ .../javascripts/discourse/models/custom-wizard-api.js.es6 | 4 ++-- .../javascripts/discourse/routes/admin-wizards-api.js.es6 | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 3787c60e..ec4b5170 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -11,9 +11,6 @@ export default Ember.Controller.extend({ actions: { addParam() { - if (this.get('api.authParams') == undefined) { - this.set('api.authParams',[]); - }; this.get('api.authParams').pushObject({}); }, @@ -22,9 +19,6 @@ export default Ember.Controller.extend({ }, addEndpoint() { - if (this.get('api.endpoints') == undefined) { - this.set('api.endpoints',[]); - }; this.get('api.endpoints').pushObject({}); }, diff --git a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 index 6cfbae06..6817ae70 100644 --- a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 @@ -10,9 +10,9 @@ const CustomWizardApi = Discourse.Model.extend({ }); CustomWizardApi.reopenClass({ - create(params) { + create(params = {}) { const api = this._super.apply(this); - const authorization = params.authorization; + const authorization = params.authorization || {}; const endpoints = params.endpoints; api.setProperties({ diff --git a/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 index 34320924..dda7d416 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 @@ -3,7 +3,7 @@ import CustomWizardApi from '../models/custom-wizard-api'; export default Discourse.Route.extend({ model(params) { if (params.service === 'new') { - return {}; + return CustomWizardApi.create(); } else { return CustomWizardApi.find(params.service); } From f331f80cbbe3003fa254a1d87519268c20ec11c8 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Sun, 2 Jun 2019 20:54:31 +1000 Subject: [PATCH 16/31] Update wizard api CRUD - New api metadata model - New api id system - Minor UI updates --- .travis.yml | 2 +- .../controllers/admin-wizards-api.js.es6 | 76 +++++++-- .../controllers/admin-wizards-apis.js.es6 | 3 + .../custom-wizard-admin-route-map.js.es6 | 2 +- .../discourse/models/custom-wizard-api.js.es6 | 17 +- .../discourse/routes/admin-wizards-api.js.es6 | 12 +- .../routes/admin-wizards-apis.js.es6 | 20 +++ .../discourse/templates/admin-wizards-api.hbs | 157 ++++++++++-------- .../templates/admin-wizards-apis.hbs | 8 +- assets/stylesheets/wizard_custom_admin.scss | 44 ++++- config/locales/client.en.yml | 57 ++++--- controllers/api.rb | 95 +++++++---- jobs/refresh_api_access_token.rb | 2 +- lib/api/api.rb | 26 ++- lib/api/authorization.rb | 67 ++++---- lib/api/endpoint.rb | 34 ++-- plugin.rb | 7 +- serializers/api/api_serializer.rb | 23 ++- serializers/api/basic_api_serializer.rb | 2 +- 19 files changed, 446 insertions(+), 208 deletions(-) create mode 100644 assets/javascripts/discourse/controllers/admin-wizards-apis.js.es6 diff --git a/.travis.yml b/.travis.yml index 01ff46d8..b48ce894 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ # Uncomment tests runner when tests are added. sudo: required -#services: +#names: #- docker before_install: diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index ec4b5170..832bf1c4 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -1,13 +1,32 @@ import { ajax } from 'discourse/lib/ajax'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import CustomWizardApi from '../models/custom-wizard-api'; +import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import DiscourseURL from 'discourse/lib/url'; export default Ember.Controller.extend({ + queryParams: ['refresh_list'], loadingSubscriptions: false, notAuthorized: Ember.computed.not('api.authorized'), authorizationTypes: ['oauth', 'basic'], isOauth: Ember.computed.equal('api.authType', 'oauth'), endpointMethods: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'], + saveDisabled: Ember.computed.empty('api.name'), + showRemove: Ember.computed.not('isNew'), + + @computed('saveDisabled', 'authType', 'authUrl') + authDisabled(saveDisabled, authType, authUrl) { + return saveDisabled || !authType || !authUrl; + }, + + @observes('api.title') + titleWatcher() { + const title = this.get('api.title'); + + if (this.get('originalTitle')) { + this.set('originalTitle', title); + } + }, actions: { addParam() { @@ -48,12 +67,25 @@ export default Ember.Controller.extend({ save() { const api = this.get('api'); - const service = api.service; + const name = api.name; + let refreshList = false; - let data = {}; + if (!name ) return; - data['auth_type'] = api.authType; - data['auth_url'] = api.authUrl; + let data = { + title: api.title + }; + + if (this.get('isNew') || (api.title !== this.get('originalTitle'))) { + refreshList = true; + } + + if (api.get('isNew')) { + data['new'] = true; + }; + + if (api.authType) data['auth_type'] = api.authType; + if (api.authUrl) data['auth_url'] = api.authUrl; if (data.auth_type === 'oauth') { data['client_id'] = api.clientId; @@ -72,23 +104,43 @@ export default Ember.Controller.extend({ } const endpoints = api.endpoints; - if (endpoints != undefined) { - if (endpoints.length) { - data['endpoints'] = JSON.stringify(endpoints); - } + if (endpoints.length) { + data['endpoints'] = JSON.stringify(endpoints); } - this.set('savingApi', true); + this.set('updating', true); - ajax(`/admin/wizards/apis/${service}`, { + ajax(`/admin/wizards/apis/${name.underscore()}`, { type: 'PUT', data }).catch(popupAjaxError) .then(result => { if (result.success) { - this.set('api', CustomWizardApi.create(result.api)); + if (refreshList) { + this.transitionToRoute('adminWizardsApi', result.api.name.dasherize()).then(() => { + this.send('refreshModel'); + }); + } else { + this.set('api', CustomWizardApi.create(result.api)); + } } - }).finally(() => this.set('savingApi', false)); + }).finally(() => this.set('updating', false)); + }, + + remove() { + const name = this.get('api.name'); + if (!name) return; + + this.set('updating', true); + + ajax(`/admin/wizards/apis/${name.underscore()}`, { + type: 'DELETE' + }).catch(popupAjaxError) + .then(result => { + if (result.success) { + DiscourseURL.routeTo('/admin/wizards/apis?refresh=true'); + } + }).finally(() => this.set('updating', false)); } } }); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-apis.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-apis.js.es6 new file mode 100644 index 00000000..52748bc8 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-apis.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Controller.extend({ + queryParams: ['refresh'] +}); diff --git a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 index dfe53a82..f5022153 100644 --- a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 +++ b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 @@ -9,7 +9,7 @@ export default { this.route('adminWizardSubmissions', { path: '/:wizard_id', resetNamespace: true }); }); this.route('adminWizardsApis', { path: '/apis', resetNamespace: true }, function() { - this.route('adminWizardsApi', { path: '/:service', resetNamespace: true }); + this.route('adminWizardsApi', { path: '/:name', resetNamespace: true }); }); }); } diff --git a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 index 6817ae70..5f051cc9 100644 --- a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 @@ -2,10 +2,11 @@ import { ajax } from 'discourse/lib/ajax'; import { default as computed } from 'ember-addons/ember-computed-decorators'; const CustomWizardApi = Discourse.Model.extend({ - @computed('service') - redirectUri(service) { + @computed('name') + redirectUri(name) { + let nameParam = name.toString().dasherize(); const baseUrl = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: ''); - return baseUrl + `/admin/wizards/apis/${service}/redirect`; + return baseUrl + `/admin/wizards/apis/${nameParam}/redirect`; } }); @@ -16,7 +17,8 @@ CustomWizardApi.reopenClass({ const endpoints = params.endpoints; api.setProperties({ - service: params.service, + name: params.name, + title: params.title, authType: authorization.auth_type, authUrl: authorization.auth_url, tokenUrl: authorization.token_url, @@ -29,14 +31,15 @@ CustomWizardApi.reopenClass({ code: authorization.code, tokenExpiresAt: authorization.token_expires_at, tokenRefreshAt: authorization.token_refresh_at, - endpoints: Ember.A(endpoints) + endpoints: Ember.A(endpoints), + isNew: params.isNew }); return api; }, - find(service) { - return ajax(`/admin/wizards/apis/${service}`, { + find(name) { + return ajax(`/admin/wizards/apis/${name}`, { type: 'GET' }).then(result => { return CustomWizardApi.create(result); diff --git a/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 index dda7d416..58f624b5 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6 @@ -1,11 +1,17 @@ import CustomWizardApi from '../models/custom-wizard-api'; export default Discourse.Route.extend({ + queryParams: { + refresh_list: { + refreshModel: true + } + }, + model(params) { - if (params.service === 'new') { - return CustomWizardApi.create(); + if (params.name === 'new') { + return CustomWizardApi.create({ isNew: true }); } else { - return CustomWizardApi.find(params.service); + return CustomWizardApi.find(params.name); } }, diff --git a/assets/javascripts/discourse/routes/admin-wizards-apis.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-apis.js.es6 index b7e89e10..54174c6b 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-apis.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-apis.js.es6 @@ -5,7 +5,27 @@ export default Discourse.Route.extend({ return CustomWizardApi.list(); }, + afterModel(model) { + const apiParams = this.paramsFor('admin-wizards-api'); + + if (model.length) { + if (!apiParams.name) { + this.transitionTo('adminWizardsApi', model[0].name.dasherize()); + } else { + return; + } + } else { + this.transitionTo('adminWizardsApi', 'new'); + } + }, + setupController(controller, model){ controller.set("model", model); + }, + + actions: { + refreshModel() { + this.refresh(); + } } }); diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index f91a4c3f..a17a0c0b 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -1,51 +1,77 @@ -
-
- {{input value=api.service placeholder=(i18n 'admin.wizard.api.service')}} +
+
- {{#if savingApi}} + {{#if updating}} {{loading-spinner size="small"}} {{/if}} - {{d-button label="admin.wizard.api.save" action="save" class="btn-primary"}} - {{d-button action="removeApi" label="admin.wizard.api.remove"}} + + {{d-button label="admin.wizard.api.save" action="save" class="btn-primary" disabled=saveDisabled}} + + {{#if showRemove}} + {{d-button action="remove" label="admin.wizard.api.remove"}} + {{/if}}
-
- {{i18n 'admin.wizard.api.auth'}} +
+
+ {{i18n 'admin.wizard.api.auth.label'}} +
+ +
+ {{d-button label="admin.wizard.api.auth.btn" action="authorize" disabled=authDisabled class="btn-primary"}} +
- {{i18n 'admin.wizard.api.auth_settings'}} + {{i18n 'admin.wizard.api.auth.settings'}}
-
-
- {{i18n 'admin.wizard.api.redirect_uri'}} - {{api.redirectUri}} + {{#if api.name}} +
+
+ +
+ {{api.redirectUri}} +
+
-
+ {{/if}} -
- +
+
- {{combo-box value=api.authType content=authorizationTypes none='admin.wizard.api.auth_type_none'}} + {{combo-box value=api.authType content=authorizationTypes none='admin.wizard.api.auth.type_none'}}
- +
{{input value=api.authUrl}}
- +
{{input value=api.tokenUrl}}
@@ -53,85 +79,82 @@ {{#if isOauth}}
- +
{{input value=api.clientId}}
- +
{{input value=api.clientSecret}}
- +
{{#each api.authParams as |param|}}
- {{input value=param.key placeholder=(i18n 'admin.wizard.api.param_key')}} - {{input value=param.value placeholder=(i18n 'admin.wizard.api.param_value')}} + {{input value=param.key placeholder=(i18n 'admin.wizard.api.auth.params.key')}} + {{input value=param.value placeholder=(i18n 'admin.wizard.api.auth.params.value')}} {{d-button action='removeParam' actionParam=param icon='times'}}
{{/each}} - {{d-button label='admin.wizard.api.param_new' icon='plus' action='addParam'}} + {{d-button label='admin.wizard.api.auth.params.new' icon='plus' action='addParam'}}
{{/if}} - -
- {{d-button label="admin.wizard.api.authorize" action="authorize"}} -
-
- {{i18n 'admin.wizard.api.auth_status'}} -
-
{{#if api.authorized}} - {{i18n "admin.wizard.api.authorized"}} + {{i18n "admin.wizard.api.status.authorized"}} {{else}} - {{i18n "admin.wizard.api.not_authorized"}} + {{i18n "admin.wizard.api.status.not_authorized"}} {{/if}}
+ +
+ {{i18n 'admin.wizard.api.status.label'}} +
+
- + +
+ {{api.code}} +
+
+ +
+
{{api.accessToken}}
- -
- {{api.tokenExpiresAt}} -
-
- -
- -
- {{api.tokenRefreshAt}} -
-
- -
- +
{{api.refreshToken}}
- +
- {{api.code}} + {{api.tokenExpiresAt}} +
+
+ +
+ +
+ {{api.tokenRefreshAt}}
@@ -144,17 +167,19 @@
{{d-button action='addEndpoint' label='admin.wizard.api.endpoint.add' icon='plus'}} -
-
    - {{#each api.endpoints as |endpoint|}} -
  • -
    - {{combo-box content=endpointMethods value=endpoint.method none="admin.wizard.api.endpoint.method"}} - {{input value=endpoint.url placeholder=(i18n 'admin.wizard.api.endpoint.url') class='endpoint-url'}} - {{d-button action='removeEndpoint' actionParam=endpoint icon='times' class='remove-endpoint'}} -
    -
  • - {{/each}} -
-
+ {{#if api.endpoints}} +
+
    + {{#each api.endpoints as |endpoint|}} +
  • +
    + {{combo-box content=endpointMethods value=endpoint.method none="admin.wizard.api.endpoint.method"}} + {{input value=endpoint.url placeholder=(i18n 'admin.wizard.api.endpoint.url') class='endpoint-url'}} + {{d-button action='removeEndpoint' actionParam=endpoint icon='times' class='remove-endpoint'}} +
    +
  • + {{/each}} +
+
+ {{/if}}
diff --git a/assets/javascripts/discourse/templates/admin-wizards-apis.hbs b/assets/javascripts/discourse/templates/admin-wizards-apis.hbs index 78a5c954..c5f67660 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-apis.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-apis.hbs @@ -3,7 +3,13 @@
    {{#each model as |api|}}
  • - {{#link-to "adminWizardsApi" api.service}}{{api.service}}{{/link-to}} + {{#link-to "adminWizardsApi" (dasherize api.name)}} + {{#if api.title}} + {{api.title}} + {{else}} + {{api.name}} + {{/if}} + {{/link-to}}
  • {{/each}}
diff --git a/assets/stylesheets/wizard_custom_admin.scss b/assets/stylesheets/wizard_custom_admin.scss index 7bec453f..8026525c 100644 --- a/assets/stylesheets/wizard_custom_admin.scss +++ b/assets/stylesheets/wizard_custom_admin.scss @@ -308,12 +308,23 @@ .content-list { margin-right: 20px; } + + .new-api { + margin-top: 20px; + } + + .metadata .title input { + width: 400px; + } } .wizard-api-header { display: flex; justify-content: space-between; - margin-bottom: 20px; + + &.page { + margin-bottom: 20px; + } } .wizard-api-authentication { @@ -324,8 +335,37 @@ .settings { border-right: 1px solid #333; - margin-right: 10px; + margin-right: 20px; padding-right: 20px; + width: 50%; + max-width: 50%; + } + + .redirect-uri .controls { + word-break: break-all; + } + + .auth-type .select-kit { + min-width: 210px; + width: 210px; + margin-bottom: 10px; + } + + .status { + width: 50%; + max-width: 50%; + + .wizard-header { + overflow: hidden; + } + + .authorization { + float: right; + } + + .control-group { + margin-bottom: 15px; + } } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0df2b330..c14795d7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -127,36 +127,47 @@ en: wizard_field: "Wizard Field" user_field: "User Field" api: + label: "API" nav_label: 'APIs' new: 'New Api' - - auth: "Authentication" - auth_settings: "Settings" - auth_status: "Status" - service: 'Api Service' - redirect_uri: "Redirect Uri" - auth_type: 'Authorization Type' - auth_type_none: 'Select a type' - token_url: "Token Url" - auth_url: 'Authorization Url' - client_id: 'Client Id' - client_secret: 'Client Secret' - - status: "Status" - authorized: 'Authorized' - not_authorized: "Not Authorized" - params: 'Params' - param_new: 'New Param' - param_key: 'Param Key' - param_value: 'Param Value' - + name: "Name (can't be changed)" + name_placeholder: 'Underscored' + title: 'Title' + title_placeholder: 'Display name' remove: 'Delete' - authorize: 'Authorize' save: "Save" + auth: + label: "Authorization" + btn: 'Authorize' + settings: "Settings" + status: "Status" + redirect_uri: "Redirect url" + type: 'Type' + type_none: 'Select a type' + url: "Authorization url" + token_url: "Token url" + client_id: 'Client id' + client_secret: 'Client secret' + params: + label: 'Params' + new: 'New param' + key: 'key' + value: 'value' + + status: + label: "Status" + authorized: 'Authorized' + not_authorized: "Not authorized" + code: "Code" + access_token: "Access token" + refresh_token: "Refresh token" + expires_at: "Expires at" + refresh_at: "Refresh at" + endpoint: label: "Endpoints" - add: "Add Endpoint" + add: "Add endpoint" method: "Select a method" url: "Enter a url" diff --git a/controllers/api.rb b/controllers/api.rb index 059063ca..5d8ba61b 100644 --- a/controllers/api.rb +++ b/controllers/api.rb @@ -11,55 +11,75 @@ class CustomWizard::ApiController < ::ApplicationController CustomWizard::Api.list, each_serializer: CustomWizard::BasicApiSerializer ) - render json: MultiJson.dump(serializer) end def find - params.require(:service) - render_serialized(CustomWizard::Api.new(params[:service]), CustomWizard::ApiSerializer, root: false) + render_serialized(CustomWizard::Api.get(api_params[:name]), CustomWizard::ApiSerializer, root: false) end def save - byebug - params.require(:service) - service = params.permit(:service) - auth_data = params[:auth_params] - endpoints_data = params[:endpoints] + current = CustomWizard::Api.get(api_params[:name]) - service_auth_data = JSON.parse(auth_data) if !auth_data.nil? - service_endpoints = JSON.parse(endpoints_data) if !endpoints_data.nil? - - if !service_auth_data.nil? - CustomWizard::Api::Authorization.set(service, service_auth_data) + if api_params[:new] && current + raise Discourse::InvalidParameters, "An API with that name already exists: '#{current.title || current.name}'" end - if !service_endpoints.nil? - service_endpoints.each do |endpoint| - CustomWizard::Api::Endpoint.set(service, endpoint) + PluginStoreRow.transaction do + CustomWizard::Api.set(api_params[:name], title: api_params[:title]) + + if auth_data.present? + CustomWizard::Api::Authorization.set(api_params[:name], auth_data) + end + + if api_params[:endpoints].is_a? String + begin + endpoints = JSON.parse(api_params[:endpoints]) + endpoints.each do |endpoint| + CustomWizard::Api::Endpoint.set(api_params[:name], endpoint) + end + rescue => e + puts e + end end end render json: success_json.merge( - api: CustomWizard::ApiSerializer.new(params[:service], root: false) + api: CustomWizard::ApiSerializer.new( + CustomWizard::Api.new(api_params[:name]), + root: false + ) ) end + def remove + PluginStoreRow.transaction do + CustomWizard::Api.remove(api_params[:name]) + CustomWizard::Api::Authorization.remove(api_params[:name]) + CustomWizard::Api::Endpoint.remove(api_params[:name]) + end + + render json: success_json + end + def redirect - params.require(:service) - params.require(:code) + params.require(:name) + params.require(:code) - CustomWizard::Api::Authorization.set(params[:service], code: params[:code]) + CustomWizard::Api::Authorization.set(params[:name], code: params[:code]) + CustomWizard::Api::Authorization.get_token(params[:name]) - CustomWizard::Api::Authorization.get_token(params[:service]) - - return redirect_to path('/admin/wizards/apis/' + params[:service]) + return redirect_to path('/admin/wizards/apis/' + params[:name]) end private - def auth_data - @auth_data ||= params.permit( + def api_params + params.require(:name) + + data = params.permit( + :name, + :title, :auth_type, :auth_url, :token_url, @@ -67,11 +87,30 @@ class CustomWizard::ApiController < ::ApplicationController :client_secret, :username, :password, - :auth_params + :auth_params, + :endpoints, + :new ).to_h + + data[:name] = data[:name].underscore + + @api_params ||= data end - def endpoint_data - @endpoint_data ||= JSON.parse(params.permit(endpoints: [:id, :type, :url])) + def auth_data + auth_data = api_params.slice( + :auth_type, + :auth_url, + :token_url, + :client_id, + :client_secret, + :username, + :password, + :params + ) + + auth_data[:params] = JSON.parse(auth_data[:params]) if auth_data[:params].present? + + @auth_data ||= auth_data end end diff --git a/jobs/refresh_api_access_token.rb b/jobs/refresh_api_access_token.rb index c7fb94e5..edea0052 100644 --- a/jobs/refresh_api_access_token.rb +++ b/jobs/refresh_api_access_token.rb @@ -1,7 +1,7 @@ module Jobs class RefreshApiAccessToken < Jobs::Base def execute(args) - CustomWizard::Api::Authorization.refresh_token(args[:service]) + CustomWizard::Api::Authorization.refresh_token(args[:name]) end end end diff --git a/lib/api/api.rb b/lib/api/api.rb index dde491d4..2a614c6b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,16 +1,32 @@ class CustomWizard::Api include ActiveModel::SerializerSupport - attr_accessor :service + attr_accessor :name, + :title - def initialize(service) - @service = service + def initialize(name, data={}) + @name = name + @title = data['title'] + end + + def self.set(name, data) + PluginStore.set("custom_wizard_api_#{name}", "metadata", data) + end + + def self.get(name) + if data = PluginStore.get("custom_wizard_api_#{name}", "metadata") + self.new(name, data) + end + end + + def self.remove(name) + PluginStore.remove("custom_wizard_api_#{name}", "metadata") end def self.list - PluginStoreRow.where("plugin_name LIKE 'custom_wizard_api_%' AND key = 'authorization'") + PluginStoreRow.where("plugin_name LIKE 'custom_wizard_api_%' AND key = 'metadata'") .map do |record| - self.new(record['plugin_name'].split('_').last) + self.new(record['plugin_name'].sub("custom_wizard_api_", ""), ::JSON.parse(record['value'])) end end end diff --git a/lib/api/authorization.rb b/lib/api/authorization.rb index 9ae5a211..35be314a 100644 --- a/lib/api/authorization.rb +++ b/lib/api/authorization.rb @@ -4,7 +4,7 @@ class CustomWizard::Api::Authorization include ActiveModel::SerializerSupport attr_accessor :authorized, - :service, + :name, :auth_type, :auth_url, :token_url, @@ -19,12 +19,15 @@ class CustomWizard::Api::Authorization :username, :password - def initialize(service, params) - @service = service - data = params.is_a?(String) ? ::JSON.parse(params) : params + def initialize(name, data, opts = {}) + unless opts[:data_only] + @name = name + end - data.each do |k, v| - self.send "#{k}=", v if self.respond_to?(k) + if data = data.is_a?(String) ? ::JSON.parse(data) : data + data.each do |k, v| + self.send "#{k}=", v if self.respond_to?(k) + end end end @@ -32,52 +35,56 @@ class CustomWizard::Api::Authorization @authorized ||= @access_token && @token_expires_at.to_datetime > Time.now end - def self.set(service, data) - model = self.get(service) || {} + def self.set(name, data = {}) + record = self.get(name, data_only: true) data.each do |k, v| - model.send "#{k}=", v if model.respond_to?(k) + record.send "#{k}=", v if record.respond_to?(k) end - PluginStore.set("custom_wizard_api_#{service}", 'authorization', model.as_json) + PluginStore.set("custom_wizard_api_#{name}", 'authorization', record.as_json) - self.get(service) + self.get(name) end - def self.get(service) - data = PluginStore.get("custom_wizard_api_#{service}", 'authorization') - self.new(service, data) + def self.get(name, opts = {}) + data = PluginStore.get("custom_wizard_api_#{name}", 'authorization') + self.new(name, data, opts) end - def self.get_header_authorization_string(service) - protocol = authentication_protocol(service) - raise Discourse::InvalidParameters.new(:service) unless protocol.present? + def self.remove(name) + PluginStore.remove("custom_wizard_api_#{name}", "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 if protocol = BASIC_AUTH - username = username(service) + username = username(name) raise Discourse::InvalidParameters.new(:username) unless username.present? - password = password(service) + password = password(name) raise Discourse::InvalidParameters.new(:password) unless password.present? authorization_string = (username + ":" + password).chomp "Basic #{Base64.strict_encode64(authorization_string)}" else # must be OAUTH2 - access_token = access_token(service) + access_token = access_token(name) raise Discourse::InvalidParameters.new(access_token) unless access_token.present? "Bearer #{access_token}" end end - def self.get_token(service) - authorization = CustomWizard::Api::Authorization.get(service) + def self.get_token(name) + authorization = CustomWizard::Api::Authorization.get(name) body = { client_id: authorization.client_id, client_secret: authorization.client_secret, code: authorization.code, grant_type: 'authorization_code', - redirect_uri: Discourse.base_url + "/admin/wizards/apis/#{service}/redirect" + redirect_uri: Discourse.base_url + "/admin/wizards/apis/#{name}/redirect" } result = Excon.post( @@ -88,11 +95,11 @@ class CustomWizard::Api::Authorization :body => URI.encode_www_form(body) ) - self.handle_token_result(service, result) + self.handle_token_result(name, result) end - def self.refresh_token(service) - authorization = CustomWizard::Api::Authorization.get(service) + def self.refresh_token(name) + authorization = CustomWizard::Api::Authorization.get(name) body = { grant_type: 'refresh_token', @@ -110,10 +117,10 @@ class CustomWizard::Api::Authorization :body => URI.encode_www_form(body) ) - self.handle_token_result(service, result) + self.handle_token_result(name, result) end - def self.handle_token_result(service, result) + def self.handle_token_result(name, result) data = JSON.parse(result.body) return false if (data['error']) @@ -124,12 +131,12 @@ class CustomWizard::Api::Authorization refresh_at = expires_at.to_time - 2.hours opts = { - service: service + name: name } Jobs.enqueue_at(refresh_at, :refresh_api_access_token, opts) - CustomWizard::Api::Authorization.set(service, + CustomWizard::Api::Authorization.set(name, access_token: access_token, refresh_token: refresh_token, token_expires_at: expires_at, diff --git a/lib/api/endpoint.rb b/lib/api/endpoint.rb index b1e40e69..340a7e35 100644 --- a/lib/api/endpoint.rb +++ b/lib/api/endpoint.rb @@ -5,42 +5,46 @@ class CustomWizard::Api::Endpoint :method, :url - def initialize(service, params) - @service = service - data = params.is_a?(String) ? ::JSON.parse(params) : params - - data.each do |k, v| - self.send "#{k}=", v if self.respond_to?(k) + def initialize(name, params) + @name = name + if data = params.is_a?(String) ? ::JSON.parse(params) : params + data.each do |k, v| + self.send "#{k}=", v if self.respond_to?(k) + end end end - def self.set(service, data) - model = data[:endpoint_id] ? self.get(service, data[:endpoint_id]) : {} + def self.set(name, data) + model = data[:endpoint_id] ? self.get(name, data[:endpoint_id]) : {} endpoint_id = model[:endpoint_id] || SecureRandom.hex(8) data.each do |k, v| model.send "#{k}=", v if model.respond_to?(k) end - PluginStore.set("custom_wizard_api_#{service}", "endpoint_#{endpoint_id}", model.as_json) + PluginStore.set("custom_wizard_api_#{name}", "endpoint_#{endpoint_id}", model.as_json) - self.get(service) + self.get(name) end - def self.get(service, endpoint_id) + def self.get(name, endpoint_id) return nil if !endpoint_id - data = PluginStore.get("custom_wizard_api_#{service}", "endpoint_#{endpoint_id}") + data = PluginStore.get("custom_wizard_api_#{name}", "endpoint_#{endpoint_id}") data[:id] = endpoint_id - self.new(service, data) + self.new(name, data) + end + + def self.remove(name) + PluginStoreRow.where("plugin_name = 'custom_wizard_api_#{name}' AND key LIKE 'endpoint_%'").destroy_all end def self.list PluginStoreRow.where("plugin_name LIKE 'custom_wizard_api_%' AND key LIKE 'endpoint_%'") .map do |record| - service = record['plugin_name'].split('_').last + name = record['plugin_name'].sub("custom_wizard_api_", "") data = ::JSON.parse(record['value']) data[:id] = record['key'].split('_').last - self.new(service, data) + self.new(name, data) end end end diff --git a/plugin.rb b/plugin.rb index d35d929f..f0d7cfc6 100644 --- a/plugin.rb +++ b/plugin.rb @@ -69,9 +69,10 @@ after_initialize do get 'admin/wizards/submissions/:wizard_id' => 'admin#submissions' get 'admin/wizards/apis' => 'api#list' get 'admin/wizards/apis/new' => 'api#index' - get 'admin/wizards/apis/:service' => 'api#find' - put 'admin/wizards/apis/:service' => 'api#save' - get 'admin/wizards/apis/:service/redirect' => 'api#redirect' + get 'admin/wizards/apis/:name' => 'api#find' + put 'admin/wizards/apis/:name' => 'api#save' + delete 'admin/wizards/apis/:name' => 'api#remove' + get 'admin/wizards/apis/:name/redirect' => 'api#redirect' end end diff --git a/serializers/api/api_serializer.rb b/serializers/api/api_serializer.rb index aae825c2..0856b461 100644 --- a/serializers/api/api_serializer.rb +++ b/serializers/api/api_serializer.rb @@ -1,19 +1,24 @@ class CustomWizard::ApiSerializer < ApplicationSerializer - attributes :service, + attributes :name, + :title, :authorization, :endpoints def authorization - CustomWizard::Api::AuthorizationSerializer.new( - CustomWizard::Api::Authorization.get(object.service), - root: false - ) + if authorization = CustomWizard::Api::Authorization.get(object.name) + CustomWizard::Api::AuthorizationSerializer.new( + authorization, + root: false + ) + end end def endpoints - ActiveModel::ArraySerializer.new( - CustomWizard::Api::Endpoint.list, - each_serializer: CustomWizard::Api::EndpointSerializer - ) + if endpoints = CustomWizard::Api::Endpoint.list + ActiveModel::ArraySerializer.new( + endpoints, + each_serializer: CustomWizard::Api::EndpointSerializer + ) + end end end diff --git a/serializers/api/basic_api_serializer.rb b/serializers/api/basic_api_serializer.rb index b5430897..6f583f7d 100644 --- a/serializers/api/basic_api_serializer.rb +++ b/serializers/api/basic_api_serializer.rb @@ -1,3 +1,3 @@ class CustomWizard::BasicApiSerializer < ApplicationSerializer - attributes :service + attributes :name, :title end From cc98a14d1542928659187059ca95d80db96b5bc7 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Sun, 2 Jun 2019 23:59:35 +0100 Subject: [PATCH 17/31] added basic auth interface changes and supporting code --- .../controllers/admin-wizards-api.js.es6 | 3 +- .../discourse/models/custom-wizard-api.js.es6 | 2 + .../discourse/templates/admin-wizards-api.hbs | 152 ++++++++++-------- config/locales/client.en.yml | 2 + 4 files changed, 91 insertions(+), 68 deletions(-) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 832bf1c4..2457a917 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -59,7 +59,7 @@ export default Ember.Controller.extend({ }); } } else { - // basic auth + // basic auth - no need to authorize separately } window.location.href = authUrl + query; @@ -104,6 +104,7 @@ export default Ember.Controller.extend({ } const endpoints = api.endpoints; + if (endpoints.length) { data['endpoints'] = JSON.stringify(endpoints); } diff --git a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 index 5f051cc9..726cebe0 100644 --- a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 @@ -24,6 +24,8 @@ CustomWizardApi.reopenClass({ tokenUrl: authorization.token_url, clientId: authorization.client_id, clientSecret: authorization.client_secret, + username: authorization.username, + password: authorization.password, authParams: Ember.A(authorization.auth_params), authorized: authorization.authorized, accessToken: authorization.access_token, diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index a17a0c0b..9d929985 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -45,15 +45,17 @@ {{i18n 'admin.wizard.api.auth.settings'}}
- {{#if api.name}} -
-
- -
- {{api.redirectUri}} + {{#if isOauth}} + {{#if api.name}} +
+
+ +
+ {{api.redirectUri}} +
-
+ {{/if}} {{/if}}
@@ -63,21 +65,21 @@
-
- -
- {{input value=api.authUrl}} -
-
- -
- -
- {{input value=api.tokenUrl}} -
-
- {{#if isOauth}} +
+ +
+ {{input value=api.authUrl}} +
+
+ +
+ +
+ {{input value=api.tokenUrl}} +
+
+
@@ -105,59 +107,75 @@ {{d-button label='admin.wizard.api.auth.params.new' icon='plus' action='addParam'}}
+ {{else}} +
+ +
+ {{input value=api.username}} +
+
+ +
+ +
+ {{input value=api.password}} +
+
{{/if}}
-
-
- {{#if api.authorized}} - - {{i18n "admin.wizard.api.status.authorized"}} - {{else}} - - {{i18n "admin.wizard.api.status.not_authorized"}} - {{/if}} -
+ {{#if isOauth}} +
+
+ {{#if api.authorized}} + + {{i18n "admin.wizard.api.status.authorized"}} + {{else}} + + {{i18n "admin.wizard.api.status.not_authorized"}} + {{/if}} +
-
- {{i18n 'admin.wizard.api.status.label'}} -
+
+ {{i18n 'admin.wizard.api.status.label'}} +
-
- -
- {{api.code}} +
+ +
+ {{api.code}} +
+
+ +
+ +
+ {{api.accessToken}} +
+
+ +
+ +
+ {{api.refreshToken}} +
+
+ +
+ +
+ {{api.tokenExpiresAt}} +
+
+ +
+ +
+ {{api.tokenRefreshAt}} +
- -
- -
- {{api.accessToken}} -
-
- -
- -
- {{api.refreshToken}} -
-
- -
- -
- {{api.tokenExpiresAt}} -
-
- -
- -
- {{api.tokenRefreshAt}} -
-
-
+ {{/if}}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c14795d7..4bdbd5f9 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -149,6 +149,8 @@ en: token_url: "Token url" client_id: 'Client id' client_secret: 'Client secret' + username: 'username' + password: 'password' params: label: 'Params' new: 'New param' From 22d1c6fd064ccdcf18798f0014489f79caeea5a8 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 3 Jun 2019 09:40:54 +1000 Subject: [PATCH 18/31] API Admin client-side cleanups --- .../controllers/admin-wizards-api.js.es6 | 47 +++++++++++-------- .../discourse/templates/admin-wizards-api.hbs | 11 ++++- assets/stylesheets/wizard_custom_admin.scss | 7 +-- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 2457a917..0fae7419 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -10,15 +10,20 @@ export default Ember.Controller.extend({ notAuthorized: Ember.computed.not('api.authorized'), authorizationTypes: ['oauth', 'basic'], isOauth: Ember.computed.equal('api.authType', 'oauth'), + isBasicAuth: Ember.computed.equal('api.authType', 'basic'), endpointMethods: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'], - saveDisabled: Ember.computed.empty('api.name'), showRemove: Ember.computed.not('isNew'), - @computed('saveDisabled', 'authType', 'authUrl') + @computed('saveDisabled', 'api.authType', 'api.authUrl') authDisabled(saveDisabled, authType, authUrl) { return saveDisabled || !authType || !authUrl; }, + @computed('api.name', 'api.authType') + saveDisabled(name, authType) { + return !name || !authType; + }, + @observes('api.title') titleWatcher() { const title = this.get('api.title'); @@ -48,18 +53,19 @@ export default Ember.Controller.extend({ authorize() { const api = this.get('api'); const { authType, authUrl, authParams } = api; + + if (authType !== 'oauth') return; + let query = '?'; - if (authType === 'oauth') { - query += `client_id=${api.clientId}&redirect_uri=${encodeURIComponent(api.redirectUri)}&response_type=code`; + query += `client_id=${api.clientId}`; + query += `&redirect_uri=${encodeURIComponent(api.redirectUri)}`; + query += `&response_type=code`; - if (authParams) { - authParams.forEach(p => { - query += `&${p.key}=${encodeURIComponent(p.value)}`; - }); - } - } else { - // basic auth - no need to authorize separately + if (authParams) { + authParams.forEach(p => { + query += `&${p.key}=${encodeURIComponent(p.value)}`; + }); } window.location.href = authUrl + query; @@ -68,15 +74,18 @@ export default Ember.Controller.extend({ save() { const api = this.get('api'); const name = api.name; + const authType = api.authType; let refreshList = false; - if (!name ) return; + if (!name || !authType) return; let data = { - title: api.title + auth_type: authType }; - if (this.get('isNew') || (api.title !== this.get('originalTitle'))) { + if (api.title) data['title'] = api.title; + + if (api.get('isNew') || (api.title !== this.get('originalTitle'))) { refreshList = true; } @@ -84,10 +93,8 @@ export default Ember.Controller.extend({ data['new'] = true; }; - if (api.authType) data['auth_type'] = api.authType; - if (api.authUrl) data['auth_url'] = api.authUrl; - - if (data.auth_type === 'oauth') { + if (authType === 'oauth') { + data['auth_url'] = api.authUrl; data['client_id'] = api.clientId; data['client_secret'] = api.clientSecret; @@ -98,13 +105,13 @@ export default Ember.Controller.extend({ } data['token_url'] = api.tokenUrl; - } else { + } else if (authType === 'basic') { data['username'] = api.username; data['password'] = api.password; } const endpoints = api.endpoints; - + if (endpoints.length) { data['endpoints'] = JSON.stringify(endpoints); } diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index 9d929985..8ab9cf7c 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -34,7 +34,12 @@
- {{d-button label="admin.wizard.api.auth.btn" action="authorize" disabled=authDisabled class="btn-primary"}} + {{#if isOauth}} + {{d-button label="admin.wizard.api.auth.btn" + action="authorize" + disabled=authDisabled + class="btn-primary"}} + {{/if}}
@@ -107,7 +112,9 @@ {{d-button label='admin.wizard.api.auth.params.new' icon='plus' action='addParam'}}
- {{else}} + {{/if}} + + {{#if isBasicAuth}}
diff --git a/assets/stylesheets/wizard_custom_admin.scss b/assets/stylesheets/wizard_custom_admin.scss index 8026525c..68ecff6a 100644 --- a/assets/stylesheets/wizard_custom_admin.scss +++ b/assets/stylesheets/wizard_custom_admin.scss @@ -334,9 +334,6 @@ margin-bottom: 20px; .settings { - border-right: 1px solid #333; - margin-right: 20px; - padding-right: 20px; width: 50%; max-width: 50%; } @@ -352,6 +349,9 @@ } .status { + border-left: 1px solid $primary; + margin-left: 20px; + padding-left: 20px; width: 50%; max-width: 50%; @@ -384,6 +384,7 @@ .endpoint { display: flex; + margin-top: 20px; .combo-box { width: 200px; From 819c76b3ff3aa750c7ba5f34d772e79cea7349d8 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 3 Jun 2019 12:49:54 +1000 Subject: [PATCH 19/31] API data validation and administration improvements --- .../controllers/admin-wizards-api.js.es6 | 77 +++++++++++-------- .../discourse/models/custom-wizard-api.js.es6 | 1 + .../discourse/templates/admin-wizards-api.hbs | 77 +++++++++++++------ assets/stylesheets/wizard_custom_admin.scss | 37 +++++++-- config/locales/client.en.yml | 1 + controllers/api.rb | 2 +- lib/api/api.rb | 4 +- lib/api/authorization.rb | 45 ++++++----- lib/api/endpoint.rb | 56 ++++++++------ serializers/api/endpoint_serializer.rb | 7 +- 10 files changed, 200 insertions(+), 107 deletions(-) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 0fae7419..926aa751 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -1,8 +1,7 @@ import { ajax } from 'discourse/lib/ajax'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import CustomWizardApi from '../models/custom-wizard-api'; -import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; -import DiscourseURL from 'discourse/lib/url'; +import { default as computed } from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend({ queryParams: ['refresh_list'], @@ -13,10 +12,11 @@ export default Ember.Controller.extend({ isBasicAuth: Ember.computed.equal('api.authType', 'basic'), endpointMethods: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'], showRemove: Ember.computed.not('isNew'), + responseIcon: null, - @computed('saveDisabled', 'api.authType', 'api.authUrl') - authDisabled(saveDisabled, authType, authUrl) { - return saveDisabled || !authType || !authUrl; + @computed('saveDisabled', 'api.authType', 'api.authUrl', 'api.clientId', 'api.clientSecret') + authDisabled(saveDisabled, authType, authUrl, clientId, clientSecret) { + return saveDisabled || !authType || !authUrl || !clientId || !clientSecret; }, @computed('api.name', 'api.authType') @@ -24,15 +24,6 @@ export default Ember.Controller.extend({ return !name || !authType; }, - @observes('api.title') - titleWatcher() { - const title = this.get('api.title'); - - if (this.get('originalTitle')) { - this.set('originalTitle', title); - } - }, - actions: { addParam() { this.get('api.authParams').pushObject({}); @@ -76,6 +67,7 @@ export default Ember.Controller.extend({ const name = api.name; const authType = api.authType; let refreshList = false; + let error; if (!name || !authType) return; @@ -85,7 +77,9 @@ export default Ember.Controller.extend({ if (api.title) data['title'] = api.title; - if (api.get('isNew') || (api.title !== this.get('originalTitle'))) { + const originalTitle = this.get('api.originalTitle'); + console.log(api, originalTitle); + if (api.get('isNew') || (originalTitle && (api.title !== originalTitle))) { refreshList = true; } @@ -93,29 +87,47 @@ export default Ember.Controller.extend({ data['new'] = true; }; + let requiredParams; + if (authType === 'oauth') { - data['auth_url'] = api.authUrl; - data['client_id'] = api.clientId; - data['client_secret'] = api.clientSecret; - - let params = api.authParams; - - if (params) { - data['auth_params'] = JSON.stringify(params); - } - - data['token_url'] = api.tokenUrl; + requiredParams = ['authUrl', 'tokenUrl', 'clientId', 'clientSecret']; } else if (authType === 'basic') { - data['username'] = api.username; - data['password'] = api.password; + requiredParams = ['username', 'password']; + } + + for (let rp of requiredParams) { + if (!api[rp]) { + let key = rp.replace('auth', ''); + error = `${I18n.t(`admin.wizard.api.auth.${key.underscore()}`)} is required for ${authType}`; + break; + } + data[rp.underscore()] = api[rp]; + } + + const params = api.authParams; + if (params.length) { + data['auth_params'] = JSON.stringify(params); } const endpoints = api.endpoints; - if (endpoints.length) { + for (let e of endpoints) { + if (!e.name) { + error = 'Every endpoint must have a name'; + break; + } + } data['endpoints'] = JSON.stringify(endpoints); } + if (error) { + this.set('error', error); + setTimeout(() => { + this.set('error', ''); + }, 6000); + return; + } + this.set('updating', true); ajax(`/admin/wizards/apis/${name.underscore()}`, { @@ -130,7 +142,10 @@ export default Ember.Controller.extend({ }); } else { this.set('api', CustomWizardApi.create(result.api)); + this.set('responseIcon', 'check'); } + } else { + this.set('responseIcon', 'times'); } }).finally(() => this.set('updating', false)); }, @@ -146,7 +161,9 @@ export default Ember.Controller.extend({ }).catch(popupAjaxError) .then(result => { if (result.success) { - DiscourseURL.routeTo('/admin/wizards/apis?refresh=true'); + this.transitionToRoute('adminWizardsApis').then(() => { + this.send('refreshModel'); + }); } }).finally(() => this.set('updating', false)); } diff --git a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 index 726cebe0..791a153a 100644 --- a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 @@ -19,6 +19,7 @@ CustomWizardApi.reopenClass({ api.setProperties({ name: params.name, title: params.title, + originalTitle: params.title, authType: authorization.auth_type, authUrl: authorization.auth_url, tokenUrl: authorization.token_url, diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index 8ab9cf7c..0ed5766e 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -1,23 +1,11 @@
- -
{{#if updating}} {{loading-spinner size="small"}} + {{else}} + {{#if responseIcon}} + {{d-icon responseIcon}} + {{/if}} {{/if}} {{d-button label="admin.wizard.api.save" action="save" class="btn-primary" disabled=saveDisabled}} @@ -25,15 +13,41 @@ {{#if showRemove}} {{d-button action="remove" label="admin.wizard.api.remove"}} {{/if}} + + {{#if error}} +
+ {{error}} +
+ {{/if}} +
+ +
+ {{#if api.isNew}} + {{i18n 'admin.wizard.api.new'}} + {{else}} + {{api.title}} + {{/if}} +
+ +
-
- {{i18n 'admin.wizard.api.auth.label'}} -
- -
+
{{#if isOauth}} {{d-button label="admin.wizard.api.auth.btn" action="authorize" @@ -41,6 +55,10 @@ class="btn-primary"}} {{/if}}
+ +
+ {{i18n 'admin.wizard.api.auth.label'}} +
@@ -198,9 +216,20 @@ {{#each api.endpoints as |endpoint|}}
  • - {{combo-box content=endpointMethods value=endpoint.method none="admin.wizard.api.endpoint.method"}} - {{input value=endpoint.url placeholder=(i18n 'admin.wizard.api.endpoint.url') class='endpoint-url'}} - {{d-button action='removeEndpoint' actionParam=endpoint icon='times' class='remove-endpoint'}} +
    + {{input value=endpoint.name + placeholder=(i18n 'admin.wizard.api.endpoint.name')}} + {{combo-box content=endpointMethods + value=endpoint.method + none="admin.wizard.api.endpoint.method"}} + {{input value=endpoint.url + placeholder=(i18n 'admin.wizard.api.endpoint.url') + class='endpoint-url'}} + {{d-button action='removeEndpoint' + actionParam=endpoint + icon='times' + class='remove-endpoint'}} +
  • {{/each}} diff --git a/assets/stylesheets/wizard_custom_admin.scss b/assets/stylesheets/wizard_custom_admin.scss index 68ecff6a..f97a4500 100644 --- a/assets/stylesheets/wizard_custom_admin.scss +++ b/assets/stylesheets/wizard_custom_admin.scss @@ -316,15 +316,34 @@ .metadata .title input { width: 400px; } + + .buttons { + text-align: right; + vertical-align: middle; + + > .d-icon, > .spinner { + margin-right: 7px; + } + + .error { + margin-top: 10px; + color: $danger; + } + } } .wizard-api-header { - display: flex; - justify-content: space-between; - &.page { margin-bottom: 20px; } + + .buttons { + float: right; + } + + .wizard-header { + overflow: hidden; + } } .wizard-api-authentication { @@ -388,12 +407,18 @@ .combo-box { width: 200px; - margin-right: 20px; + margin-right: 10px; + margin-top: -2px; + width: 150px; + } + + input { + margin: 0; + margin-right: 10px; } .endpoint-url { - margin: 0; - width: 450px; + width: 300px; } .remove-endpoint { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4bdbd5f9..700f50ca 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -170,6 +170,7 @@ en: endpoint: label: "Endpoints" add: "Add endpoint" + name: "Endpoint name" method: "Select a method" url: "Enter a url" diff --git a/controllers/api.rb b/controllers/api.rb index 5d8ba61b..86fc6841 100644 --- a/controllers/api.rb +++ b/controllers/api.rb @@ -46,7 +46,7 @@ class CustomWizard::ApiController < ::ApplicationController render json: success_json.merge( api: CustomWizard::ApiSerializer.new( - CustomWizard::Api.new(api_params[:name]), + CustomWizard::Api.get(api_params[:name]), root: false ) ) diff --git a/lib/api/api.rb b/lib/api/api.rb index 2a614c6b..a5d4c155 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -6,7 +6,9 @@ class CustomWizard::Api def initialize(name, data={}) @name = name - @title = data['title'] + data.each do |k, v| + self.send "#{k}=", v if self.respond_to?(k) + end end def self.set(name, data) diff --git a/lib/api/authorization.rb b/lib/api/authorization.rb index 35be314a..ac625475 100644 --- a/lib/api/authorization.rb +++ b/lib/api/authorization.rb @@ -3,8 +3,8 @@ require 'excon' class CustomWizard::Api::Authorization include ActiveModel::SerializerSupport - attr_accessor :authorized, - :name, + attr_accessor :api_name, + :authorized, :auth_type, :auth_url, :token_url, @@ -19,15 +19,11 @@ class CustomWizard::Api::Authorization :username, :password - def initialize(name, data, opts = {}) - unless opts[:data_only] - @name = name - end + def initialize(api_name, data={}) + @api_name = api_name - if data = data.is_a?(String) ? ::JSON.parse(data) : data - data.each do |k, v| - self.send "#{k}=", v if self.respond_to?(k) - end + data.each do |k, v| + self.send "#{k}=", v if self.respond_to?(k) end end @@ -35,25 +31,32 @@ class CustomWizard::Api::Authorization @authorized ||= @access_token && @token_expires_at.to_datetime > Time.now end - def self.set(name, data = {}) - record = self.get(name, data_only: true) + def self.set(api_name, new_data = {}) + data = self.get(api_name, data_only: true) || {} - data.each do |k, v| - record.send "#{k}=", v if record.respond_to?(k) + new_data.each do |k, v| + data[k.to_sym] = v end - PluginStore.set("custom_wizard_api_#{name}", 'authorization', record.as_json) + PluginStore.set("custom_wizard_api_#{api_name}", 'authorization', data) - self.get(name) + self.get(api_name) end - def self.get(name, opts = {}) - data = PluginStore.get("custom_wizard_api_#{name}", 'authorization') - self.new(name, data, opts) + def self.get(api_name, opts = {}) + if data = PluginStore.get("custom_wizard_api_#{api_name}", 'authorization') + if opts[:data_only] + data + else + self.new(api_name, data) + end + else + nil + end end - def self.remove(name) - PluginStore.remove("custom_wizard_api_#{name}", "authorization") + def self.remove(api_name) + PluginStore.remove("custom_wizard_api_#{api_name}", "authorization") end def self.get_header_authorization_string(name) diff --git a/lib/api/endpoint.rb b/lib/api/endpoint.rb index 340a7e35..1d14d6ba 100644 --- a/lib/api/endpoint.rb +++ b/lib/api/endpoint.rb @@ -2,49 +2,59 @@ class CustomWizard::Api::Endpoint include ActiveModel::SerializerSupport attr_accessor :id, + :name, + :api_name, :method, :url - def initialize(name, params) - @name = name - if data = params.is_a?(String) ? ::JSON.parse(params) : params - data.each do |k, v| - self.send "#{k}=", v if self.respond_to?(k) - end - end - end - - def self.set(name, data) - model = data[:endpoint_id] ? self.get(name, data[:endpoint_id]) : {} - endpoint_id = model[:endpoint_id] || SecureRandom.hex(8) + def initialize(api_name, data={}) + @api_name = api_name data.each do |k, v| - model.send "#{k}=", v if model.respond_to?(k) + self.send "#{k}=", v if self.respond_to?(k) + end + 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) + + new_data.each do |k, v| + data[k.to_sym] = v end - PluginStore.set("custom_wizard_api_#{name}", "endpoint_#{endpoint_id}", model.as_json) + PluginStore.set("custom_wizard_api_#{api_name}", "endpoint_#{endpoint_id}", data) - self.get(name) + self.get(api_name, endpoint_id) end - def self.get(name, endpoint_id) + def self.get(api_name, endpoint_id, opts={}) return nil if !endpoint_id - data = PluginStore.get("custom_wizard_api_#{name}", "endpoint_#{endpoint_id}") - data[:id] = endpoint_id - self.new(name, data) + + if data = PluginStore.get("custom_wizard_api_#{api_name}", "endpoint_#{endpoint_id}") + data[:id] = endpoint_id + + if opts[:data_only] + data + else + self.new(api_name, data) + end + else + nil + end end - def self.remove(name) - PluginStoreRow.where("plugin_name = 'custom_wizard_api_#{name}' AND key LIKE 'endpoint_%'").destroy_all + def self.remove(api_name) + 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_%'") .map do |record| - name = record['plugin_name'].sub("custom_wizard_api_", "") + api_name = record['plugin_name'].sub("custom_wizard_api_", "") data = ::JSON.parse(record['value']) data[:id] = record['key'].split('_').last - self.new(name, data) + self.new(api_name, data) end end end diff --git a/serializers/api/endpoint_serializer.rb b/serializers/api/endpoint_serializer.rb index 7ca802c4..18c1406c 100644 --- a/serializers/api/endpoint_serializer.rb +++ b/serializers/api/endpoint_serializer.rb @@ -1,5 +1,10 @@ class CustomWizard::Api::EndpointSerializer < ApplicationSerializer attributes :id, - :type, + :name, + :method, :url + + def method + object.send('method') + end end From 5ffcee1dde8fb92f82821e71f46c9b89c8c90455 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 3 Jun 2019 17:09:24 +1000 Subject: [PATCH 20/31] 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 From 06f9b4be6943e6869933a5140db7ffc8a86094d0 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Tue, 4 Jun 2019 19:51:24 +0100 Subject: [PATCH 21/31] fix for empty api_body and addition of catch for bad JSON --- lib/builder.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/builder.rb b/lib/builder.rb index bc873261..654ff58b 100644 --- a/lib/builder.rb +++ b/lib/builder.rb @@ -394,7 +394,18 @@ class CustomWizard::Builder end def send_to_api(user, action, data) - api_body = CustomWizard::Builder.fill_placeholders(JSON.generate(JSON.parse(action['api_body'])), user, data) + + api_body = nil + + if action['api_body'] != "" + begin + api_body_parsed = JSON.parse(action['api_body']) + rescue + raise Discourse::InvalidParameters, "Invalid API body definition: #{action['api_body']} for #{action['title']}" + end + api_body = CustomWizard::Builder.fill_placeholders(JSON.generate(api_body_parsed), user, data) + end + result = CustomWizard::Api::Endpoint.request(action['api'], action['api_endpoint'], api_body) if result['error'] From def5f8e669fac479cc6b3739c00c1c79c65a1f6b Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Wed, 5 Jun 2019 22:23:15 +0100 Subject: [PATCH 22/31] added validation for endpoint body JSON in API admin and fixed error handling in actual API calls --- .../discourse/models/custom-wizard.js.es6 | 11 +++++ config/locales/client.en.yml | 1 + lib/api/endpoint.rb | 9 +++-- lib/builder.rb | 4 +- lib/test_harness.rb | 40 +++++++++++++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 lib/test_harness.rb diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6 index 34248250..6cb18535 100644 --- a/assets/javascripts/discourse/models/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard.js.es6 @@ -17,6 +17,7 @@ const wizardProperties = [ const CustomWizard = Discourse.Model.extend({ save() { return new Ember.RSVP.Promise((resolve, reject) => { + const id = this.get('id'); if (!id || !id.underscore()) return reject({ error: 'id_required' }); @@ -127,6 +128,16 @@ const CustomWizard = Discourse.Model.extend({ error = 'id_required'; return; } + //check if api_body is valid JSON + let api_body = a.get('api_body'); + if (api_body != '') { + try { + JSON.parse(api_body); + } catch (e) { + error = 'invalid_api_body'; + return; + } + } a.set('id', id.underscore()); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e3388f57..8062302d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -59,6 +59,7 @@ en: name_required: "Wizards must have a name." steps_required: "Wizards must have at least one step." id_required: "All wizards, steps, fields and actions need an id." + invalid_api_body: "Request body JSON needs to be a valid JSON." type_required: "All fields need a type." after_time_need_time: "After time is enabled but no time is set." after_time_invalid: "After time is invalid." diff --git a/lib/api/endpoint.rb b/lib/api/endpoint.rb index 3f475301..fc378df6 100644 --- a/lib/api/endpoint.rb +++ b/lib/api/endpoint.rb @@ -85,8 +85,11 @@ class CustomWizard::Api::Endpoint params[:body] = body end - response = connection.request(params) - - JSON.parse(response.body) + begin + response = connection.request(params) + return JSON.parse(response.body) + rescue + return JSON.parse "[{\"error\":\"API request failed\"}]" + end end end diff --git a/lib/builder.rb b/lib/builder.rb index 654ff58b..3aeb2ede 100644 --- a/lib/builder.rb +++ b/lib/builder.rb @@ -408,8 +408,8 @@ class CustomWizard::Builder result = CustomWizard::Api::Endpoint.request(action['api'], action['api_endpoint'], api_body) - if result['error'] - updater.errors.add(:send_message, result['error']) + if result[0].has_key? 'error' + updater.errors.add(:send_message, result[0]['error']) else ## add validation callback end diff --git a/lib/test_harness.rb b/lib/test_harness.rb new file mode 100644 index 00000000..7d34b2cb --- /dev/null +++ b/lib/test_harness.rb @@ -0,0 +1,40 @@ +require 'excon' +# require 'httplog' + +class CustomWizard::APITestHarness + + def self.basic + + CustomWizard::Authorization.set_authentication_protocol("chargify", "basic_authentication") + CustomWizard::Authorization.set_username("chargify", "W2iA5khmmRso3oySy1KUeJP17ilUuN6OZkgT8PPwk") + CustomWizard::Authorization.set_password("chargify", "X") + authentication_string = CustomWizard::Authorization.get_header_authorization_string("chargify") + puts 'authentication string is ' + authentication_string + response = Excon.get( + "https://merefield-technology.chargify.com/subscriptions.json", + :headers => { + "Authorization" => "#{authentication_string}" + } + ) + JSON.parse(response.body) + end + + def self.oauth_two + + CustomWizard::Authorization.set_authentication_protocol("google", "OAuth2_authentication") + CustomWizard::Authorization.set_client_id("chargify", "W2iA5khmmRso3oySy1KUeJP17ilUuN6OZkgT8PPwk") + CustomWizard::Authorization.set_client_secret("chargify", "X") + + puts curl + authentication_string = CustomWizard::Authorization.get_header_authorization_string("chargify") + puts 'authentication string is ' + authentication_string + response = Excon.get( + "https://merefield-technology.chargify.com/subscriptions.json", + :headers => { + "Authorization" => "#{authentication_string}" + } + ) + JSON.parse(response.body) + end + +end From 1544d4494b03c64ac9122f9243c4756516711cb9 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Wed, 5 Jun 2019 22:29:35 +0100 Subject: [PATCH 23/31] removed test file --- lib/test_harness.rb | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 lib/test_harness.rb diff --git a/lib/test_harness.rb b/lib/test_harness.rb deleted file mode 100644 index 7d34b2cb..00000000 --- a/lib/test_harness.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'excon' -# require 'httplog' - -class CustomWizard::APITestHarness - - def self.basic - - CustomWizard::Authorization.set_authentication_protocol("chargify", "basic_authentication") - CustomWizard::Authorization.set_username("chargify", "W2iA5khmmRso3oySy1KUeJP17ilUuN6OZkgT8PPwk") - CustomWizard::Authorization.set_password("chargify", "X") - authentication_string = CustomWizard::Authorization.get_header_authorization_string("chargify") - puts 'authentication string is ' + authentication_string - response = Excon.get( - "https://merefield-technology.chargify.com/subscriptions.json", - :headers => { - "Authorization" => "#{authentication_string}" - } - ) - JSON.parse(response.body) - end - - def self.oauth_two - - CustomWizard::Authorization.set_authentication_protocol("google", "OAuth2_authentication") - CustomWizard::Authorization.set_client_id("chargify", "W2iA5khmmRso3oySy1KUeJP17ilUuN6OZkgT8PPwk") - CustomWizard::Authorization.set_client_secret("chargify", "X") - - puts curl - authentication_string = CustomWizard::Authorization.get_header_authorization_string("chargify") - puts 'authentication string is ' + authentication_string - response = Excon.get( - "https://merefield-technology.chargify.com/subscriptions.json", - :headers => { - "Authorization" => "#{authentication_string}" - } - ) - JSON.parse(response.body) - end - -end From 95ba52f97326999bb23b5a072a47bc06051a5176 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Thu, 6 Jun 2019 17:10:13 +0100 Subject: [PATCH 24/31] added logging for api calls --- .../controllers/admin-wizards-api.js.es6 | 13 ++++ .../discourse/models/custom-wizard-api.js.es6 | 3 +- .../discourse/templates/admin-wizards-api.hbs | 26 +++++++ config/locales/client.en.yml | 3 + controllers/api.rb | 6 ++ lib/api/endpoint.rb | 5 ++ lib/api/logentry.rb | 69 +++++++++++++++++++ plugin.rb | 3 + serializers/api/api_serializer.rb | 12 +++- serializers/api/log_serializer.rb | 7 ++ 10 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 lib/api/logentry.rb create mode 100644 serializers/api/log_serializer.rb diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 7306012e..30fa0130 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -165,6 +165,19 @@ export default Ember.Controller.extend({ }); } }).finally(() => this.set('updating', false)); + }, + + clearLogs() { + ajax(`/admin/wizards/apis/logs/${name.underscore()}`, { + type: 'DELETE' + }).catch(popupAjaxError) + .then(result => { + if (result.success) { + this.transitionToRoute('adminWizardsApis').then(() => { + this.send('refreshModel'); + }); + } + }).finally(() => this.set('updating', false)); } } }); diff --git a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 index 791a153a..b335b88a 100644 --- a/assets/javascripts/discourse/models/custom-wizard-api.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-api.js.es6 @@ -35,7 +35,8 @@ CustomWizardApi.reopenClass({ tokenExpiresAt: authorization.token_expires_at, tokenRefreshAt: authorization.token_refresh_at, endpoints: Ember.A(endpoints), - isNew: params.isNew + isNew: params.isNew, + log: params.log }); return api; diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index 0ed5766e..04ffb4fd 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -237,3 +237,29 @@
    {{/if}}
    + +
    + {{i18n 'admin.wizard.api.log.label'}} + {{d-button action='clearLogs' + icon='trash-alt' + class='clear-logs'}} +
    + +
    +
    + + + + + + {{#each api.log as |logentry|}} + + + + + + + {{/each}} +
    DatetimeStatusEndpointError
    {{logentry.time}}{{logentry.status}}{{logentry.endpoint_url}}{{logentry.error}}
    +
    +
    diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8062302d..86423497 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -184,6 +184,9 @@ en: method: "Select a method" url: "Enter a url" + log: + label: "Logs" + wizard_js: location: name: diff --git a/controllers/api.rb b/controllers/api.rb index 524f4e84..05075c12 100644 --- a/controllers/api.rb +++ b/controllers/api.rb @@ -57,11 +57,17 @@ class CustomWizard::ApiController < ::ApplicationController CustomWizard::Api.remove(api_params[:name]) CustomWizard::Api::Authorization.remove(api_params[:name]) CustomWizard::Api::Endpoint.remove(api_params[:name]) + CustomWizard::Api::LogEntry.clear(api_params[:name]) end render json: success_json end + def clearlogs + CustomWizard::Api::LogEntry.clear(api_params[:name]) + render json: success_json + end + def redirect params.require(:name) params.require(:code) diff --git a/lib/api/endpoint.rb b/lib/api/endpoint.rb index fc378df6..356687e7 100644 --- a/lib/api/endpoint.rb +++ b/lib/api/endpoint.rb @@ -87,8 +87,13 @@ class CustomWizard::Api::Endpoint begin response = connection.request(params) + log_params = {time: Time.now, status: 'SUCCESS', endpoint_url: endpoint.url, error: ""} + CustomWizard::Api::LogEntry.set(api_name, log_params) return JSON.parse(response.body) rescue + # TODO: improve error detail + log_params = {time: Time.now, status: 'FAILURE', endpoint_url: endpoint.url, error: "API request failed"} + CustomWizard::Api::LogEntry.set(api_name, log_params) return JSON.parse "[{\"error\":\"API request failed\"}]" end end diff --git a/lib/api/logentry.rb b/lib/api/logentry.rb new file mode 100644 index 00000000..083262aa --- /dev/null +++ b/lib/api/logentry.rb @@ -0,0 +1,69 @@ +class CustomWizard::Api::LogEntry + include ActiveModel::SerializerSupport + + attr_accessor :log_id, + :time, + :status, + :endpoint_url, + :error + + def initialize(api_name, data={}) + @api_name = api_name + + data.each do |k, v| + self.send "#{k}=", v if self.respond_to?(k) + end + end + + def self.set(api_name, new_data) + if new_data['log_id'] + data = self.get(api_name, new_data['log_id'], data_only: true) + log_id = new_data['log_id'] + else + data = {} + log_id = SecureRandom.hex(3) + end + + new_data.each do |k, v| + data[k.to_sym] = v + end + + PluginStore.set("custom_wizard_api_#{api_name}", "log_#{log_id}", data) + + self.get(api_name, log_id) + end + + def self.get(api_name, log_id, opts={}) + return nil if !log_id + + if data = PluginStore.get("custom_wizard_api_#{api_name}", "log_#{log_id}") + if opts[:data_only] + data + else + data[:log_id] = log_id + self.new(api_name, data) + end + else + nil + end + end + + def self.remove(api_name) + PluginStoreRow.where("plugin_name = 'custom_wizard_api_#{api_name}' AND key LIKE 'log_%'").destroy_all + end + + def self.list(api_name) + PluginStoreRow.where("plugin_name LIKE 'custom_wizard_api_#{api_name}' AND key LIKE 'log_%'") + .map do |record| + api_name = record['plugin_name'].sub("custom_wizard_api_", "") + data = ::JSON.parse(record['value']) + data[:log_id] = record['key'].split('_').last + self.new(api_name, data) + end + end + + def self.clear(api_name) + PluginStoreRow.where("plugin_name = 'custom_wizard_api_#{api_name}' AND key LIKE 'log_%'").destroy_all + end + +end diff --git a/plugin.rb b/plugin.rb index da45683d..588689e3 100644 --- a/plugin.rb +++ b/plugin.rb @@ -72,6 +72,7 @@ after_initialize do get 'admin/wizards/apis/:name' => 'api#find' put 'admin/wizards/apis/:name' => 'api#save' delete 'admin/wizards/apis/:name' => 'api#remove' + delete 'admin/wizards/apis/logs/:name' => 'api#clearlogs' get 'admin/wizards/apis/:name/redirect' => 'api#redirect' end end @@ -92,12 +93,14 @@ after_initialize do load File.expand_path('../lib/api/api.rb', __FILE__) load File.expand_path('../lib/api/authorization.rb', __FILE__) load File.expand_path('../lib/api/endpoint.rb', __FILE__) + load File.expand_path('../lib/api/logentry.rb', __FILE__) load File.expand_path('../controllers/api.rb', __FILE__) load File.expand_path('../serializers/api/api_serializer.rb', __FILE__) 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__) + load File.expand_path('../serializers/api/log_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 651e29fd..9d7fba70 100644 --- a/serializers/api/api_serializer.rb +++ b/serializers/api/api_serializer.rb @@ -2,7 +2,8 @@ class CustomWizard::ApiSerializer < ApplicationSerializer attributes :name, :title, :authorization, - :endpoints + :endpoints, + :log def authorization if authorization = CustomWizard::Api::Authorization.get(object.name) @@ -21,4 +22,13 @@ class CustomWizard::ApiSerializer < ApplicationSerializer ) end end + + def log + if log = CustomWizard::Api::LogEntry.list(object.name) + ActiveModel::ArraySerializer.new( + log, + each_serializer: CustomWizard::Api::LogSerializer + ) + end + end end diff --git a/serializers/api/log_serializer.rb b/serializers/api/log_serializer.rb new file mode 100644 index 00000000..754c15f3 --- /dev/null +++ b/serializers/api/log_serializer.rb @@ -0,0 +1,7 @@ +class CustomWizard::Api::LogSerializer < ApplicationSerializer + attributes :log_id, + :time, + :status, + :endpoint_url, + :error +end From 4f195c704a254b1d23750815dd618bced7d51506 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Fri, 7 Jun 2019 09:57:34 +1000 Subject: [PATCH 25/31] Additional error handling --- lib/builder.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/builder.rb b/lib/builder.rb index 3aeb2ede..7fd4fa6d 100644 --- a/lib/builder.rb +++ b/lib/builder.rb @@ -400,7 +400,7 @@ class CustomWizard::Builder if action['api_body'] != "" begin api_body_parsed = JSON.parse(action['api_body']) - rescue + rescue JSON::ParserError raise Discourse::InvalidParameters, "Invalid API body definition: #{action['api_body']} for #{action['title']}" end api_body = CustomWizard::Builder.fill_placeholders(JSON.generate(api_body_parsed), user, data) @@ -408,8 +408,9 @@ class CustomWizard::Builder result = CustomWizard::Api::Endpoint.request(action['api'], action['api_endpoint'], api_body) - if result[0].has_key? 'error' - updater.errors.add(:send_message, result[0]['error']) + if error = result['error'] || (result[0] && result[0]['error']) + error = error['message'] || error + updater.errors.add(:send_to_api, error) else ## add validation callback end From b9f8cc61b2002f8d92cd641131d862efcbc239c7 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Fri, 7 Jun 2019 13:09:31 +1000 Subject: [PATCH 26/31] distinguish between 2 legged and 3 legged oauth && other authorization improvements --- .../controllers/admin-wizards-api.js.es6 | 72 ++++++++++----- .../discourse/templates/admin-wizards-api.hbs | 59 +++++++----- controllers/api.rb | 15 +++ jobs/refresh_api_access_token.rb | 2 +- lib/api/authorization.rb | 91 +++++++++---------- plugin.rb | 1 + 6 files changed, 147 insertions(+), 93 deletions(-) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 30fa0130..22f18ea8 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -7,16 +7,16 @@ export default Ember.Controller.extend({ queryParams: ['refresh_list'], loadingSubscriptions: false, notAuthorized: Ember.computed.not('api.authorized'), - authorizationTypes: ['oauth', 'basic'], - isOauth: Ember.computed.equal('api.authType', 'oauth'), - isBasicAuth: Ember.computed.equal('api.authType', 'basic'), endpointMethods: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'], showRemove: Ember.computed.not('isNew'), + showRedirectUri: Ember.computed.and('threeLeggedOauth', 'api.name'), responseIcon: null, - @computed('saveDisabled', 'api.authType', 'api.authUrl', 'api.clientId', 'api.clientSecret') - authDisabled(saveDisabled, authType, authUrl, clientId, clientSecret) { - return saveDisabled || !authType || !authUrl || !clientId || !clientSecret; + @computed('saveDisabled', 'api.authType', 'api.authUrl', 'api.tokenUrl', 'api.clientId', 'api.clientSecret', 'threeLeggedOauth') + authDisabled(saveDisabled, authType, authUrl, tokenUrl, clientId, clientSecret, threeLeggedOauth) { + if (saveDisabled || !authType || !tokenUrl || !clientId || !clientSecret) return true; + if (threeLeggedOauth) return !authUrl; + return false; }, @computed('api.name', 'api.authType') @@ -24,6 +24,17 @@ export default Ember.Controller.extend({ return !name || !authType; }, + authorizationTypes: ['basic', 'oauth_2', 'oauth_3'], + isBasicAuth: Ember.computed.equal('api.authType', 'basic'), + + @computed('api.authType') + isOauth(authType) { + return authType && authType.indexOf('oauth') > -1; + }, + + twoLeggedOauth: Ember.computed.equal('api.authType', 'oauth_2'), + threeLeggedOauth: Ember.computed.equal('api.authType', 'oauth_3'), + actions: { addParam() { this.get('api.authParams').pushObject({}); @@ -43,23 +54,40 @@ export default Ember.Controller.extend({ authorize() { const api = this.get('api'); - const { authType, authUrl, authParams } = api; + const { name, authType, authUrl, authParams } = api; - if (authType !== 'oauth') return; + this.set('authErrorMessage', ''); - let query = '?'; + if (authType === 'oauth_2') { + this.set('authorizing', true); + ajax(`/admin/wizards/apis/${name.underscore()}/authorize`).catch(popupAjaxError) + .then(result => { + if (result.success) { + this.set('api', CustomWizardApi.create(result.api)); + } else if (result.failed && result.message) { + this.set('authErrorMessage', result.message); + } else { + this.set('authErrorMessage', 'Authorization Failed'); + } + setTimeout(() => { + this.set('authErrorMessage', ''); + }, 6000); + }).finally(() => this.set('authorizing', false)); + } else if (authType === 'oauth_3') { + let query = '?'; - query += `client_id=${api.clientId}`; - query += `&redirect_uri=${encodeURIComponent(api.redirectUri)}`; - query += `&response_type=code`; + query += `client_id=${api.clientId}`; + query += `&redirect_uri=${encodeURIComponent(api.redirectUri)}`; + query += `&response_type=code`; - if (authParams) { - authParams.forEach(p => { - query += `&${p.key}=${encodeURIComponent(p.value)}`; - }); + if (authParams) { + authParams.forEach(p => { + query += `&${p.key}=${encodeURIComponent(p.value)}`; + }); + } + + window.location.href = authUrl + query; } - - window.location.href = authUrl + query; }, save() { @@ -88,10 +116,12 @@ export default Ember.Controller.extend({ let requiredParams; - if (authType === 'oauth') { - requiredParams = ['authUrl', 'tokenUrl', 'clientId', 'clientSecret']; - } else if (authType === 'basic') { + if (authType === 'basic') { requiredParams = ['username', 'password']; + } else if (authType === 'oauth_2') { + requiredParams = ['tokenUrl', 'clientId', 'clientSecret']; + } else if (authType === 'oauth_3') { + requiredParams = ['authUrl', 'tokenUrl', 'clientId', 'clientSecret']; } for (let rp of requiredParams) { diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index 04ffb4fd..32618101 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -49,6 +49,13 @@
    {{#if isOauth}} + {{#if authorizing}} + {{loading-spinner size="small"}} + {{else}} + {{#if authErrorMessage}} + {{authErrorMessage}} + {{/if}} + {{/if}} {{d-button label="admin.wizard.api.auth.btn" action="authorize" disabled=authDisabled @@ -68,17 +75,15 @@ {{i18n 'admin.wizard.api.auth.settings'}}
    - {{#if isOauth}} - {{#if api.name}} -
    -
    - -
    - {{api.redirectUri}} -
    + {{#if showRedirectUri}} +
    +
    + +
    + {{api.redirectUri}}
    - {{/if}} +
    {{/if}}
    @@ -89,12 +94,14 @@
    {{#if isOauth}} -
    - -
    - {{input value=api.authUrl}} + {{#if threeLeggedOauth}} +
    + +
    + {{input value=api.authUrl}} +
    -
    + {{/if}}
    @@ -165,12 +172,14 @@ {{i18n 'admin.wizard.api.status.label'}}
    -
    - -
    - {{api.code}} + {{#if threeLeggedOauth}} +
    + +
    + {{api.code}} +
    -
    + {{/if}}
    @@ -179,12 +188,14 @@
    -
    - -
    - {{api.refreshToken}} + {{#if threeLeggedOauth}} +
    + +
    + {{api.refreshToken}} +
    -
    + {{/if}}
    diff --git a/controllers/api.rb b/controllers/api.rb index 05075c12..d87a0fe4 100644 --- a/controllers/api.rb +++ b/controllers/api.rb @@ -63,6 +63,21 @@ class CustomWizard::ApiController < ::ApplicationController render json: success_json end + def authorize + result = CustomWizard::Api::Authorization.get_token(api_params[:name]) + + if result['error'] + render json: failed_json.merge(message: result['error_description'] || result['error']) + else + render json: success_json.merge( + api: CustomWizard::ApiSerializer.new( + CustomWizard::Api.get(api_params[:name]), + root: false + ) + ) + end + end + def clearlogs CustomWizard::Api::LogEntry.clear(api_params[:name]) render json: success_json diff --git a/jobs/refresh_api_access_token.rb b/jobs/refresh_api_access_token.rb index edea0052..54f6da55 100644 --- a/jobs/refresh_api_access_token.rb +++ b/jobs/refresh_api_access_token.rb @@ -1,7 +1,7 @@ module Jobs class RefreshApiAccessToken < Jobs::Base def execute(args) - CustomWizard::Api::Authorization.refresh_token(args[:name]) + CustomWizard::Api::Authorization.get_token(args[:name], refresh: true) end end end diff --git a/lib/api/authorization.rb b/lib/api/authorization.rb index 7f702f6b..3c57a955 100644 --- a/lib/api/authorization.rb +++ b/lib/api/authorization.rb @@ -77,71 +77,68 @@ class CustomWizard::Api::Authorization end end - def self.get_token(name) + def self.get_token(name, opts = {}) authorization = CustomWizard::Api::Authorization.get(name) + type = authorization.auth_type - body = { - client_id: authorization.client_id, - client_secret: authorization.client_secret, - code: authorization.code, - grant_type: 'authorization_code', - redirect_uri: Discourse.base_url + "/admin/wizards/apis/#{name}/redirect" - } + body = {} - result = Excon.post( + if opts[:refresh] && type === 'oauth_3' + body['grant_type'] = 'refresh_token' + elsif type === 'oauth_2' + body['grant_type'] = 'client_credentials' + elsif type === 'oauth_3' + body['grant_type'] = 'authorization_code' + end + + unless opts[:refresh] + body['client_id'] = authorization.client_id + body['client_secret'] = authorization.client_secret + end + + if type === 'oauth_3' + body['code'] = authorization.code + body['redirect_uri'] = Discourse.base_url + "/admin/wizards/apis/#{name}/redirect" + end + + connection = Excon.new( authorization.token_url, :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, - :body => URI.encode_www_form(body) + :method => 'GET', + :query => URI.encode_www_form(body) ) - self.handle_token_result(name, result) - end - - def self.refresh_token(name) - authorization = CustomWizard::Api::Authorization.get(name) - - body = { - grant_type: 'refresh_token', - refresh_token: authorization.refresh_token - } - - authorization_string = authorization.client_id + ':' + authorization.client_secret - - result = Excon.post( - authorization.token_url, - :headers => { - "Content-Type" => "application/x-www-form-urlencoded", - "Authorization" => "Basic #{Base64.strict_encode64(authorization_string)}" - }, - :body => URI.encode_www_form(body) - ) + result = connection.request() self.handle_token_result(name, result) end def self.handle_token_result(name, result) - data = JSON.parse(result.body) + result_data = JSON.parse(result.body) - return false if (data['error']) + if result_data['error'] + return result_data + end - access_token = data['access_token'] - refresh_token = data['refresh_token'] - expires_at = Time.now + data['expires_in'].seconds - refresh_at = expires_at.to_time - 2.hours + data = {} - opts = { - name: name - } + data['access_token'] = result_data['access_token'] + data['refresh_token'] = result_data['refresh_token'] if result_data['refresh_token'] + data['token_type'] = result_data['token_type'] if result_data['token_type'] - Jobs.enqueue_at(refresh_at, :refresh_api_access_token, opts) + if result_data['expires_in'] + data['token_expires_at'] = Time.now + result_data['expires_in'].seconds + data['token_refresh_at'] = data['token_expires_at'].to_time - 10.minutes - CustomWizard::Api::Authorization.set(name, - access_token: access_token, - refresh_token: refresh_token, - token_expires_at: expires_at, - token_refresh_at: refresh_at - ) + opts = { + name: name + } + + Jobs.enqueue_at(data['token_refresh_at'], :refresh_api_access_token, opts) + end + + CustomWizard::Api::Authorization.set(name, data) end end diff --git a/plugin.rb b/plugin.rb index 588689e3..1709b7ea 100644 --- a/plugin.rb +++ b/plugin.rb @@ -74,6 +74,7 @@ after_initialize do delete 'admin/wizards/apis/:name' => 'api#remove' delete 'admin/wizards/apis/logs/:name' => 'api#clearlogs' get 'admin/wizards/apis/:name/redirect' => 'api#redirect' + get 'admin/wizards/apis/:name/authorize' => 'api#authorize' end end From c6fed81d28f32dd727211683c401cd59823f3fd3 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Wed, 12 Jun 2019 21:57:38 +0100 Subject: [PATCH 27/31] updated some ember actions to closure actions to remove deprecation warnings, fixed issue where you could not remove last auth parameter as key would never be cleared --- .../discourse/templates/admin-wizards-api.hbs | 16 ++++++++-------- lib/api/authorization.rb | 7 +++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index 32618101..6a0a8067 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -8,10 +8,10 @@ {{/if}} {{/if}} - {{d-button label="admin.wizard.api.save" action="save" class="btn-primary" disabled=saveDisabled}} + {{d-button label="admin.wizard.api.save" action=(action "save") class="btn-primary" disabled=saveDisabled}} {{#if showRemove}} - {{d-button action="remove" label="admin.wizard.api.remove"}} + {{d-button action=(action "remove") label="admin.wizard.api.remove"}} {{/if}} {{#if error}} @@ -57,7 +57,7 @@ {{/if}} {{/if}} {{d-button label="admin.wizard.api.auth.btn" - action="authorize" + action=(action "authorize") disabled=authDisabled class="btn-primary"}} {{/if}} @@ -131,10 +131,10 @@
    {{input value=param.key placeholder=(i18n 'admin.wizard.api.auth.params.key')}} {{input value=param.value placeholder=(i18n 'admin.wizard.api.auth.params.value')}} - {{d-button action='removeParam' actionParam=param icon='times'}} + {{d-button action=(action "removeParam") actionParam=param icon='times'}}
    {{/each}} - {{d-button label='admin.wizard.api.auth.params.new' icon='plus' action='addParam'}} + {{d-button label='admin.wizard.api.auth.params.new' icon='plus' action=(action "addParam")}}
    {{/if}} @@ -219,7 +219,7 @@
    - {{d-button action='addEndpoint' label='admin.wizard.api.endpoint.add' icon='plus'}} + {{d-button action=(action "addEndpoint") label='admin.wizard.api.endpoint.add' icon='plus'}} {{#if api.endpoints}}
    @@ -236,7 +236,7 @@ {{input value=endpoint.url placeholder=(i18n 'admin.wizard.api.endpoint.url') class='endpoint-url'}} - {{d-button action='removeEndpoint' + {{d-button action=(action "removeEndpoint") actionParam=endpoint icon='times' class='remove-endpoint'}} @@ -251,7 +251,7 @@
    {{i18n 'admin.wizard.api.log.label'}} - {{d-button action='clearLogs' + {{d-button action=(action "clearLogs") icon='trash-alt' class='clear-logs'}}
    diff --git a/lib/api/authorization.rb b/lib/api/authorization.rb index 3c57a955..9dd51d50 100644 --- a/lib/api/authorization.rb +++ b/lib/api/authorization.rb @@ -32,6 +32,7 @@ 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) || {} @@ -40,6 +41,12 @@ class CustomWizard::Api::Authorization data[k.to_sym] = v end + data.each do |k, v| + unless new_data.key?(k.to_s) + data.delete(k) + end + end + PluginStore.set("custom_wizard_api_#{api_name}", 'authorization', data) self.get(api_name) From 7efaf25572681e4fa6661b20e393ea2b1e2e9e8c Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Sun, 16 Jun 2019 12:49:51 +0100 Subject: [PATCH 28/31] added logging to token refresh, added user column to log and log display --- .../discourse/templates/admin-wizards-api.hbs | 10 ++++++++-- lib/api/authorization.rb | 10 ++++++++-- lib/api/endpoint.rb | 7 ++++--- lib/api/logentry.rb | 15 +++++++++++++-- lib/builder.rb | 2 +- serializers/api/log_serializer.rb | 9 +++++++-- 6 files changed, 41 insertions(+), 12 deletions(-) diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index 6a0a8067..160225ea 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -260,14 +260,20 @@
    + - + {{#each api.log as |logentry|}} + - + {{/each}} diff --git a/lib/api/authorization.rb b/lib/api/authorization.rb index 9dd51d50..f6a11794 100644 --- a/lib/api/authorization.rb +++ b/lib/api/authorization.rb @@ -116,8 +116,14 @@ class CustomWizard::Api::Authorization :method => 'GET', :query => URI.encode_www_form(body) ) - - result = connection.request() + begin + result = connection.request() + log_params = {time: Time.now, user_id: 0, status: 'SUCCESS', url: token_url, error: ""} + CustomWizard::Api::LogEntry.set(name, log_params) + rescue + log_params = {time: Time.now, user_id: 0, status: 'FAILURE', url: token_url, error: "Token refresh request failed"} + CustomWizard::Api::LogEntry.set(name, log_params) + end self.handle_token_result(name, result) end diff --git a/lib/api/endpoint.rb b/lib/api/endpoint.rb index 356687e7..3036a49f 100644 --- a/lib/api/endpoint.rb +++ b/lib/api/endpoint.rb @@ -62,7 +62,7 @@ class CustomWizard::Api::Endpoint end end - def self.request(api_name, endpoint_id, body) + def self.request(user, api_name, endpoint_id, body) endpoint = self.get(api_name, endpoint_id) auth = CustomWizard::Api::Authorization.get_header_authorization_string(api_name) @@ -87,12 +87,13 @@ class CustomWizard::Api::Endpoint begin response = connection.request(params) - log_params = {time: Time.now, status: 'SUCCESS', endpoint_url: endpoint.url, error: ""} + log_params = {time: Time.now, user_id: user.id, status: 'SUCCESS', url: endpoint.url, error: ""} + CustomWizard::Api::LogEntry.set(api_name, log_params) return JSON.parse(response.body) rescue # TODO: improve error detail - log_params = {time: Time.now, status: 'FAILURE', endpoint_url: endpoint.url, error: "API request failed"} + log_params = {time: Time.now, user_id: user.id, status: 'FAILURE', url: endpoint.url, error: "API request failed"} CustomWizard::Api::LogEntry.set(api_name, log_params) return JSON.parse "[{\"error\":\"API request failed\"}]" end diff --git a/lib/api/logentry.rb b/lib/api/logentry.rb index 083262aa..6b7831bd 100644 --- a/lib/api/logentry.rb +++ b/lib/api/logentry.rb @@ -3,9 +3,14 @@ class CustomWizard::Api::LogEntry attr_accessor :log_id, :time, + :user_id, :status, - :endpoint_url, - :error + :url, + :error, + :username, + :userpath, + :name, + :avatar_template def initialize(api_name, data={}) @api_name = api_name @@ -58,6 +63,12 @@ class CustomWizard::Api::LogEntry api_name = record['plugin_name'].sub("custom_wizard_api_", "") data = ::JSON.parse(record['value']) data[:log_id] = record['key'].split('_').last + this_user = User.find_by(id: data['user_id']) + data[:user_id] = this_user.id || nil + data[:username] = this_user.username || "" + data[:userpath] = "/u/#{this_user.username_lower}/activity" + data[:name] = this_user.name || "" + data[:avatar_template] = "/user_avatar/default/#{this_user.username_lower}/97/#{this_user.uploaded_avatar_id}.png" self.new(api_name, data) end end diff --git a/lib/builder.rb b/lib/builder.rb index 7fd4fa6d..c2be9282 100644 --- a/lib/builder.rb +++ b/lib/builder.rb @@ -406,7 +406,7 @@ class CustomWizard::Builder api_body = CustomWizard::Builder.fill_placeholders(JSON.generate(api_body_parsed), user, data) end - result = CustomWizard::Api::Endpoint.request(action['api'], action['api_endpoint'], api_body) + result = CustomWizard::Api::Endpoint.request(user, action['api'], action['api_endpoint'], api_body) if error = result['error'] || (result[0] && result[0]['error']) error = error['message'] || error diff --git a/serializers/api/log_serializer.rb b/serializers/api/log_serializer.rb index 754c15f3..13ee1555 100644 --- a/serializers/api/log_serializer.rb +++ b/serializers/api/log_serializer.rb @@ -2,6 +2,11 @@ class CustomWizard::Api::LogSerializer < ApplicationSerializer attributes :log_id, :time, :status, - :endpoint_url, - :error + :url, + :error, + :user_id, + :username, + :userpath, + :name, + :avatar_template end From fa39bbff2960a0fc85124d3fa34a7e449cf4f03b Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Sun, 16 Jun 2019 14:33:30 +0100 Subject: [PATCH 29/31] FIX: logs weren't deleting when trash can was pressed --- .../javascripts/discourse/controllers/admin-wizards-api.js.es6 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 index 22f18ea8..42451421 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6 @@ -198,6 +198,9 @@ export default Ember.Controller.extend({ }, clearLogs() { + const name = this.get('api.name'); + if (!name) return; + ajax(`/admin/wizards/apis/logs/${name.underscore()}`, { type: 'DELETE' }).catch(popupAjaxError) From 15e52a5e42a5f87a2ef6b9f9c589ebd5503f5753 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Sun, 16 Jun 2019 14:44:23 +0100 Subject: [PATCH 30/31] added CSS for log table to match existing --- assets/stylesheets/wizard_custom_admin.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assets/stylesheets/wizard_custom_admin.scss b/assets/stylesheets/wizard_custom_admin.scss index 2ebbb253..ab078fc4 100644 --- a/assets/stylesheets/wizard_custom_admin.scss +++ b/assets/stylesheets/wizard_custom_admin.scss @@ -408,6 +408,7 @@ .wizard-api-endpoints { background-color: $primary-very-low; padding: 20px; + margin-bottom: 20px; .endpoint-list { margin-top: 20px; @@ -443,3 +444,9 @@ } } } + +.wizard-api-log { + background-color: #f8f8f8; + padding: 20px; + margin-bottom: 20px; +} From 8db52e065632c4bf42c5ac6a94c6b66b7bf9a423 Mon Sep 17 00:00:00 2001 From: Robert Barrow Date: Sun, 16 Jun 2019 16:40:47 +0100 Subject: [PATCH 31/31] a slew of bug fixes to ensure a fault free OAuth 2 authorisation --- controllers/api.rb | 3 ++- lib/api/authorization.rb | 12 +++--------- lib/api/logentry.rb | 18 +++++++++++++----- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/controllers/api.rb b/controllers/api.rb index d87a0fe4..a4ed8e6d 100644 --- a/controllers/api.rb +++ b/controllers/api.rb @@ -29,6 +29,7 @@ class CustomWizard::ApiController < ::ApplicationController CustomWizard::Api.set(api_params[:name], title: api_params[:title]) if auth_data.present? + auth_data['auth_params'] = auth_data['auth_params'] || [] CustomWizard::Api::Authorization.set(api_params[:name], auth_data) end @@ -66,7 +67,7 @@ class CustomWizard::ApiController < ::ApplicationController def authorize result = CustomWizard::Api::Authorization.get_token(api_params[:name]) - if result['error'] + if result.instance_variable_defined?(:@error) render json: failed_json.merge(message: result['error_description'] || result['error']) else render json: success_json.merge( diff --git a/lib/api/authorization.rb b/lib/api/authorization.rb index f6a11794..745ecc9b 100644 --- a/lib/api/authorization.rb +++ b/lib/api/authorization.rb @@ -41,12 +41,6 @@ class CustomWizard::Api::Authorization data[k.to_sym] = v end - data.each do |k, v| - unless new_data.key?(k.to_s) - data.delete(k) - end - end - PluginStore.set("custom_wizard_api_#{api_name}", 'authorization', data) self.get(api_name) @@ -118,10 +112,10 @@ class CustomWizard::Api::Authorization ) begin result = connection.request() - log_params = {time: Time.now, user_id: 0, status: 'SUCCESS', url: token_url, error: ""} + log_params = {time: Time.now, user_id: 0, status: 'SUCCESS', url: authorization.token_url, error: ""} CustomWizard::Api::LogEntry.set(name, log_params) - rescue - log_params = {time: Time.now, user_id: 0, status: 'FAILURE', url: token_url, error: "Token refresh request failed"} + rescue SystemCallError => e + log_params = {time: Time.now, user_id: 0, status: 'FAILURE', url: authorization.token_url, error: "Token refresh request failed: #{e.inspect}"} CustomWizard::Api::LogEntry.set(name, log_params) end diff --git a/lib/api/logentry.rb b/lib/api/logentry.rb index 6b7831bd..1408615d 100644 --- a/lib/api/logentry.rb +++ b/lib/api/logentry.rb @@ -64,11 +64,19 @@ class CustomWizard::Api::LogEntry data = ::JSON.parse(record['value']) data[:log_id] = record['key'].split('_').last this_user = User.find_by(id: data['user_id']) - data[:user_id] = this_user.id || nil - data[:username] = this_user.username || "" - data[:userpath] = "/u/#{this_user.username_lower}/activity" - data[:name] = this_user.name || "" - data[:avatar_template] = "/user_avatar/default/#{this_user.username_lower}/97/#{this_user.uploaded_avatar_id}.png" + unless this_user.nil? + data[:user_id] = this_user.id || nil + data[:username] = this_user.username || "" + data[:userpath] = "/u/#{this_user.username_lower}/activity" + data[:name] = this_user.name || "" + data[:avatar_template] = "/user_avatar/default/#{this_user.username_lower}/97/#{this_user.uploaded_avatar_id}.png" + else + data[:user_id] = nil + data[:username] = "" + data[:userpath] = "" + data[:name] = "" + data[:avatar_template] = "" + end self.new(api_name, data) end end
    DatetimeUser StatusEndpointURL Error
    {{logentry.time}} + + {{logentry.status}}{{logentry.endpoint_url}}{{logentry.url}} {{logentry.error}}