Merge pull request #12 from angusmcleod/api_authentication
Api authentication
Dieser Commit ist enthalten in:
Commit
91a9d30a8a
31 geänderte Dateien mit 1592 neuen und 22 gelöschten Zeilen
|
@ -1,7 +1,7 @@
|
||||||
# Uncomment tests runner when tests are added.
|
# Uncomment tests runner when tests are added.
|
||||||
|
|
||||||
sudo: required
|
sudo: required
|
||||||
#services:
|
#names:
|
||||||
#- docker
|
#- docker
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
|
|
|
@ -4,6 +4,7 @@ const ACTION_TYPES = [
|
||||||
{ id: 'create_topic', name: 'Create Topic' },
|
{ id: 'create_topic', name: 'Create Topic' },
|
||||||
{ id: 'update_profile', name: 'Update Profile' },
|
{ 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' }
|
||||||
{ id: 'add_to_group', name: 'Add to Group' },
|
{ id: 'add_to_group', name: 'Add to Group' },
|
||||||
{ id: 'route_to', name: 'Route To' }
|
{ id: 'route_to', name: 'Route To' }
|
||||||
];
|
];
|
||||||
|
@ -28,6 +29,8 @@ export default Ember.Component.extend({
|
||||||
createTopic: Ember.computed.equal('action.type', 'create_topic'),
|
createTopic: Ember.computed.equal('action.type', 'create_topic'),
|
||||||
updateProfile: Ember.computed.equal('action.type', 'update_profile'),
|
updateProfile: Ember.computed.equal('action.type', 'update_profile'),
|
||||||
sendMessage: Ember.computed.equal('action.type', 'send_message'),
|
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'),
|
addToGroup: Ember.computed.equal('action.type', 'add_to_group'),
|
||||||
routeTo: Ember.computed.equal('action.type', 'route_to'),
|
routeTo: Ember.computed.equal('action.type', 'route_to'),
|
||||||
disableId: Ember.computed.not('action.isNew'),
|
disableId: Ember.computed.not('action.isNew'),
|
||||||
|
@ -54,5 +57,21 @@ export default Ember.Component.extend({
|
||||||
toggleCustomCategoryWizardField() {
|
toggleCustomCategoryWizardField() {
|
||||||
const user = this.get('action.custom_category_user_field');
|
const user = this.get('action.custom_category_user_field');
|
||||||
if (user) this.set('action.custom_category_wizard_field', false);
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
216
assets/javascripts/discourse/controllers/admin-wizards-api.js.es6
Normale Datei
216
assets/javascripts/discourse/controllers/admin-wizards-api.js.es6
Normale Datei
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
queryParams: ['refresh']
|
||||||
|
});
|
|
@ -8,6 +8,9 @@ export default {
|
||||||
this.route('adminWizardsSubmissions', { path: '/submissions', resetNamespace: true }, function() {
|
this.route('adminWizardsSubmissions', { path: '/submissions', resetNamespace: true }, function() {
|
||||||
this.route('adminWizardSubmissions', { path: '/:wizard_id', resetNamespace: true });
|
this.route('adminWizardSubmissions', { path: '/:wizard_id', resetNamespace: true });
|
||||||
});
|
});
|
||||||
|
this.route('adminWizardsApis', { path: '/apis', resetNamespace: true }, function() {
|
||||||
|
this.route('adminWizardsApi', { path: '/:name', resetNamespace: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
62
assets/javascripts/discourse/models/custom-wizard-api.js.es6
Normale Datei
62
assets/javascripts/discourse/models/custom-wizard-api.js.es6
Normale Datei
|
@ -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;
|
|
@ -17,6 +17,7 @@ const wizardProperties = [
|
||||||
const CustomWizard = Discourse.Model.extend({
|
const CustomWizard = Discourse.Model.extend({
|
||||||
save() {
|
save() {
|
||||||
return new Ember.RSVP.Promise((resolve, reject) => {
|
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
|
||||||
const id = this.get('id');
|
const id = this.get('id');
|
||||||
if (!id || !id.underscore()) return reject({ error: 'id_required' });
|
if (!id || !id.underscore()) return reject({ error: 'id_required' });
|
||||||
|
|
||||||
|
@ -129,6 +130,16 @@ const CustomWizard = Discourse.Model.extend({
|
||||||
error = 'id_required';
|
error = 'id_required';
|
||||||
return;
|
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());
|
a.set('id', id.underscore());
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,8 @@ export default Discourse.Route.extend({
|
||||||
afterModel(model) {
|
afterModel(model) {
|
||||||
return Ember.RSVP.all([
|
return Ember.RSVP.all([
|
||||||
this._getFieldTypes(model),
|
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) {
|
setupController(controller, model) {
|
||||||
const newWizard = this.get('newWizard');
|
const newWizard = this.get('newWizard');
|
||||||
const steps = model.get('steps') || [];
|
const steps = model.get('steps') || [];
|
||||||
|
|
21
assets/javascripts/discourse/routes/admin-wizards-api.js.es6
Normale Datei
21
assets/javascripts/discourse/routes/admin-wizards-api.js.es6
Normale Datei
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
31
assets/javascripts/discourse/routes/admin-wizards-apis.js.es6
Normale Datei
31
assets/javascripts/discourse/routes/admin-wizards-apis.js.es6
Normale Datei
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,16 +0,0 @@
|
||||||
<div class="wizard-submissions">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
{{#each fields as |f|}}
|
|
||||||
<th>{{f}}</th>
|
|
||||||
{{/each}}
|
|
||||||
</tr>
|
|
||||||
{{#each submissions as |s|}}
|
|
||||||
<tr>
|
|
||||||
{{#each-in s as |k v|}}
|
|
||||||
<td>{{v}}</td>
|
|
||||||
{{/each-in}}
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
282
assets/javascripts/discourse/templates/admin-wizards-api.hbs
Normale Datei
282
assets/javascripts/discourse/templates/admin-wizards-api.hbs
Normale Datei
|
@ -0,0 +1,282 @@
|
||||||
|
<div class="wizard-api-header page">
|
||||||
|
<div class='buttons'>
|
||||||
|
{{#if updating}}
|
||||||
|
{{loading-spinner size="small"}}
|
||||||
|
{{else}}
|
||||||
|
{{#if responseIcon}}
|
||||||
|
{{d-icon responseIcon}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{d-button label="admin.wizard.api.save" action=(action "save") class="btn-primary" disabled=saveDisabled}}
|
||||||
|
|
||||||
|
{{#if showRemove}}
|
||||||
|
{{d-button action=(action "remove") label="admin.wizard.api.remove"}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if error}}
|
||||||
|
<div class="error">
|
||||||
|
{{error}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-header">
|
||||||
|
{{#if api.isNew}}
|
||||||
|
{{i18n 'admin.wizard.api.new'}}
|
||||||
|
{{else}}
|
||||||
|
{{api.title}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="title">
|
||||||
|
<label>{{i18n 'admin.wizard.api.title'}}</label>
|
||||||
|
{{input value=api.title placeholder=(i18n 'admin.wizard.api.title_placeholder')}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="name">
|
||||||
|
<label>{{i18n 'admin.wizard.api.name'}}</label>
|
||||||
|
{{#if api.isNew}}
|
||||||
|
{{input value=api.name placeholder=(i18n 'admin.wizard.api.name_placeholder')}}
|
||||||
|
{{else}}
|
||||||
|
{{api.name}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-api-header">
|
||||||
|
<div class="buttons">
|
||||||
|
{{#if isOauth}}
|
||||||
|
{{#if authorizing}}
|
||||||
|
{{loading-spinner size="small"}}
|
||||||
|
{{else}}
|
||||||
|
{{#if authErrorMessage}}
|
||||||
|
<span>{{authErrorMessage}}</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{d-button label="admin.wizard.api.auth.btn"
|
||||||
|
action=(action "authorize")
|
||||||
|
disabled=authDisabled
|
||||||
|
class="btn-primary"}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-header">
|
||||||
|
{{i18n 'admin.wizard.api.auth.label'}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-api-authentication">
|
||||||
|
<div class="settings">
|
||||||
|
|
||||||
|
<div class="wizard-header medium">
|
||||||
|
{{i18n 'admin.wizard.api.auth.settings'}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if showRedirectUri}}
|
||||||
|
<div class="control-group redirect-uri">
|
||||||
|
<div class="control-label">
|
||||||
|
<label>{{i18n 'admin.wizard.api.auth.redirect_uri'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{api.redirectUri}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="control-group auth-type">
|
||||||
|
<label>{{i18n 'admin.wizard.api.auth.type'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{combo-box value=api.authType content=authorizationTypes none='admin.wizard.api.auth.type_none'}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if isOauth}}
|
||||||
|
{{#if threeLeggedOauth}}
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.auth.url'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{input value=api.authUrl}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.auth.token_url'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{input value=api.tokenUrl}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.auth.client_id'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{input value=api.clientId}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.auth.client_secret'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{input value=api.clientSecret}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.auth.params.label'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{#each api.authParams as |param|}}
|
||||||
|
<div class="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'}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
{{d-button label='admin.wizard.api.auth.params.new' icon='plus' action=(action "addParam")}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isBasicAuth}}
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.auth.username'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{input value=api.username}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.auth.password'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{input value=api.password}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if isOauth}}
|
||||||
|
<div class="status">
|
||||||
|
<div class="authorization">
|
||||||
|
{{#if api.authorized}}
|
||||||
|
<span class="authorization-indicator authorized"></span>
|
||||||
|
<span>{{i18n "admin.wizard.api.status.authorized"}}</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="authorization-indicator not-authorized"></span>
|
||||||
|
<span>{{i18n "admin.wizard.api.status.not_authorized"}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-header medium">
|
||||||
|
{{i18n 'admin.wizard.api.status.label'}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if threeLeggedOauth}}
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.status.code'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{api.code}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.status.access_token'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{api.accessToken}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if threeLeggedOauth}}
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.status.refresh_token'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{api.refreshToken}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.status.expires_at'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{api.tokenExpiresAt}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n 'admin.wizard.api.status.refresh_at'}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{api.tokenRefreshAt}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-header">
|
||||||
|
{{i18n 'admin.wizard.api.endpoint.label'}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-api-endpoints">
|
||||||
|
{{d-button action=(action "addEndpoint") label='admin.wizard.api.endpoint.add' icon='plus'}}
|
||||||
|
|
||||||
|
{{#if api.endpoints}}
|
||||||
|
<div class="endpoint-list">
|
||||||
|
<ul>
|
||||||
|
{{#each api.endpoints as |endpoint|}}
|
||||||
|
<li>
|
||||||
|
<div class="endpoint">
|
||||||
|
<div class="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'}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-header">
|
||||||
|
{{i18n 'admin.wizard.api.log.label'}}
|
||||||
|
{{d-button action=(action "clearLogs")
|
||||||
|
icon='trash-alt'
|
||||||
|
class='clear-logs'}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-api-log">
|
||||||
|
<div class="log-list">
|
||||||
|
<table class="wizard-api-log-table">
|
||||||
|
<th>Datetime</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Error</th>
|
||||||
|
{{#each api.log as |logentry|}}
|
||||||
|
<tr>
|
||||||
|
<td>{{logentry.time}}</td>
|
||||||
|
<td class="user-image">
|
||||||
|
<div class="user-image-inner">
|
||||||
|
<a href="{{unbound logentry.userpath}}" data-user-card="{{unbound logentry.username}}">{{avatar logentry imageSize="large"}}</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{logentry.status}}</td>
|
||||||
|
<td>{{logentry.url}}</td>
|
||||||
|
<td>{{logentry.error}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
26
assets/javascripts/discourse/templates/admin-wizards-apis.hbs
Normale Datei
26
assets/javascripts/discourse/templates/admin-wizards-apis.hbs
Normale Datei
|
@ -0,0 +1,26 @@
|
||||||
|
<div class='row'>
|
||||||
|
<div class='content-list wizard-list'>
|
||||||
|
<ul>
|
||||||
|
{{#each model as |api|}}
|
||||||
|
<li>
|
||||||
|
{{#link-to "adminWizardsApi" (dasherize api.name)}}
|
||||||
|
{{#if api.title}}
|
||||||
|
{{api.title}}
|
||||||
|
{{else}}
|
||||||
|
{{api.name}}
|
||||||
|
{{/if}}
|
||||||
|
{{/link-to}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<div class="new-api">
|
||||||
|
{{#link-to 'adminWizardsApi' 'new' class="btn"}}
|
||||||
|
{{d-icon "plus"}} {{i18n 'admin.wizard.api.new'}}
|
||||||
|
{{/link-to}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
{{outlet}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,6 +1,7 @@
|
||||||
{{#admin-nav}}
|
{{#admin-nav}}
|
||||||
{{nav-item route='adminWizardsCustom' label='admin.wizard.custom_label'}}
|
{{nav-item route='adminWizardsCustom' label='admin.wizard.custom_label'}}
|
||||||
{{nav-item route='adminWizardsSubmissions' label='admin.wizard.submissions_label'}}
|
{{nav-item route='adminWizardsSubmissions' label='admin.wizard.submissions_label'}}
|
||||||
|
{{nav-item route='adminWizardsApis' label='admin.wizard.api.nav_label'}}
|
||||||
{{/admin-nav}}
|
{{/admin-nav}}
|
||||||
|
|
||||||
<div class="admin-container">
|
<div class="admin-container">
|
||||||
|
|
|
@ -96,7 +96,7 @@
|
||||||
<label>{{i18n 'admin.wizard.action.post_builder.user_fields'}}{{builderUserFields}}</label>
|
<label>{{i18n 'admin.wizard.action.post_builder.user_fields'}}{{builderUserFields}}</label>
|
||||||
<label>{{i18n 'admin.wizard.action.post_builder.wizard_fields'}}{{builderWizardFields}}</label>
|
<label>{{i18n 'admin.wizard.action.post_builder.wizard_fields'}}{{builderWizardFields}}</label>
|
||||||
{{d-editor value=action.post_template
|
{{d-editor value=action.post_template
|
||||||
placeholder='admin.wizard.action.post_builder.placeholder'
|
placeholder='admin.wizard.action.interpolate_fields'
|
||||||
classNames='post-builder-editor'}}
|
classNames='post-builder-editor'}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,7 +157,7 @@
|
||||||
<label>{{i18n 'admin.wizard.action.post_builder.user_fields'}}{{builderUserFields}}</label>
|
<label>{{i18n 'admin.wizard.action.post_builder.user_fields'}}{{builderUserFields}}</label>
|
||||||
<label>{{i18n 'admin.wizard.action.post_builder.wizard_fields'}}{{builderWizardFields}}</label>
|
<label>{{i18n 'admin.wizard.action.post_builder.wizard_fields'}}{{builderWizardFields}}</label>
|
||||||
{{d-editor value=action.post_template
|
{{d-editor value=action.post_template
|
||||||
placeholder='admin.wizard.action.post_builder.placeholder'
|
placeholder='admin.wizard.action.interpolate_fields'
|
||||||
classNames='post-builder-editor'}}
|
classNames='post-builder-editor'}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -205,6 +205,44 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if sendToApi}}
|
||||||
|
<div class="setting">
|
||||||
|
<div class="setting-label">
|
||||||
|
<h3>{{i18n "admin.wizard.action.send_to_api.api"}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="setting-value">
|
||||||
|
{{combo-box value=action.api
|
||||||
|
content=availableApis
|
||||||
|
none='admin.wizard.action.send_to_api.select_an_api'
|
||||||
|
isDisabled=action.custom_title_enabled}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting">
|
||||||
|
<div class="setting-label">
|
||||||
|
<h3>{{i18n "admin.wizard.action.send_to_api.endpoint"}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="setting-value">
|
||||||
|
{{combo-box value=action.api_endpoint
|
||||||
|
content=availableEndpoints
|
||||||
|
none='admin.wizard.action.send_to_api.select_an_endpoint'
|
||||||
|
isDisabled=apiEmpty}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting api-body">
|
||||||
|
<div class="setting-label">
|
||||||
|
<h3>{{i18n "admin.wizard.action.send_to_api.body"}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="setting-value">
|
||||||
|
<label>{{i18n 'admin.wizard.action.post_builder.user_fields'}}{{builderUserFields}}</label>
|
||||||
|
<label>{{i18n 'admin.wizard.action.post_builder.wizard_fields'}}{{builderWizardFields}}</label>
|
||||||
|
{{textarea value=action.api_body
|
||||||
|
placeholder=(i18n 'admin.wizard.action.interpolate_fields')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if addToGroup}}
|
{{#if addToGroup}}
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<div class="setting-label">
|
<div class="setting-label">
|
||||||
|
|
|
@ -122,6 +122,23 @@
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
padding: 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 {
|
.wizard-links {
|
||||||
|
@ -315,3 +332,139 @@
|
||||||
.wizard-step-contents {
|
.wizard-step-contents {
|
||||||
height: unset !important;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ en:
|
||||||
name_required: "Wizards must have a name."
|
name_required: "Wizards must have a name."
|
||||||
steps_required: "Wizards must have at least one step."
|
steps_required: "Wizards must have at least one step."
|
||||||
id_required: "All wizards, steps, fields and actions need an id."
|
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."
|
type_required: "All fields need a type."
|
||||||
after_time_need_time: "After time is enabled but no time is set."
|
after_time_need_time: "After time is enabled but no time is set."
|
||||||
after_time_invalid: "After time is invalid."
|
after_time_invalid: "After time is invalid."
|
||||||
|
@ -114,6 +115,8 @@ en:
|
||||||
add_fields: "{{type}} Fields"
|
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."
|
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"
|
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:
|
skip_redirect:
|
||||||
label: "Skip Redirect"
|
label: "Skip Redirect"
|
||||||
description: "Don't redirect the user to this {{type}} after the wizard completes"
|
description: "Don't redirect the user to this {{type}} after the wizard completes"
|
||||||
|
@ -146,6 +149,64 @@ en:
|
||||||
label: "Custom Category"
|
label: "Custom Category"
|
||||||
wizard_field: "Wizard Field"
|
wizard_field: "Wizard Field"
|
||||||
user_field: "User 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:
|
wizard_js:
|
||||||
location:
|
location:
|
||||||
|
|
138
controllers/api.rb
Normale Datei
138
controllers/api.rb
Normale Datei
|
@ -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
|
7
jobs/refresh_api_access_token.rb
Normale Datei
7
jobs/refresh_api_access_token.rb
Normale Datei
|
@ -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
|
34
lib/api/api.rb
Normale Datei
34
lib/api/api.rb
Normale Datei
|
@ -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
|
151
lib/api/authorization.rb
Normale Datei
151
lib/api/authorization.rb
Normale Datei
|
@ -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
|
101
lib/api/endpoint.rb
Normale Datei
101
lib/api/endpoint.rb
Normale Datei
|
@ -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
|
88
lib/api/logentry.rb
Normale Datei
88
lib/api/logentry.rb
Normale Datei
|
@ -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
|
|
@ -444,6 +444,28 @@ class CustomWizard::Builder
|
||||||
end
|
end
|
||||||
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)
|
def add_to_group(user, action, data)
|
||||||
if group_id = data[action['group_id']]
|
if group_id = data[action['group_id']]
|
||||||
if group = Group.find(group_id)
|
if group = Group.find(group_id)
|
||||||
|
|
22
plugin.rb
22
plugin.rb
|
@ -54,6 +54,7 @@ after_initialize do
|
||||||
require_dependency 'admin_constraint'
|
require_dependency 'admin_constraint'
|
||||||
Discourse::Application.routes.append do
|
Discourse::Application.routes.append do
|
||||||
mount ::CustomWizard::Engine, at: 'w'
|
mount ::CustomWizard::Engine, at: 'w'
|
||||||
|
post 'wizard/authorization/callback' => "custom_wizard/authorization#callback"
|
||||||
|
|
||||||
scope module: 'custom_wizard', constraints: AdminConstraint.new do
|
scope module: 'custom_wizard', constraints: AdminConstraint.new do
|
||||||
get 'admin/wizards' => 'admin#index'
|
get 'admin/wizards' => 'admin#index'
|
||||||
|
@ -66,6 +67,14 @@ after_initialize do
|
||||||
delete 'admin/wizards/custom/remove' => 'admin#remove'
|
delete 'admin/wizards/custom/remove' => 'admin#remove'
|
||||||
get 'admin/wizards/submissions' => 'admin#index'
|
get 'admin/wizards/submissions' => 'admin#index'
|
||||||
get 'admin/wizards/submissions/:wizard_id' => 'admin#submissions'
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -81,6 +90,19 @@ after_initialize do
|
||||||
load File.expand_path('../controllers/steps.rb', __FILE__)
|
load File.expand_path('../controllers/steps.rb', __FILE__)
|
||||||
load File.expand_path('../controllers/admin.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
|
::UsersController.class_eval do
|
||||||
def wizard_path
|
def wizard_path
|
||||||
if custom_wizard_redirect = current_user.custom_fields['redirect_to_wizard']
|
if custom_wizard_redirect = current_user.custom_fields['redirect_to_wizard']
|
||||||
|
|
34
serializers/api/api_serializer.rb
Normale Datei
34
serializers/api/api_serializer.rb
Normale Datei
|
@ -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
|
16
serializers/api/authorization_serializer.rb
Normale Datei
16
serializers/api/authorization_serializer.rb
Normale Datei
|
@ -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
|
14
serializers/api/basic_api_serializer.rb
Normale Datei
14
serializers/api/basic_api_serializer.rb
Normale Datei
|
@ -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
|
4
serializers/api/basic_endpoint_serializer.rb
Normale Datei
4
serializers/api/basic_endpoint_serializer.rb
Normale Datei
|
@ -0,0 +1,4 @@
|
||||||
|
class CustomWizard::Api::BasicEndpointSerializer < ApplicationSerializer
|
||||||
|
attributes :id,
|
||||||
|
:name
|
||||||
|
end
|
10
serializers/api/endpoint_serializer.rb
Normale Datei
10
serializers/api/endpoint_serializer.rb
Normale Datei
|
@ -0,0 +1,10 @@
|
||||||
|
class CustomWizard::Api::EndpointSerializer < ApplicationSerializer
|
||||||
|
attributes :id,
|
||||||
|
:name,
|
||||||
|
:method,
|
||||||
|
:url
|
||||||
|
|
||||||
|
def method
|
||||||
|
object.send('method')
|
||||||
|
end
|
||||||
|
end
|
12
serializers/api/log_serializer.rb
Normale Datei
12
serializers/api/log_serializer.rb
Normale Datei
|
@ -0,0 +1,12 @@
|
||||||
|
class CustomWizard::Api::LogSerializer < ApplicationSerializer
|
||||||
|
attributes :log_id,
|
||||||
|
:time,
|
||||||
|
:status,
|
||||||
|
:url,
|
||||||
|
:error,
|
||||||
|
:user_id,
|
||||||
|
:username,
|
||||||
|
:userpath,
|
||||||
|
:name,
|
||||||
|
:avatar_template
|
||||||
|
end
|
Laden …
In neuem Issue referenzieren