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/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6
index 8a0f39e8..4957dbc6 100644
--- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6
+++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6
@@ -4,6 +4,7 @@ const ACTION_TYPES = [
{ id: 'create_topic', name: 'Create Topic' },
{ id: 'update_profile', name: 'Update Profile' },
{ id: 'send_message', name: 'Send Message' },
+ { id: 'send_to_api', name: 'Send to API' }
{ id: 'add_to_group', name: 'Add to Group' },
{ id: 'route_to', name: 'Route To' }
];
@@ -28,6 +29,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'),
addToGroup: Ember.computed.equal('action.type', 'add_to_group'),
routeTo: Ember.computed.equal('action.type', 'route_to'),
disableId: Ember.computed.not('action.isNew'),
@@ -54,5 +57,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
new file mode 100644
index 00000000..42451421
--- /dev/null
+++ b/assets/javascripts/discourse/controllers/admin-wizards-api.js.es6
@@ -0,0 +1,216 @@
+import { ajax } from 'discourse/lib/ajax';
+import { popupAjaxError } from 'discourse/lib/ajax-error';
+import CustomWizardApi from '../models/custom-wizard-api';
+import { default as computed } from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Controller.extend({
+ queryParams: ['refresh_list'],
+ loadingSubscriptions: false,
+ notAuthorized: Ember.computed.not('api.authorized'),
+ 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.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')
+ saveDisabled(name, authType) {
+ 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({});
+ },
+
+ removeParam(param) {
+ 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 { name, authType, authUrl, authParams } = api;
+
+ this.set('authErrorMessage', '');
+
+ 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`;
+
+ if (authParams) {
+ authParams.forEach(p => {
+ query += `&${p.key}=${encodeURIComponent(p.value)}`;
+ });
+ }
+
+ window.location.href = authUrl + query;
+ }
+ },
+
+ save() {
+ const api = this.get('api');
+ const name = api.name;
+ const authType = api.authType;
+ let refreshList = false;
+ let error;
+
+ if (!name || !authType) return;
+
+ let data = {
+ auth_type: authType
+ };
+
+ if (api.title) data['title'] = api.title;
+
+ const originalTitle = this.get('api.originalTitle');
+ if (api.get('isNew') || (originalTitle && (api.title !== originalTitle))) {
+ refreshList = true;
+ }
+
+ if (api.get('isNew')) {
+ data['new'] = true;
+ };
+
+ let requiredParams;
+
+ 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) {
+ 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()}`, {
+ type: 'PUT',
+ data
+ }).catch(popupAjaxError)
+ .then(result => {
+ if (result.success) {
+ if (refreshList) {
+ this.transitionToRoute('adminWizardsApi', result.api.name.dasherize()).then(() => {
+ this.send('refreshModel');
+ });
+ } else {
+ this.set('api', CustomWizardApi.create(result.api));
+ this.set('responseIcon', 'check');
+ }
+ } else {
+ this.set('responseIcon', 'times');
+ }
+ }).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) {
+ this.transitionToRoute('adminWizardsApis').then(() => {
+ this.send('refreshModel');
+ });
+ }
+ }).finally(() => this.set('updating', false));
+ },
+
+ clearLogs() {
+ const name = this.get('api.name');
+ if (!name) return;
+
+ 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/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 46d0e411..f5022153 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: '/: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
new file mode 100644
index 00000000..b335b88a
--- /dev/null
+++ b/assets/javascripts/discourse/models/custom-wizard-api.js.es6
@@ -0,0 +1,62 @@
+import { ajax } from 'discourse/lib/ajax';
+import { default as computed } from 'ember-addons/ember-computed-decorators';
+
+const CustomWizardApi = Discourse.Model.extend({
+ @computed('name')
+ redirectUri(name) {
+ let nameParam = name.toString().dasherize();
+ const baseUrl = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: '');
+ return baseUrl + `/admin/wizards/apis/${nameParam}/redirect`;
+ }
+});
+
+CustomWizardApi.reopenClass({
+ create(params = {}) {
+ const api = this._super.apply(this);
+ const authorization = params.authorization || {};
+ const endpoints = params.endpoints;
+
+ api.setProperties({
+ name: params.name,
+ title: params.title,
+ originalTitle: params.title,
+ authType: authorization.auth_type,
+ authUrl: authorization.auth_url,
+ 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,
+ refreshToken: authorization.refresh_token,
+ code: authorization.code,
+ tokenExpiresAt: authorization.token_expires_at,
+ tokenRefreshAt: authorization.token_refresh_at,
+ endpoints: Ember.A(endpoints),
+ isNew: params.isNew,
+ log: params.log
+ });
+
+ return api;
+ },
+
+ find(name) {
+ return ajax(`/admin/wizards/apis/${name}`, {
+ type: 'GET'
+ }).then(result => {
+ return CustomWizardApi.create(result);
+ });
+ },
+
+ list() {
+ return ajax("/admin/wizards/apis", {
+ type: 'GET'
+ }).then(result => {
+ return result;
+ });
+ }
+});
+
+export default CustomWizardApi;
diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6
index 3141afc2..7e8ad927 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' });
@@ -129,6 +130,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/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/routes/admin-wizards-api.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6
new file mode 100644
index 00000000..58f624b5
--- /dev/null
+++ b/assets/javascripts/discourse/routes/admin-wizards-api.js.es6
@@ -0,0 +1,21 @@
+import CustomWizardApi from '../models/custom-wizard-api';
+
+export default Discourse.Route.extend({
+ queryParams: {
+ refresh_list: {
+ refreshModel: true
+ }
+ },
+
+ model(params) {
+ if (params.name === 'new') {
+ return CustomWizardApi.create({ isNew: true });
+ } else {
+ return CustomWizardApi.find(params.name);
+ }
+ },
+
+ setupController(controller, model){
+ controller.set("api", 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..54174c6b
--- /dev/null
+++ b/assets/javascripts/discourse/routes/admin-wizards-apis.js.es6
@@ -0,0 +1,31 @@
+import CustomWizardApi from '../models/custom-wizard-api';
+
+export default Discourse.Route.extend({
+ model() {
+ 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-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|}}
- {{f}} |
- {{/each}}
-
- {{#each submissions as |s|}}
-
- {{#each-in s as |k v|}}
- {{v}} |
- {{/each-in}}
-
- {{/each}}
-
-
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..160225ea
--- /dev/null
+++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs
@@ -0,0 +1,282 @@
+
+
+
+
+
+
+
+
+
+ {{#if showRedirectUri}}
+
+
+
+
+ {{api.redirectUri}}
+
+
+
+ {{/if}}
+
+
+
+
+ {{combo-box value=api.authType content=authorizationTypes none='admin.wizard.api.auth.type_none'}}
+
+
+
+ {{#if isOauth}}
+ {{#if threeLeggedOauth}}
+
+
+
+ {{input value=api.authUrl}}
+
+
+ {{/if}}
+
+
+
+
+ {{input value=api.tokenUrl}}
+
+
+
+
+
+
+ {{input value=api.clientId}}
+
+
+
+
+
+
+ {{input value=api.clientSecret}}
+
+
+
+
+
+
+ {{#each api.authParams as |param|}}
+
+ {{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=(action "removeParam") actionParam=param icon='times'}}
+
+ {{/each}}
+ {{d-button label='admin.wizard.api.auth.params.new' icon='plus' action=(action "addParam")}}
+
+
+ {{/if}}
+
+ {{#if isBasicAuth}}
+
+
+
+ {{input value=api.username}}
+
+
+
+
+
+
+ {{input value=api.password}}
+
+
+ {{/if}}
+
+
+ {{#if isOauth}}
+
+
+ {{#if api.authorized}}
+
+ {{i18n "admin.wizard.api.status.authorized"}}
+ {{else}}
+
+ {{i18n "admin.wizard.api.status.not_authorized"}}
+ {{/if}}
+
+
+
+
+ {{#if threeLeggedOauth}}
+
+
+
+ {{api.code}}
+
+
+ {{/if}}
+
+
+
+
+ {{api.accessToken}}
+
+
+
+ {{#if threeLeggedOauth}}
+
+
+
+ {{api.refreshToken}}
+
+
+ {{/if}}
+
+
+
+
+ {{api.tokenExpiresAt}}
+
+
+
+
+
+
+ {{api.tokenRefreshAt}}
+
+
+
+ {{/if}}
+
+
+
+
+
+ {{d-button action=(action "addEndpoint") label='admin.wizard.api.endpoint.add' icon='plus'}}
+
+ {{#if api.endpoints}}
+
+
+ {{#each api.endpoints as |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=(action "removeEndpoint")
+ actionParam=endpoint
+ icon='times'
+ class='remove-endpoint'}}
+
+
+
+ {{/each}}
+
+
+ {{/if}}
+
+
+
+
+
+
+
+ Datetime |
+ User |
+ Status |
+ URL |
+ Error |
+ {{#each api.log as |logentry|}}
+
+ {{logentry.time}} |
+
+
+ |
+ {{logentry.status}} |
+ {{logentry.url}} |
+ {{logentry.error}} |
+
+ {{/each}}
+
+
+
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..c5f67660
--- /dev/null
+++ b/assets/javascripts/discourse/templates/admin-wizards-apis.hbs
@@ -0,0 +1,26 @@
+
+
+
+ {{#each model as |api|}}
+ -
+ {{#link-to "adminWizardsApi" (dasherize api.name)}}
+ {{#if api.title}}
+ {{api.title}}
+ {{else}}
+ {{api.name}}
+ {{/if}}
+ {{/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/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs
index 4ef128c0..59fa22ab 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'}}
@@ -205,6 +205,44 @@
{{/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}}
+
{{#if addToGroup}}
diff --git a/assets/stylesheets/wizard_custom_admin.scss b/assets/stylesheets/wizard_custom_admin.scss
index 832188bd..0cfb4bb7 100644
--- a/assets/stylesheets/wizard_custom_admin.scss
+++ b/assets/stylesheets/wizard_custom_admin.scss
@@ -122,6 +122,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 {
@@ -312,6 +329,142 @@
}
}
-.wizard-step-contents{
- height: unset !important;
+.wizard-step-contents {
+ height: unset !important;
+}
+
+.admin-wizards-api {
+ margin-bottom: 40px;
+
+ .content-list {
+ margin-right: 20px;
+ }
+
+ .new-api {
+ margin-top: 20px;
+ }
+
+ .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 {
+ &.page {
+ margin-bottom: 20px;
+ }
+
+ .buttons {
+ float: right;
+ }
+
+ .wizard-header {
+ overflow: hidden;
+ }
+}
+
+.wizard-api-authentication {
+ display: flex;
+ background-color: $primary-very-low;
+ padding: 20px;
+ margin-bottom: 20px;
+
+ .settings {
+ 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 {
+ border-left: 1px solid $primary;
+ margin-left: 20px;
+ padding-left: 20px;
+ width: 50%;
+ max-width: 50%;
+
+ .wizard-header {
+ overflow: hidden;
+ }
+
+ .authorization {
+ float: right;
+ }
+
+ .control-group {
+ margin-bottom: 15px;
+ }
+ }
+}
+
+.wizard-api-endpoints {
+ background-color: $primary-very-low;
+ padding: 20px;
+ margin-bottom: 20px;
+
+ .endpoint-list {
+ margin-top: 20px;
+
+ ul {
+ margin: 0;
+ list-style: none;
+ }
+ }
+
+ .endpoint {
+ display: flex;
+ margin-top: 20px;
+
+ .combo-box {
+ width: 200px;
+ margin-right: 10px;
+ margin-top: -2px;
+ width: 150px;
+ }
+
+ input {
+ margin: 0;
+ margin-right: 10px;
+ }
+
+ .endpoint-url {
+ width: 300px;
+ }
+
+ .remove-endpoint {
+ margin-left: auto;
+ }
+ }
+}
+
+.wizard-api-log {
+ background-color: #f8f8f8;
+ padding: 20px;
+ margin-bottom: 20px;
+}
+
+.wizard-step-contents{
+ height: unset !important;
}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 194e1428..7634dcc5 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."
@@ -114,6 +115,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"
@@ -146,6 +149,64 @@ en:
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'
+ new: 'New Api'
+ name: "Name (can't be changed)"
+ name_placeholder: 'Underscored'
+ title: 'Title'
+ title_placeholder: 'Display name'
+ remove: 'Delete'
+ 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'
+ username: 'username'
+ password: 'password'
+ 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"
+ name: "Endpoint name"
+ method: "Select a method"
+ url: "Enter a url"
+
+ log:
+ label: "Logs"
wizard_js:
location:
diff --git a/controllers/api.rb b/controllers/api.rb
new file mode 100644
index 00000000..a4ed8e6d
--- /dev/null
+++ b/controllers/api.rb
@@ -0,0 +1,138 @@
+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
+ render_serialized(CustomWizard::Api.get(api_params[:name]), CustomWizard::ApiSerializer, root: false)
+ end
+
+ def save
+ current = CustomWizard::Api.get(api_params[:name])
+
+ if api_params[:new] && current
+ raise Discourse::InvalidParameters, "An API with that name already exists: '#{current.title || current.name}'"
+ end
+
+ PluginStoreRow.transaction do
+ 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
+
+ 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(
+ CustomWizard::Api.get(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])
+ CustomWizard::Api::LogEntry.clear(api_params[:name])
+ end
+
+ render json: success_json
+ end
+
+ def authorize
+ result = CustomWizard::Api::Authorization.get_token(api_params[:name])
+
+ if result.instance_variable_defined?(:@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
+ end
+
+ def redirect
+ params.require(:name)
+ params.require(:code)
+
+ CustomWizard::Api::Authorization.set(params[:name], code: params[:code])
+ CustomWizard::Api::Authorization.get_token(params[:name])
+
+ return redirect_to path('/admin/wizards/apis/' + params[:name])
+ end
+
+ private
+
+ def api_params
+ params.require(:name)
+
+ data = params.permit(
+ :name,
+ :title,
+ :auth_type,
+ :auth_url,
+ :token_url,
+ :client_id,
+ :client_secret,
+ :username,
+ :password,
+ :auth_params,
+ :endpoints,
+ :new
+ ).to_h
+
+ data[:name] = data[:name].underscore
+
+ @api_params ||= data
+ end
+
+ def auth_data
+ auth_data = api_params.slice(
+ :auth_type,
+ :auth_url,
+ :token_url,
+ :client_id,
+ :client_secret,
+ :username,
+ :password,
+ :auth_params
+ )
+
+ auth_data[:auth_params] = JSON.parse(auth_data[:auth_params]) if auth_data[:auth_params].present?
+
+ @auth_data ||= auth_data
+ end
+end
diff --git a/jobs/refresh_api_access_token.rb b/jobs/refresh_api_access_token.rb
new file mode 100644
index 00000000..54f6da55
--- /dev/null
+++ b/jobs/refresh_api_access_token.rb
@@ -0,0 +1,7 @@
+module Jobs
+ class RefreshApiAccessToken < Jobs::Base
+ def execute(args)
+ CustomWizard::Api::Authorization.get_token(args[:name], refresh: true)
+ end
+ end
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
new file mode 100644
index 00000000..a5d4c155
--- /dev/null
+++ b/lib/api/api.rb
@@ -0,0 +1,34 @@
+class CustomWizard::Api
+ include ActiveModel::SerializerSupport
+
+ attr_accessor :name,
+ :title
+
+ def initialize(name, data={})
+ @name = name
+ data.each do |k, v|
+ self.send "#{k}=", v if self.respond_to?(k)
+ end
+ 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 = 'metadata'")
+ .map do |record|
+ 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
new file mode 100644
index 00000000..745ecc9b
--- /dev/null
+++ b/lib/api/authorization.rb
@@ -0,0 +1,151 @@
+require 'excon'
+
+class CustomWizard::Api::Authorization
+ include ActiveModel::SerializerSupport
+
+ attr_accessor :api_name,
+ :authorized,
+ :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(api_name, data={})
+ @api_name = api_name
+
+ data.each do |k, v|
+ self.send "#{k}=", v if self.respond_to?(k)
+ end
+ end
+
+ def authorized
+ @authorized ||= @access_token && @token_expires_at.to_datetime > Time.now
+ 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|
+ data[k.to_sym] = v
+ end
+
+ PluginStore.set("custom_wizard_api_#{api_name}", 'authorization', data)
+
+ self.get(api_name)
+ 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
+ else
+ self.new(api_name, data)
+ end
+ else
+ nil
+ end
+ end
+
+ def self.remove(api_name)
+ PluginStore.remove("custom_wizard_api_#{api_name}", "authorization")
+ end
+
+ def self.get_header_authorization_string(name)
+ auth = CustomWizard::Api::Authorization.get(name)
+ raise Discourse::InvalidParameters.new(:name) unless auth.present?
+
+ 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
+ raise Discourse::InvalidParameters.new(auth.access_token) unless auth.access_token.present?
+ "Bearer #{auth.access_token}"
+ end
+ end
+
+ def self.get_token(name, opts = {})
+ authorization = CustomWizard::Api::Authorization.get(name)
+ type = authorization.auth_type
+
+ body = {}
+
+ 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"
+ },
+ :method => 'GET',
+ :query => URI.encode_www_form(body)
+ )
+ begin
+ result = connection.request()
+ log_params = {time: Time.now, user_id: 0, status: 'SUCCESS', url: authorization.token_url, error: ""}
+ CustomWizard::Api::LogEntry.set(name, log_params)
+ 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
+
+ self.handle_token_result(name, result)
+ end
+
+ def self.handle_token_result(name, result)
+ result_data = JSON.parse(result.body)
+
+ if result_data['error']
+ return result_data
+ end
+
+ data = {}
+
+ 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']
+
+ 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
+
+ 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/lib/api/endpoint.rb b/lib/api/endpoint.rb
new file mode 100644
index 00000000..3036a49f
--- /dev/null
+++ b/lib/api/endpoint.rb
@@ -0,0 +1,101 @@
+class CustomWizard::Api::Endpoint
+ include ActiveModel::SerializerSupport
+
+ attr_accessor :id,
+ :name,
+ :api_name,
+ :method,
+ :url
+
+ 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['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
+ end
+
+ PluginStore.set("custom_wizard_api_#{api_name}", "endpoint_#{endpoint_id}", data)
+
+ self.get(api_name, endpoint_id)
+ end
+
+ def self.get(api_name, endpoint_id, opts={})
+ return nil if !endpoint_id
+
+ if data = PluginStore.get("custom_wizard_api_#{api_name}", "endpoint_#{endpoint_id}")
+ if opts[:data_only]
+ data
+ else
+ data[:id] = endpoint_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 'endpoint_%'").destroy_all
+ end
+
+ 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'])
+ data[:id] = record['key'].split('_').last
+ self.new(api_name, data)
+ end
+ end
+
+ 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)
+
+ 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
+
+ begin
+ response = connection.request(params)
+ 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, 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
+ end
+end
diff --git a/lib/api/logentry.rb b/lib/api/logentry.rb
new file mode 100644
index 00000000..1408615d
--- /dev/null
+++ b/lib/api/logentry.rb
@@ -0,0 +1,88 @@
+class CustomWizard::Api::LogEntry
+ include ActiveModel::SerializerSupport
+
+ attr_accessor :log_id,
+ :time,
+ :user_id,
+ :status,
+ :url,
+ :error,
+ :username,
+ :userpath,
+ :name,
+ :avatar_template
+
+ 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
+ this_user = User.find_by(id: data['user_id'])
+ 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
+
+ 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/lib/builder.rb b/lib/builder.rb
index 45b191b4..8533c4c3 100644
--- a/lib/builder.rb
+++ b/lib/builder.rb
@@ -444,6 +444,28 @@ class CustomWizard::Builder
end
end
+ def send_to_api(user, action, data)
+ api_body = nil
+
+ if action['api_body'] != ""
+ begin
+ api_body_parsed = JSON.parse(action['api_body'])
+ 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)
+ end
+
+ 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
+ updater.errors.add(:send_to_api, error)
+ else
+ ## add validation callback
+ end
+ end
+
def add_to_group(user, action, data)
if group_id = data[action['group_id']]
if group = Group.find(group_id)
diff --git a/plugin.rb b/plugin.rb
index 231bb7e1..1709b7ea 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -54,6 +54,7 @@ after_initialize do
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'
@@ -66,6 +67,14 @@ 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' => 'api#list'
+ get 'admin/wizards/apis/new' => 'api#index'
+ 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'
+ get 'admin/wizards/apis/:name/authorize' => 'api#authorize'
end
end
@@ -81,6 +90,19 @@ after_initialize do
load File.expand_path('../controllers/steps.rb', __FILE__)
load File.expand_path('../controllers/admin.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('../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
if custom_wizard_redirect = current_user.custom_fields['redirect_to_wizard']
diff --git a/serializers/api/api_serializer.rb b/serializers/api/api_serializer.rb
new file mode 100644
index 00000000..9d7fba70
--- /dev/null
+++ b/serializers/api/api_serializer.rb
@@ -0,0 +1,34 @@
+class CustomWizard::ApiSerializer < ApplicationSerializer
+ attributes :name,
+ :title,
+ :authorization,
+ :endpoints,
+ :log
+
+ def authorization
+ if authorization = CustomWizard::Api::Authorization.get(object.name)
+ CustomWizard::Api::AuthorizationSerializer.new(
+ authorization,
+ root: false
+ )
+ end
+ end
+
+ def endpoints
+ if endpoints = CustomWizard::Api::Endpoint.list(object.name)
+ ActiveModel::ArraySerializer.new(
+ endpoints,
+ each_serializer: CustomWizard::Api::EndpointSerializer
+ )
+ 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/authorization_serializer.rb b/serializers/api/authorization_serializer.rb
new file mode 100644
index 00000000..2ca347b5
--- /dev/null
+++ b/serializers/api/authorization_serializer.rb
@@ -0,0 +1,16 @@
+class CustomWizard::Api::AuthorizationSerializer < ApplicationSerializer
+ attributes :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
diff --git a/serializers/api/basic_api_serializer.rb b/serializers/api/basic_api_serializer.rb
new file mode 100644
index 00000000..d0214d65
--- /dev/null
+++ b/serializers/api/basic_api_serializer.rb
@@ -0,0 +1,14 @@
+class CustomWizard::BasicApiSerializer < ApplicationSerializer
+ 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
diff --git a/serializers/api/endpoint_serializer.rb b/serializers/api/endpoint_serializer.rb
new file mode 100644
index 00000000..18c1406c
--- /dev/null
+++ b/serializers/api/endpoint_serializer.rb
@@ -0,0 +1,10 @@
+class CustomWizard::Api::EndpointSerializer < ApplicationSerializer
+ attributes :id,
+ :name,
+ :method,
+ :url
+
+ def method
+ object.send('method')
+ end
+end
diff --git a/serializers/api/log_serializer.rb b/serializers/api/log_serializer.rb
new file mode 100644
index 00000000..13ee1555
--- /dev/null
+++ b/serializers/api/log_serializer.rb
@@ -0,0 +1,12 @@
+class CustomWizard::Api::LogSerializer < ApplicationSerializer
+ attributes :log_id,
+ :time,
+ :status,
+ :url,
+ :error,
+ :user_id,
+ :username,
+ :userpath,
+ :name,
+ :avatar_template
+end