diff --git a/application/Espo/Controllers/ExternalAccount.php b/application/Espo/Controllers/ExternalAccount.php new file mode 100644 index 0000000000..3c4b2c887b --- /dev/null +++ b/application/Espo/Controllers/ExternalAccount.php @@ -0,0 +1,104 @@ +getEntityManager()->getRepository('Integration')->find(); + $arr = array(); + foreach ($integrations as $entity) { + if ($entity->get('enabled') && $this->getMetadata()->get('integrations.' . $entity->id .'.allowUserAccounts')) { + $arr[] = array( + 'id' => $entity->id + ); + } + } + return array( + 'list' => $arr + ); + } + + public function actionGetOAuthCredentials($params, $data, $request) + { + $id = $request->get('id'); + list($integration, $userId) = explode('__', $id); + + if (!$this->getUser()->isAdmin()) { + if ($this->getUser()->id != $userId) { + throw new Forbidden(); + } + } + + $entity = $this->getEntityManager()->getEntity('Integration', $integration); + if ($entity) { + return array( + 'clientId' => $entity->get('clientId'), + 'redirectUri' => $this->getConfig()->get('siteUrl') . '/oauthcallback' + ); + } + } + + public function actionRead($params, $data, $request) + { + list($integration, $userId) = explode('__', $params['id']); + + if (!$this->getUser()->isAdmin()) { + if ($this->getUser()->id != $userId) { + throw new Forbidden(); + } + } + + $entity = $this->getEntityManager()->getEntity('ExternalAccount', $params['id']); + return $entity->toArray(); + } + + public function actionUpdate($params, $data) + { + return $this->actionPatch($params, $data); + } + + public function actionPatch($params, $data) + { + list($integration, $userId) = explode('__', $params['id']); + + if (!$this->getUser()->isAdmin()) { + if ($this->getUser()->id != $userId) { + throw new Forbidden(); + } + } + + $entity = $this->getEntityManager()->getEntity('ExternalAccount', $params['id']); + $entity->set($data); + $this->getEntityManager()->saveEntity($entity); + + return $entity->toArray(); + } +} + diff --git a/application/Espo/Entities/ExternalAccount.php b/application/Espo/Entities/ExternalAccount.php new file mode 100644 index 0000000000..e52c51e256 --- /dev/null +++ b/application/Espo/Entities/ExternalAccount.php @@ -0,0 +1,28 @@ +get(); + $entity->id = $id; + } + return $entity; + } +} + diff --git a/application/Espo/Resources/i18n/en_US/ExternalAccount.json b/application/Espo/Resources/i18n/en_US/ExternalAccount.json new file mode 100644 index 0000000000..1478eede4d --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/ExternalAccount.json @@ -0,0 +1,7 @@ +{ + "labels": { + "Connect": "Connect" + }, + "help": { + } +} diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index f60cc725ef..b61bb915cd 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -7,7 +7,8 @@ "EmailTemplate": "Email Template", "EmailAccount": "Email Account", "OutboundEmail": "Outbound Email", - "ScheduledJob": "Scheduled Job" + "ScheduledJob": "Scheduled Job", + "ExternalAccount": "External Account" }, "scopeNamesPlural": { "Email": "Emails", @@ -17,7 +18,8 @@ "EmailTemplate": "Email Templates", "EmailAccount": "Email Accounts", "OutboundEmail": "Outbound Emails", - "ScheduledJob": "Scheduled Jobs" + "ScheduledJob": "Scheduled Jobs", + "ExternalAccount": "External Accounts" }, "labels": { "Misc": "Misc", diff --git a/application/Espo/Resources/i18n/en_US/Integration.json b/application/Espo/Resources/i18n/en_US/Integration.json index fe92108a0f..37aed3d2e5 100644 --- a/application/Espo/Resources/i18n/en_US/Integration.json +++ b/application/Espo/Resources/i18n/en_US/Integration.json @@ -2,7 +2,8 @@ "fields": { "enabled": "Enabled", "clientId": "Client ID", - "clientSecret": "Client Secret" + "clientSecret": "Client Secret", + "redirectUri": "Redirect URI" }, "messages": { "selectIntegration": "Select an integration in menu." diff --git a/application/Espo/Resources/metadata/clientDefs/ExternalAccount.json b/application/Espo/Resources/metadata/clientDefs/ExternalAccount.json new file mode 100644 index 0000000000..3a3b749aef --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/ExternalAccount.json @@ -0,0 +1,3 @@ +{ + "controller": "Controllers.ExternalAccount" +} diff --git a/application/Espo/Resources/metadata/entityDefs/ExternalAccount.json b/application/Espo/Resources/metadata/entityDefs/ExternalAccount.json new file mode 100644 index 0000000000..3dcdfa336b --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/ExternalAccount.json @@ -0,0 +1,10 @@ +{ + "fields": { + "data": { + "type": "text" + }, + "enabled": { + "type": "bool" + } + } +} diff --git a/application/Espo/Resources/metadata/integrations/Google.json b/application/Espo/Resources/metadata/integrations/Google.json index 176dc89fa4..c16428360d 100644 --- a/application/Espo/Resources/metadata/integrations/Google.json +++ b/application/Espo/Resources/metadata/integrations/Google.json @@ -13,10 +13,8 @@ }, "params": { "url": "https://accounts.google.com/o/oauth2/auth", - "scope": "profile calendar contacts" + "scope": "https://www.googleapis.com/auth/calendar" }, "allowUserAccounts": true, - "authMethod": "OAuth2", - "adminView": "Integrations.Google.Admin", - "userView": "Integrations.Google.User" + "authMethod": "OAuth2" } diff --git a/application/Espo/Resources/metadata/scopes/ExternalAccount.json b/application/Espo/Resources/metadata/scopes/ExternalAccount.json new file mode 100644 index 0000000000..6fb84e513f --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/ExternalAccount.json @@ -0,0 +1,7 @@ +{ + "entity":true, + "layouts":false, + "tab":false, + "acl":false, + "customizable":false +} diff --git a/frontend/client/res/templates/admin/integrations/edit.tpl b/frontend/client/res/templates/admin/integrations/edit.tpl index 9842ee3182..74c17e7faa 100644 --- a/frontend/client/res/templates/admin/integrations/edit.tpl +++ b/frontend/client/res/templates/admin/integrations/edit.tpl @@ -10,17 +10,17 @@
{{{enabled}}}
{{#each dataFieldList}} -
+
{{{var this ../this}}}
{{/each}}
-
- {{#if helpText}} - {{{../helpText}}} - {{/if}} + {{#if helpText}} +
+ {{{../helpText}}}
+ {{/if}}
diff --git a/frontend/client/res/templates/admin/integrations/oauth2.tpl b/frontend/client/res/templates/admin/integrations/oauth2.tpl new file mode 100644 index 0000000000..f661420a58 --- /dev/null +++ b/frontend/client/res/templates/admin/integrations/oauth2.tpl @@ -0,0 +1,32 @@ +
+ + +
+ +
+
+
+ +
{{{enabled}}}
+
+ {{#each dataFieldList}} +
+ +
{{{var this ../this}}}
+
+ {{/each}} +
+ +
+ +
+
+
+
+ {{#if helpText}} +
+ {{{../helpText}}} +
+ {{/if}} +
+
diff --git a/frontend/client/res/templates/external-account/index.tpl b/frontend/client/res/templates/external-account/index.tpl new file mode 100644 index 0000000000..761e7dcca7 --- /dev/null +++ b/frontend/client/res/templates/external-account/index.tpl @@ -0,0 +1,22 @@ + + +
+
+ + {{#unless externalAccountListCount}} + {{translate 'No Data'}} + {{/unless}} +
+ +
+

+
+ {{{content}}} +
+
+
+ diff --git a/frontend/client/res/templates/external-account/oauth2.tpl b/frontend/client/res/templates/external-account/oauth2.tpl new file mode 100644 index 0000000000..46b6a869c5 --- /dev/null +++ b/frontend/client/res/templates/external-account/oauth2.tpl @@ -0,0 +1,25 @@ +
+ + +
+ +
+
+
+
+ +
{{{enabled}}}
+
+
+
+ +
+
+
+ {{#if helpText}} +
+ {{{../helpText}}} +
+ {{/if}} +
+
diff --git a/frontend/client/src/controllers/external-account.js b/frontend/client/src/controllers/external-account.js new file mode 100644 index 0000000000..e05b4bbc02 --- /dev/null +++ b/frontend/client/src/controllers/external-account.js @@ -0,0 +1,53 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + ************************************************************************/ + +Espo.define('Controllers.ExternalAccount', 'Controller', function (Dep) { + + return Dep.extend({ + + defaultAction: 'list', + + list: function (options) { + this.collectionFactory.create('ExternalAccount', function (collection) { + collection.once('sync', function () { + this.main('ExternalAccount.Index', { + collection: collection, + }); + }, this); + collection.fetch(); + }, this); + }, + + edit: function (options) { + var id = options.id; + + this.collectionFactory.create('ExternalAccount', function (collection) { + collection.once('sync', function () { + this.main('ExternalAccount.Index', { + collection: collection, + id: id + }); + }, this); + collection.fetch(); + }, this); + }, + }); +}); diff --git a/frontend/client/src/language.js b/frontend/client/src/language.js index 677018428d..b571c3606f 100644 --- a/frontend/client/src/language.js +++ b/frontend/client/src/language.js @@ -31,6 +31,16 @@ _.extend(Espo.Language.prototype, { cache: null, url: 'I18n', + + has: function (name, category, scope) { + if (scope in this.data) { + if (category in this.data[scope]) { + if (name in this.data[scope][category]) { + return true; + } + } + } + }, get: function (scope, category, name) { if (scope in this.data) { diff --git a/frontend/client/src/views/admin/integrations/edit.js b/frontend/client/src/views/admin/integrations/edit.js index aa7f537665..272bbbefa9 100644 --- a/frontend/client/src/views/admin/integrations/edit.js +++ b/frontend/client/src/views/admin/integrations/edit.js @@ -46,7 +46,7 @@ Espo.define('Views.Admin.Integrations.Edit', 'View', function (Dep) { this.integration = this.options.integration; this.helpText = false; - if (this.getLanguage().get(this.integration, 'help', 'Integration') != this.integration) { + if (this.getLanguage().has(this.integration, 'help', 'Integration')) { this.helpText = this.translate(this.integration, 'help', 'Integration'); } @@ -70,7 +70,7 @@ Espo.define('Views.Admin.Integrations.Edit', 'View', function (Dep) { this.wait(true); - var fields = this.fields = this.getMetadata().get('integrations.' + this.integration + '.fields') + var fields = this.fields = this.getMetadata().get('integrations.' + this.integration + '.fields'); Object.keys(this.fields).forEach(function (name) { this.model.defs.fields[name] = this.fields[name]; @@ -88,8 +88,7 @@ Espo.define('Views.Admin.Integrations.Edit', 'View', function (Dep) { this.wait(false); }, this); - this.model.fetch(); - + this.model.fetch(); }, hideField: function (name) { diff --git a/frontend/client/src/views/admin/integrations/index.js b/frontend/client/src/views/admin/integrations/index.js index 09c5c7626a..8605dc398d 100644 --- a/frontend/client/src/views/admin/integrations/index.js +++ b/frontend/client/src/views/admin/integrations/index.js @@ -54,7 +54,6 @@ Espo.define('Views.Admin.Integrations.Index', 'View', function (Dep) { this.renderDefaultPage(); } else { this.openIntegration(this.integration); - } }); }, @@ -64,9 +63,9 @@ Espo.define('Views.Admin.Integrations.Index', 'View', function (Dep) { this.getRouter().navigate('#Admin/integrations/name=' + integration, {trigger: false}); - // TODO get viewName + var viewName = 'Admin.Integrations.' + this.getMetadata().get('integrations.' + integration + '.authMethod'); this.notify('Loading...'); - this.createView('content', 'Admin.Integrations.Edit', { + this.createView('content', viewName, { el: '#integration-content', integration: integration, }, function (view) { @@ -88,7 +87,6 @@ Espo.define('Views.Admin.Integrations.Index', 'View', function (Dep) { return; } $('#integration-header').show().html(this.integration); - }, updatePageTitle: function () { diff --git a/frontend/client/src/views/admin/integrations/oauth2.js b/frontend/client/src/views/admin/integrations/oauth2.js new file mode 100644 index 0000000000..623604980e --- /dev/null +++ b/frontend/client/src/views/admin/integrations/oauth2.js @@ -0,0 +1,37 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + ************************************************************************/ + +Espo.define('Views.Admin.Integrations.OAuth2', 'Views.Admin.Integrations.Edit', function (Dep) { + + return Dep.extend({ + + template: 'admin.integrations.oauth2', + + data: function () { + + return _.extend({ + redirectUri: this.getConfig().get('siteUrl') + '/oauthcallback' + }, Dep.prototype.data.call(this)); + }, + + }); + +}); diff --git a/frontend/client/src/views/external-account/index.js b/frontend/client/src/views/external-account/index.js new file mode 100644 index 0000000000..c21dd65219 --- /dev/null +++ b/frontend/client/src/views/external-account/index.js @@ -0,0 +1,106 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + ************************************************************************/ + +Espo.define('Views.ExternalAccount.Index', 'View', function (Dep) { + + return Dep.extend({ + + template: 'external-account.index', + + data: function () { + return { + externalAccountList: this.externalAccountList, + id: this.id, + externalAccountListCount: this.externalAccountList.length + }; + }, + + events: { + 'click #external-account-menu a.external-account-link': function (e) { + var id = $(e.currentTarget).data('id') + '__' + this.userId; + this.openExternalAccount(id); + }, + }, + + setup: function () { + this.externalAccountList = this.collection.toJSON(); + + this.userId = this.getUser().id; + this.id = this.options.id || null; + if (this.id) { + this.userId = this.id.split('__')[1]; + } + + this.on('after:render', function () { + this.renderHeader(); + if (!this.id) { + this.renderDefaultPage(); + } else { + this.openExternalAccount(this.id); + } + }); + }, + + openExternalAccount: function (id) { + this.id = id; + + var integration = this.integration = id.split('__')[0]; + this.userId = id.split('__')[1]; + + this.getRouter().navigate('#ExternalAccount/edit/' + id, {trigger: false}); + + var viewName = + this.getMetadata().get('integrations.' + integration + '.userView') || + 'ExternalAccount.' + this.getMetadata().get('integrations.' + integration + '.authMethod'); + + this.notify('Loading...'); + this.createView('content', viewName, { + el: '#external-account-content', + id: id, + integration: integration + }, function (view) { + this.renderHeader(); + view.render(); + this.notify(false); + $(window).scrollTop(0); + }.bind(this)); + }, + + renderDefaultPage: function () { + $('#external-account-header').html('').hide(); + $('#external-account-content').html(''); + }, + + renderHeader: function () { + if (!this.id) { + $('#external-account-header').html(''); + return; + } + $('#external-account-header').show().html(this.integration); + }, + + updatePageTitle: function () { + this.setPageTitle(this.translate('ExternalAccount', 'scopeNamesPlural')); + }, + }); +}); + + diff --git a/frontend/client/src/views/external-account/oauth2.js b/frontend/client/src/views/external-account/oauth2.js new file mode 100644 index 0000000000..512457ad86 --- /dev/null +++ b/frontend/client/src/views/external-account/oauth2.js @@ -0,0 +1,243 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + ************************************************************************/ + +Espo.define('Views.ExternalAccount.OAuth2', 'View', function (Dep) { + + return Dep.extend({ + + template: 'external-account.oauth2', + + events: { + + }, + + data: function () { + return { + integration: this.integration, + helpText: this.helpText + }; + }, + + events: { + 'click button[data-action="cancel"]': function () { + this.getRouter().navigate('#ExternalAccount', {trigger: true}); + }, + 'click button[data-action="save"]': function () { + this.save(); + }, + 'click [data-action="connect"]': function () { + this.connect(); + } + }, + + setup: function () { + this.integration = this.options.integration; + this.id = this.options.id; + + + this.helpText = false; + if (this.getLanguage().has(this.integration, 'help', 'ExternalAccount')) { + this.helpText = this.translate(this.integration, 'help', 'ExternalAccount'); + } + + this.fieldList = []; + + this.dataFieldList = []; + + this.model = new Espo.Model(); + this.model.id = this.id; + this.model.name = 'ExternalAccount'; + this.model.urlRoot = 'ExternalAccount'; + + this.model.defs = { + fields: { + enabled: { + required: true, + type: 'bool' + }, + } + }; + + this.wait(true); + + this.model.populateDefaults(); + + this.listenToOnce(this.model, 'sync', function () { + this.createFieldView('bool', 'enabled'); + + $.ajax({ + url: 'ExternalAccount/action/getOAuthCredentials?id=' + this.id, + dataType: 'json' + }).done(function (respose) { + this.clientId = respose.clientId; + this.redirectUri = respose.redirectUri; + this.wait(false); + }.bind(this)); + + }, this); + + this.model.fetch(); + }, + + hideField: function (name) { + this.$el.find('label.field-label-' + name).addClass('hide'); + this.$el.find('div.field-' + name).addClass('hide'); + var view = this.getView(name); + if (view) { + view.enabled = false; + } + }, + + showField: function (name) { + this.$el.find('label.field-label-' + name).removeClass('hide'); + this.$el.find('div.field-' + name).removeClass('hide'); + var view = this.getView(name); + if (view) { + view.enabled = true; + } + }, + + afterRender: function () { + if (!this.model.get('enabled')) { + this.$el.find('.data-panel').addClass('hidden'); + } + + this.listenTo(this.model, 'change:enabled', function () { + if (this.model.get('enabled')) { + this.$el.find('.data-panel').removeClass('hidden'); + } else { + this.$el.find('.data-panel').addClass('hidden'); + } + }, this); + }, + + createFieldView: function (type, name, readOnly, params) { + this.createView(name, this.getFieldManager().getViewName(type), { + model: this.model, + el: this.options.el + ' .field-' + name, + defs: { + name: name, + params: params + }, + mode: readOnly ? 'detail' : 'edit', + readOnly: readOnly, + }); + this.fieldList.push(name); + }, + + save: function () { + this.fieldList.forEach(function (field) { + var view = this.getView(field); + if (!view.readOnly) { + view.fetchToModel(); + } + }, this); + + var notValid = false; + this.fieldList.forEach(function (field) { + notValid = this.getView(field).validate() || notValid; + }, this); + + if (notValid) { + this.notify('Not valid', 'error'); + return; + } + + this.listenToOnce(this.model, 'sync', function () { + this.notify('Saved', 'success'); + }, this); + + this.notify('Saving...'); + this.model.save(); + }, + + popup: function (options, callback) { + options.windowName = options.windowName || 'ConnectWithOAuth'; + options.windowOptions = options.windowOptions || 'location=0,status=0,width=800,height=400'; + options.callback = options.callback || function(){ window.location.reload(); }; + + var self = this; + + var path = options.path; + + var arr = []; + var params = (options.params || {}); + for (var name in params) { + if (params[name]) { + arr.push(name + '=' + encodeURI(params[name])); + } + } + path += '?' + arr.join('&'); + + var parseUrl = function (str) { + var accessToken = false; + var expires = false; + + str = str.substr(str.indexOf('#') + 1, str.length); + str.split('&').forEach(function (part) { + var arr = part.split('='); + var name = decodeURI(arr[0]); + var value = decodeURI(arr[1] || ''); + + if (name == 'access_token') { + accessToken = value; + } + if (name == 'expires') { + expires = value; + } + }, this); + if (accessToken) { + return { + accessToken: accessToken, + expires: expires + } + } + } + + popup = window.open(path, options.windowName, options.windowOptions); + interval = window.setInterval(function () { + if (popup.closed) { + window.clearInterval(interval); + } else { + var res = parseUrl(popup.location.href.toString()); + callback.call(self, res.accessToken, res.expires); + popup.close(); + } + }, 500); + }, + + connect: function () { + this.popup({ + path: this.getMetadata().get('integrations.' + this.integration + '.params.url'), + params: { + client_id: this.clientId, + redirect_uri: this.redirectUri, + scope: this.getMetadata().get('integrations.' + this.integration + '.params.scope'), + response_type: 'token' + } + }, function (accessToken, expires) { + + }); + }, + + }); + +});