Spiegel von
https://github.com/paviliondev/discourse-custom-wizard.git
synchronisiert 2024-11-29 12:20:27 +01:00
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.
|
||||
|
||||
sudo: required
|
||||
#services:
|
||||
#names:
|
||||
#- docker
|
||||
|
||||
before_install:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
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('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({
|
||||
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());
|
||||
|
||||
|
|
|
@ -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') || [];
|
||||
|
|
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}}
|
||||
{{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}}
|
||||
|
||||
<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.wizard_fields'}}{{builderWizardFields}}</label>
|
||||
{{d-editor value=action.post_template
|
||||
placeholder='admin.wizard.action.post_builder.placeholder'
|
||||
placeholder='admin.wizard.action.interpolate_fields'
|
||||
classNames='post-builder-editor'}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -157,7 +157,7 @@
|
|||
<label>{{i18n 'admin.wizard.action.post_builder.user_fields'}}{{builderUserFields}}</label>
|
||||
<label>{{i18n 'admin.wizard.action.post_builder.wizard_fields'}}{{builderWizardFields}}</label>
|
||||
{{d-editor value=action.post_template
|
||||
placeholder='admin.wizard.action.post_builder.placeholder'
|
||||
placeholder='admin.wizard.action.interpolate_fields'
|
||||
classNames='post-builder-editor'}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -205,6 +205,44 @@
|
|||
</div>
|
||||
{{/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}}
|
||||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
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
|
||||
|
||||
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)
|
||||
|
|
22
plugin.rb
22
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']
|
||||
|
|
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