oauth providers

This commit is contained in:
Yuri Kuznetsov
2025-03-25 14:15:39 +02:00
parent 9043d37a08
commit 2af34da28f
56 changed files with 2737 additions and 7 deletions

View File

@@ -0,0 +1,72 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldProcessing\OAuthAccount;
use Espo\Core\FieldProcessing\Loader;
use Espo\Core\FieldProcessing\Loader\Params;
use Espo\Entities\OAuthAccount;
use Espo\ORM\Entity;
use Espo\Tools\OAuth\ConfigDataProvider;
/**
* @implements Loader<OAuthAccount>
*/
class DataLoader implements Loader
{
public function __construct(
private ConfigDataProvider $configDataProvider,
) {}
public function process(Entity $entity, Params $params): void
{
if (!$entity->get('providerId')) {
return;
}
$provider = $entity->getProvider();
$scope = null;
if ($provider->getScopes()) {
$scope = implode($provider->getScopeSeparator() ?? ' ', $provider->getScopes());
}
$data = [
'endpoint' => $provider->getAuthorizationEndpoint(),
'clientId' => $provider->getClientId(),
'redirectUri' => $this->configDataProvider->getRedirectUri(),
'scope' => $scope,
'prompt' => $provider->getAuthorizationPrompt(),
'params' => $provider->getAuthorizationParams(),
];
$entity->set('data', $data);
}
}

View File

@@ -0,0 +1,51 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldProcessing\OAuthProvider;
use Espo\Core\FieldProcessing\Loader;
use Espo\Core\FieldProcessing\Loader\Params;
use Espo\Entities\OAuthProvider;
use Espo\ORM\Entity;
use Espo\Tools\OAuth\ConfigDataProvider;
/**
* @implements Loader<OAuthProvider>
*/
class AuthorizationRedirectUriLoader implements Loader
{
public function __construct(
private ConfigDataProvider $configDataProvider,
) {}
public function process(Entity $entity, Params $params): void
{
$entity->set('authorizationRedirectUri', $this->configDataProvider->getRedirectUri());
}
}

View File

@@ -0,0 +1,71 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\Record\OAuthProvider;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Record\Input\Data;
use Espo\Core\Record\Input\Filter;
use Espo\Core\Utils\Crypt;
/**
* @noinspection PhpUnused
*/
class GeneralFilter implements Filter
{
private const ATTR_CLIENT_SECRET = 'clientSecret';
public function __construct(private Crypt $crypt) {}
/**
* @throws BadRequest
*/
public function filter(Data $data): void
{
$this->processClientSecret($data);
}
/**
* @throws BadRequest
*/
private function processClientSecret(Data $data): void
{
$value = $data->get(self::ATTR_CLIENT_SECRET);
if ($value === null) {
return;
}
if (!is_string($value)) {
throw new BadRequest();
}
$data->set(self::ATTR_CLIENT_SECRET, $this->crypt->encrypt($value));
}
}

View File

@@ -0,0 +1,47 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use Espo\Core\Controllers\RecordBase;
/**
* @noinspection PhpUnused
*/
class OAuthAccount extends RecordBase
{
protected function checkAccess(): bool
{
if (!$this->user->isAdmin()) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use Espo\Core\Controllers\Record;
/**
* @noinspection PhpUnused
*/
class OAuthProvider extends Record
{
protected function checkAccess(): bool
{
if (!$this->user->isAdmin()) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\ExtGroup\OauthGroup;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Tools\OAuth\Exceptions\AccountNotFound;
use Espo\Tools\OAuth\Exceptions\NoToken;
use Espo\Tools\OAuth\Exceptions\ProviderNotAvailable;
use Espo\Tools\OAuth\Exceptions\TokenObtainingFailure;
use Espo\Tools\OAuth\TokensProvider;
/**
* @noinspection PhpUnused
*/
class GetAccessTokenType implements Func
{
public function __construct(
private TokensProvider $provider,
) {}
public function process(EvaluatedArgumentList $arguments): string
{
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$id = $arguments[0];
if (!is_string($id)) {
throw BadArgumentType::create(1, 'string');
}
try {
$tokens = $this->provider->get($arguments[0]);
} catch (AccountNotFound|NoToken|ProviderNotAvailable|TokenObtainingFailure $e) {
$message = "Could not obtain access token for OAuth account $id.";
throw new Error($message, 500, $e);
}
$accessToken = $tokens->getAccessToken();
if (!$accessToken) {
throw new Error("No access token.");
}
return $accessToken;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
/** @noinspection PhpMultipleClassDeclarationsInspection */
namespace Espo\Entities;
use Espo\Core\Field\DateTime;
use Espo\Core\ORM\Entity;
use ValueError;
class OAuthAccount extends Entity
{
public const ENTITY_TYPE = 'OAuthAccount';
public function getProvider(): OAuthProvider
{
$provider = $this->relations->getOne('provider');
if (!$provider instanceof OAuthProvider) {
throw new ValueError("No provider.");
}
return $provider;
}
public function getAccessToken(): ?string
{
return $this->get('accessToken');
}
public function getRefreshToken(): ?string
{
return $this->get('refreshToken');
}
public function getExpiresAt(): ?DateTime
{
/** @var ?DateTime */
return $this->getValueObject('expiresAt');
}
public function setAccessToken(?string $accessToken): self
{
return $this->set('accessToken', $accessToken);
}
public function setRefreshToken(?string $refreshToken): self
{
return $this->set('refreshToken', $refreshToken);
}
public function setExpiresAt(?DateTime $expiresAt): self
{
return $this->setValueObject('expiresAt', $expiresAt);
}
}

View File

@@ -0,0 +1,118 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
/** @noinspection PhpMultipleClassDeclarationsInspection */
namespace Espo\Entities;
use Espo\Core\ORM\Entity;
use stdClass;
use ValueError;
class OAuthProvider extends Entity
{
public const ENTITY_TYPE = 'OAuthProvider';
public function isActive(): bool
{
return $this->get('isActive');
}
public function getClientId(): string
{
$value = $this->get('clientId');
if (!is_string($value)) {
throw new ValueError("No client ID.");
}
return $value;
}
public function getClientSecret(): string
{
$value = $this->get('clientSecret');
if (!is_string($value)) {
throw new ValueError("No client secret.");
}
return $value;
}
public function getTokenEndpoint(): string
{
$value = $this->get('tokenEndpoint');
if (!is_string($value)) {
throw new ValueError("No token endpoint.");
}
return $value;
}
public function getAuthorizationEndpoint(): string
{
$value = $this->get('authorizationEndpoint');
if (!is_string($value)) {
throw new ValueError("No authorization endpoint.");
}
return $value;
}
public function getResourceEndpoint(): ?string
{
return $this->get('resourceEndpoint');
}
/**
* @return string[]
*/
public function getScopes(): array
{
return $this->get('scopes') ?? [];
}
public function getScopeSeparator(): ?string
{
return $this->get('scopeSeparator');
}
public function getAuthorizationPrompt(): string
{
return $this->get('authorizationPrompt');
}
public function getAuthorizationParams(): ?stdClass
{
return $this->get('authorizationParams') ?? null;
}
}

View File

@@ -82,6 +82,7 @@
"Group Email Folders": "Group Email Folders",
"Authentication Providers": "Authentication Providers",
"Address Countries": "Address Countries",
"OAuth Providers": "OAuth Providers",
"Success": "Success",
"Fail": "Fail",
"Configuration Instructions": "Configuration Instructions",
@@ -318,7 +319,8 @@
"sms": "SMS settings.",
"pdfTemplates": "Templates for printing to PDF.",
"formulaSandbox": "Write and test formula scripts.",
"addressCountries": "Countries available for address fields."
"addressCountries": "Countries available for address fields.",
"oAuthProviders": "OAuth providers for integrations."
},
"keywords": {
"settings": "system",

View File

@@ -64,7 +64,9 @@
"AuthenticationProvider": "Authentication Provider",
"GlobalStream": "Global Stream",
"AddressCountry": "Address Country",
"AppSecret": "App Secret"
"AppSecret": "App Secret",
"OAuthProvider": "OAuth Provider",
"OAuthAccount": "OAuth Account"
},
"scopeNamesPlural": {
"Note": "Notes",
@@ -119,7 +121,9 @@
"AuthenticationProvider": "Authentication Providers",
"GlobalStream": "Global Stream",
"AddressCountry": "Address Countries",
"AppSecret": "App Secrets"
"AppSecret": "App Secrets",
"OAuthProvider": "OAuth Providers",
"OAuthAccount": "OAuth Accounts"
},
"labels": {
"Previous Page": "Previous Page",

View File

@@ -0,0 +1,16 @@
{
"labels": {
"Create OAuthAccount": "Create OAuth Account",
"Connection": "Connection"
},
"fields": {
"provider": "Provider",
"hasAccessToken": "Has Access Token",
"user": "User",
"providerIsActive": "Provider is Active",
"data": "Data"
},
"links": {
"provider": "Provider"
}
}

View File

@@ -0,0 +1,25 @@
{
"labels": {
"Create OAuthProvider": "Create OAuth Provider"
},
"fields": {
"isActive": "Is Active",
"clientId": "Client ID",
"clientSecret": "Client Secret",
"authorizationEndpoint": "Authorization Endpoint",
"tokenEndpoint": "Token Endpoint",
"resourceEndpoint": "Resource Endpoint",
"authorizationRedirectUri": "Authorization Redirect URI",
"scopes": "Scopes",
"scopeSeparator": "Scope Separator",
"hasAccessToken": "Has Access Token",
"authorizationPrompt": "Authorization Prompt",
"authorizationParams": "Authorization Params"
},
"links": {
"accounts": "Accounts"
},
"tooltips": {
"authorizationParams": "Additional query parameters to be sent to the authorization endpoint. Specified in JSON format."
}
}

View File

@@ -0,0 +1,13 @@
[
{
"rows": [
[{"name": "name"}, {"name": "provider"}],
[{"name": "user"}, {"name": "hasAccessToken"}]
]
},
{
"rows": [
[{"name": "description"}]
]
}
]

View File

@@ -0,0 +1,14 @@
[
{
"rows": [
[{"name": "name"}],
[{"name": "provider"}],
[{"name": "user"}, {"name": "hasAccessToken"}]
]
},
{
"rows": [
[{"name": "description"}]
]
}
]

View File

@@ -0,0 +1,10 @@
[
{
"name": "name",
"link": true
},
{
"name": "provider",
"width": 50
}
]

View File

@@ -0,0 +1,10 @@
[
{
"name": "name",
"link": true
},
{
"name": "hasAccessToken",
"width": 50
}
]

View File

@@ -0,0 +1,10 @@
[
{
"name": "name",
"link": true
},
{
"name": "provider",
"width": 50
}
]

View File

@@ -0,0 +1,5 @@
{
"accounts": {
"index": 0
}
}

View File

@@ -0,0 +1,27 @@
[
{
"rows": [
[{"name": "name"}, {"name": "isActive"}],
[{"name": "clientId"}, {"name": "clientSecret"}],
[{"name": "authorizationRedirectUri"}, false]
]
},
{
"rows": [
[{"name": "authorizationEndpoint"}, {"name": "tokenEndpoint"}],
[{"name": "resourceEndpoint"}, false]
]
},
{
"rows": [
[{"name": "scopes"}],
[{"name": "scopeSeparator"}, false],
[{"name": "authorizationParams"}, {"name": "authorizationPrompt"}]
]
},
{
"rows": [
[{"name": "description"}]
]
}
]

View File

@@ -0,0 +1,7 @@
[
{
"rows": [
[{"name": "name"}, {"name": "isActive"}]
]
}
]

View File

@@ -0,0 +1,10 @@
[
{
"name": "name",
"link": true
},
{
"name": "isActive",
"width": 50
}
]

View File

@@ -0,0 +1,10 @@
[
{
"name": "name",
"link": true
},
{
"name": "isActive",
"width": 50
}
]

View File

@@ -350,6 +350,12 @@
"iconClass": "fas fa-key",
"description": "appSecrets"
},
{
"url": "#Admin/oAuthProviders",
"label": "OAuth Providers",
"iconClass": "fas fa-sign-in-alt",
"description": "oAuthProviders"
},
{
"url": "#Admin/appLog",
"label": "App Log",

View File

@@ -625,6 +625,12 @@
"insertText": "ext\\acl\\getPermissionLevel(USER_ID, PERMISSION)",
"returnType": "string",
"unsafe": true
},
{
"name": "ext\\oauth\\getAccessToken",
"insertText": "ext\\oauth\\getAccessToken(ID)",
"returnType": "string",
"unsafe": true
}
],
"functionClassNameMap": {
@@ -637,6 +643,7 @@
"ext\\acl\\getLevel": "Espo\\Core\\Formula\\Functions\\ExtGroup\\AclGroup\\GetLevelType",
"ext\\acl\\getPermissionLevel": "Espo\\Core\\Formula\\Functions\\ExtGroup\\AclGroup\\GetPermissionLevelType",
"util\\base64Encode": "Espo\\Core\\Formula\\Functions\\UtilGroup\\Base64EncodeType",
"util\\base64Decode": "Espo\\Core\\Formula\\Functions\\UtilGroup\\Base64DecodeType"
"util\\base64Decode": "Espo\\Core\\Formula\\Functions\\UtilGroup\\Base64DecodeType",
"ext\\oauth\\getAccessToken": "Espo\\Core\\Formula\\Functions\\ExtGroup\\OauthGroup\\GetAccessTokenType"
}
}

View File

@@ -158,6 +158,11 @@
"exportsTo": "ace.require.define.modules",
"exportsAs": "ace/mode/javascript"
},
"ace-mode-json": {
"path": "client/lib/ace-mode-json.js",
"exportsTo": "ace.require.define.modules",
"exportsAs": "ace/mode/json"
},
"ace-ext-language_tools": {
"path": "client/lib/ace-ext-language_tools.js",
"exportsTo": "ace.require.define.modules",

View File

@@ -0,0 +1,13 @@
{
"controller": "controllers/record",
"sidePanels": {
"detail": [
{
"name": "connection",
"label": "Connection",
"view": "views/o-auth-account/records/panels/connection",
"notRefreshable": true
}
]
}
}

View File

@@ -0,0 +1,64 @@
{
"controller": "controllers/record",
"relationshipPanels": {
"accounts": {
"layout": "listForProvider",
"selectDisabled": true,
"unlinkDisabled": true
}
},
"dynamicLogic": {
"fields": {
"authorizationRedirectUri": {
"visible": {
"conditionGroup": [
{
"type": "isNotEmpty",
"attribute": "id"
}
]
}
},
"clientId": {
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "isActive"
}
]
}
},
"clientSecret": {
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "isActive"
}
]
}
},
"authorizationEndpoint": {
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "isActive"
}
]
}
},
"tokenEndpoint": {
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "isActive"
}
]
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
{
"fields": {
"accessToken": {
"forbidden": true
},
"refreshToken": {
"forbidden": true
},
"expiresAt": {
"forbidden": true
}
}
}

View File

@@ -0,0 +1,7 @@
{
"fields": {
"clientSecret": {
"internal": true
}
}
}

View File

@@ -0,0 +1,91 @@
{
"fields": {
"name": {
"type": "varchar",
"required": true,
"maxLength": 100
},
"provider": {
"type": "link",
"required": true,
"readOnlyAfterCreate": true
},
"user": {
"type": "link",
"readOnly": true
},
"hasAccessToken": {
"type": "bool",
"readOnly": true,
"notStorable": true,
"orderDisabled": true,
"directAccessDisabled": true,
"select": {
"select": "IS_NOT_NULL:(accessToken)"
}
},
"providerIsActive": {
"type": "foreign",
"link": "provider",
"field": "isActive"
},
"data": {
"type": "jsonObject",
"notStorable": true,
"directAccessDisabled": true,
"readOnly": true
},
"accessToken": {
"type": "password",
"readOnly": true,
"dbType": "text"
},
"refreshToken": {
"type": "password",
"readOnly": true,
"dbType": "text"
},
"description": {
"type": "text"
},
"expiresAt": {
"type": "datetime",
"readOnly": true
},
"createdAt": {
"type": "datetime",
"readOnly": true
},
"modifiedAt": {
"type": "datetime",
"readOnly": true
},
"createdBy": {
"type": "link",
"readOnly": true
},
"modifiedBy": {
"type": "link",
"readOnly": true
}
},
"links": {
"provider": {
"type": "belongsTo",
"entity": "OAuthProvider",
"foreign": "accounts"
},
"user": {
"type": "belongsTo",
"entity": "User"
},
"createdBy": {
"type": "belongsTo",
"entity": "User"
},
"modifiedBy": {
"type": "belongsTo",
"entity": "User"
}
}
}

View File

@@ -0,0 +1,109 @@
{
"fields": {
"name": {
"type": "varchar",
"required": true,
"maxLength": 100
},
"isActive": {
"type": "bool",
"default": true
},
"clientId": {
"type": "varchar",
"maxLength": 150
},
"clientSecret": {
"type": "password",
"maxLength": 512,
"dbType": "text"
},
"authorizationEndpoint": {
"type": "url",
"maxLength": 512,
"dbType": "text",
"strip": false
},
"tokenEndpoint": {
"type": "url",
"maxLength": 512,
"dbType": "text",
"strip": false
},
"resourceEndpoint": {
"type": "url",
"maxLength": 512,
"dbType": "text",
"strip": false
},
"authorizationRedirectUri": {
"type": "url",
"notStorable": true,
"readOnly": true,
"copyToClipboard": true,
"directAccessDisabled": true
},
"authorizationPrompt": {
"type": "enum",
"default": "none",
"options": [
"none",
"consent",
"login",
"select_account"
],
"maxLength": 14
},
"scopes": {
"type": "array",
"noEmptyString": true,
"allowCustomOptions": true,
"storeArrayValues": false,
"displayAsList": true
},
"authorizationParams": {
"type": "jsonObject",
"view": "views/o-auth-provider/fields/authorization-params",
"tooltip": true
},
"scopeSeparator": {
"type": "varchar",
"maxLength": 1
},
"description": {
"type": "text"
},
"createdAt": {
"type": "datetime",
"readOnly": true
},
"modifiedAt": {
"type": "datetime",
"readOnly": true
},
"createdBy": {
"type": "link",
"readOnly": true
},
"modifiedBy": {
"type": "link",
"readOnly": true
}
},
"links": {
"accounts": {
"type": "hasMany",
"entity": "OAuthAccount",
"foreign": "provider",
"readOnly": true
},
"createdBy": {
"type": "belongsTo",
"entity": "User"
},
"modifiedBy": {
"type": "belongsTo",
"entity": "User"
}
}
}

View File

@@ -0,0 +1,5 @@
{
"readLoaderClassNameList": [
"Espo\\Classes\\FieldProcessing\\OAuthAccount\\DataLoader"
]
}

View File

@@ -0,0 +1,12 @@
{
"readLoaderClassNameList": [
"Espo\\Classes\\FieldProcessing\\OAuthProvider\\AuthorizationRedirectUriLoader"
],
"createInputFilterClassNameList": [
"Espo\\Classes\\Record\\OAuthProvider\\GeneralFilter"
],
"updateInputFilterClassNameList": [
"Espo\\Classes\\Record\\OAuthProvider\\GeneralFilter"
],
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
}

View File

@@ -0,0 +1,3 @@
{
"entity": true
}

View File

@@ -0,0 +1,6 @@
{
"entity": true,
"duplicateCheckFieldList": [
"name"
]
}

View File

@@ -485,6 +485,16 @@
},
"noAuth": true
},
{
"method": "post",
"route": "/OAuth/:id/connection",
"actionClassName": "Espo\\Tools\\OAuth\\Api\\PostConnection"
},
{
"method": "delete",
"route": "/OAuth/:id/connection",
"actionClassName": "Espo\\Tools\\OAuth\\Api\\DeleteConnection"
},
{
"route": "/:controller/:id",
"method": "get",

View File

@@ -0,0 +1,61 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth\Api;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Record\EntityProvider;
use Espo\Entities\OAuthAccount;
use Espo\Tools\OAuth\ConnectionService;
/**
* @noinspection PhpUnused
*/
class DeleteConnection implements Action
{
public function __construct(
private ConnectionService $service,
private EntityProvider $entityProvider,
) {}
public function process(Request $request): Response
{
$id = $request->getRouteParam('id') ?? throw new BadRequest();
$account = $this->entityProvider->getByClass(OAuthAccount::class, $id);
$this->service->disconnect($account);
return ResponseComposer::json(true);
}
}

View File

@@ -0,0 +1,66 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth\Api;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Record\EntityProvider;
use Espo\Entities\OAuthAccount;
use Espo\Tools\OAuth\ConnectionService;
/**
* @noinspection PhpUnused
*/
class PostConnection implements Action
{
public function __construct(
private ConnectionService $service,
private EntityProvider $entityProvider,
) {}
public function process(Request $request): Response
{
$id = $request->getRouteParam('id') ?? throw new BadRequest();
$code = $request->getParsedBody()->code ?? null;
if (!is_string($code)) {
throw new BadRequest("No code.");
}
$account = $this->entityProvider->getByClass(OAuthAccount::class, $id);
$this->service->connect($account, $code);
return ResponseComposer::json(true);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth;
use Espo\Core\Utils\Config\ApplicationConfig;
class ConfigDataProvider
{
public function __construct(
private ApplicationConfig $config,
) {}
public function getRedirectUri(): string
{
return $this->config->getSiteUrl() . '/oauth/callback';
}
}

View File

@@ -0,0 +1,82 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Entities\OAuthAccount;
use Espo\ORM\EntityManager;
use GuzzleHttp\Exception\GuzzleException;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
class ConnectionService
{
public function __construct(
private EntityManager $entityManager,
private GenericProviderFactory $genericProviderFactory,
private TokenSetter $tokenSetter,
) {}
/**
* @throws Forbidden
* @throws Error
*/
public function connect(OAuthAccount $account, string $code): void
{
$provider = $account->getProvider();
if (!$provider->isActive()) {
throw new Forbidden("Provider is not active.");
}
$genericProvider = $this->genericProviderFactory->create($provider);
try {
$tokens = $genericProvider->getAccessToken('authorization_code', ['code' => $code]);
} catch (GuzzleException $e) {
throw new Error("Token request error.", 500, $e);
} catch (IdentityProviderException $e) {
throw new Error("Token request response error.", 500, $e);
}
$this->tokenSetter->set($account, $tokens);
$this->entityManager->saveEntity($account);
}
public function disconnect(OAuthAccount $account): void
{
$account->setAccessToken(null);
$account->setRefreshToken(null);
$account->setExpiresAt(null);
$this->entityManager->saveEntity($account);
}
}

View File

@@ -0,0 +1,33 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth\Exceptions;
class AccountNotFound extends OAuthException
{}

View File

@@ -0,0 +1,33 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth\Exceptions;
class NoToken extends OAuthException
{}

View File

@@ -0,0 +1,35 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth\Exceptions;
use Exception;
class OAuthException extends Exception
{}

View File

@@ -0,0 +1,33 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth\Exceptions;
class ProviderNotAvailable extends OAuthException
{}

View File

@@ -0,0 +1,33 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth\Exceptions;
class TokenObtainingFailure extends OAuthException
{}

View File

@@ -0,0 +1,60 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth;
use Espo\Core\Utils\Crypt;
use Espo\Entities\OAuthProvider;
use League\OAuth2\Client\Provider\GenericProvider;
/**
* @internal
*/
class GenericProviderFactory
{
public function __construct(
private ConfigDataProvider $configDataProvider,
private Crypt $crypt,
) {}
public function create(OAuthProvider $provider): GenericProvider
{
$secret = $this->crypt->decrypt($provider->getClientSecret());
return new GenericProvider([
'clientId' => $provider->getClientId(),
'clientSecret' => $secret,
'redirectUri' => $this->configDataProvider->getRedirectUri(),
'urlAccessToken' => $provider->getTokenEndpoint(),
'urlAuthorize' => 'dummy',
'urlResourceOwnerDetails' => 'dummy',
]);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\Crypt;
use Espo\Entities\OAuthAccount;
use League\OAuth2\Client\Token\AccessTokenInterface;
/**
* @internal
*/
class TokenSetter
{
public function __construct(
private Crypt $crypt,
) {}
public function set(OAuthAccount $account, AccessTokenInterface $tokens): void
{
$accessToken = $this->crypt->encrypt($tokens->getToken());
$refreshToken = $tokens->getRefreshToken() ?
$this->crypt->encrypt($tokens->getRefreshToken()) :
null;
$expires = $tokens->getExpires() !== null ?
DateTime::fromTimestamp($tokens->getExpires()) :
null;
$account->setAccessToken($accessToken);
$account->setRefreshToken($refreshToken);
$account->setExpiresAt($expires);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth;
use Espo\Core\Field\DateTime;
use SensitiveParameter;
/**
* @immutable
*/
class Tokens
{
public function __construct(
#[SensitiveParameter]
private string $accessToken,
#[SensitiveParameter]
private ?string $refreshToken,
private ?DateTime $expiresAt,
) {}
public function getAccessToken(): string
{
return $this->accessToken;
}
public function getRefreshToken(): ?string
{
return $this->refreshToken;
}
public function getExpiresAt(): ?DateTime
{
return $this->expiresAt;
}
}

View File

@@ -0,0 +1,177 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OAuth;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\Crypt;
use Espo\Entities\OAuthAccount;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\SelectBuilder;
use Espo\Tools\OAuth\Exceptions\NoToken;
use Espo\Tools\OAuth\Exceptions\ProviderNotAvailable;
use Espo\Tools\OAuth\Exceptions\AccountNotFound;
use Espo\Tools\OAuth\Exceptions\TokenObtainingFailure;
use GuzzleHttp\Exception\GuzzleException;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use LogicException;
class TokensProvider
{
private const EXPIRATION_LEAD_TIME = 60;
public function __construct(
private EntityManager $entityManager,
private GenericProviderFactory $genericProviderFactory,
private TokenSetter $tokenSetter,
private Crypt $crypt,
) {}
/**
* @throws AccountNotFound
* @throws ProviderNotAvailable
* @throws NoToken
* @throws TokenObtainingFailure
*/
public function get(string $id): Tokens
{
$account = $this->fetch($id);
if (
$account->getRefreshToken() &&
$account->getExpiresAt() &&
$account->getExpiresAt()->isGreaterThan(
DateTime::createNow()->addSeconds(- self::EXPIRATION_LEAD_TIME)
)
) {
$this->refresh($account);
}
if (!$account->getAccessToken()) {
throw new NoToken();
}
$accessToken = $this->crypt->decrypt($account->getAccessToken());
$refreshToken = $account->getRefreshToken() ?
$this->crypt->decrypt($account->getRefreshToken()) :
null;
return new Tokens(
accessToken: $accessToken,
refreshToken: $refreshToken,
expiresAt: $account->getExpiresAt(),
);
}
/**
* @throws ProviderNotAvailable
* @throws AccountNotFound
* @throws NoToken
*/
private function fetch(string $id): OAuthAccount
{
// Ensuring the token is not being refreshed.
$this->entityManager->getTransactionManager()->start();
$account = $this->entityManager
->getRDBRepositoryByClass(OAuthAccount::class)
->clone(
SelectBuilder::create()
->from(OAuthAccount::ENTITY_TYPE)
->forShare()
->build()
)
->where([Attribute::ID => $id])
->findOne();
$this->entityManager->getTransactionManager()->commit();
if (!$account) {
throw new AccountNotFound();
}
if (!$account->getProvider()->isActive()) {
throw new ProviderNotAvailable();
}
if (!$account->getAccessToken()) {
throw new NoToken();
}
return $account;
}
/**
* @throws TokenObtainingFailure
* @noinspection PhpDocRedundantThrowsInspection
*/
private function refresh(OAuthAccount $account): void
{
$this->entityManager
->getTransactionManager()
->run(function () use ($account) {
$this->refreshInTransaction($account);
});
}
/**
* @throws TokenObtainingFailure
*/
private function refreshInTransaction(OAuthAccount $account): void
{
$refreshToken = $account->getRefreshToken();
if (!$refreshToken) {
throw new LogicException();
}
$refreshToken = $this->crypt->decrypt($refreshToken);
$this->entityManager
->getRDBRepositoryByClass(OAuthAccount::class)
->forUpdate()
->sth()
->where([Attribute::ID => $account->getId()])
->find();
$genericProvider = $this->genericProviderFactory->create($account->getProvider());
try {
$tokens = $genericProvider->getAccessToken('refresh_token', ['refresh_token' => $refreshToken]);
} catch (GuzzleException|IdentityProviderException $e) {
throw new TokenObtainingFailure($e->getMessage(), $e->getCode(), $e);
}
$this->tokenSetter->set($account, $tokens);
$this->entityManager->saveEntity($account);
}
}

View File

@@ -398,6 +398,11 @@ class AdminController extends Controller {
this.getRouter().dispatch('AppSecret', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionOAuthProviders() {
this.getRouter().dispatch('OAuthProvider', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionJobs() {
this.collectionFactory.create('Job', collection => {

View File

@@ -0,0 +1,307 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import SidePanelView from 'views/record/panels/side';
export default class OAuthAccountConnectionPanelView extends SidePanelView {
// language=Handlebars
templateContent = `
{{#if hasDisconnect}}
<div class="margin-bottom">
<span
class="label label-success label-md"
>{{translate 'Connected' scope='ExternalAccount'}}</span>
</div>
<button
class="btn btn-default"
data-action="disconnect"
>{{translate 'Disconnect' scope='ExternalAccount'}}</button>
{{/if}}
{{#if hasConnect}}
<div class="margin-bottom">
<span
class="label label-default label-md"
>{{translate 'Disconnected' scope='ExternalAccount'}}</span>
</div>
<button
class="btn btn-default"
data-action="connect"
>{{translate 'Connect' scope='ExternalAccount'}}</button>
{{/if}}
`
/**
* @private
* @type {boolean}
*/
inProcess = false
data() {
const isSet = this.model.attributes.hasAccessToken !== undefined;
const hasDisconnect = !this.inProcess &&
isSet &&
this.model.attributes.hasAccessToken;
const hasConnect =
!this.inProcess &&
isSet &&
!this.model.attributes.hasAccessToken &&
this.model.attributes.providerIsActive;
// noinspection JSValidateTypes
return {
hasDisconnect,
hasConnect,
}
}
setup() {
super.setup();
this.listenTo(this.model, 'sync', () => this.reRender());
this.addActionHandler('connect', () => this.actionConnect());
this.addActionHandler('disconnect', () => this.actionDisconnect());
}
/**
* @private
*/
async actionDisconnect() {
this.inProcess = true;
await this.reRender();
Espo.Ui.notify(' ... ');
await Espo.Ajax.deleteRequest(`OAuth/${this.model.id}/connection`);
await this.model.fetch();
Espo.Ui.notify();
this.inProcess = false;
await this.reRender();
}
/**
* @private
*/
async actionConnect() {
const data = this.model.attributes.data || {};
const endpoint = data.endpoint;
const redirectUri = data.redirectUri;
const clientId = data.clientId;
const scope = data.scope;
const prompt = data.prompt;
const params = data.params;
const proxy = window.open('about:blank', 'ConnectWithOAuth', 'location=0,status=0,width=800,height=800');
const info = await this.processWithData({
endpoint,
redirectUri,
clientId,
scope,
prompt,
params,
}, proxy);
this.inProcess = true;
await this.reRender()
Espo.Ui.notify(' ... ');
try {
await Espo.Ajax.postRequest(`OAuth/${this.model.id}/connection`, {code: info.code});
} catch (e) {
this.inProcess = false;
await this.reRender();
return;
}
await this.model.fetch();
Espo.Ui.notify();
this.inProcess = false;
await this.reRender();
}
/**
* @private
* @param {{
* endpoint: string,
* clientId: string,
* redirectUri: string,
* scope: string|null,
* prompt: string,
* params: Record|null,
* }} data
* @param {WindowProxy} proxy
* @return {Promise<{code: string}>}
*/
processWithData(data, proxy) {
const state = undefined;
const params = {
client_id: data.clientId,
redirect_uri: data.redirectUri,
response_type: 'code',
prompt: data.prompt,
};
if (data.scope) {
params.scope = data.scope;
}
if (data.params) {
for (const name in data.params) {
params[name] = data.params[name];
}
}
const partList = Object.entries(params)
.map(([key, value]) => {
return key + '=' + encodeURIComponent(value);
});
const url = data.endpoint + '?' + partList.join('&');
return this.processWindow(url, state, proxy);
}
/**
* @private
* @param {string} url
* @param {string} state
* @param {WindowProxy} proxy
* @return {Promise<{code: string}>}
*/
processWindow(url, state, proxy) {
proxy.location.href = url;
return new Promise((resolve, reject) => {
const fail = () => {
window.clearInterval(interval);
if (!proxy.closed) {
proxy.close();
}
reject();
};
const interval = window.setInterval(() => {
if (proxy.closed) {
fail();
return;
}
let url;
try {
url = proxy.location.href;
} catch (e) {
return;
}
if (!url) {
return;
}
const parsedData = this.parseWindowUrl(url);
if (!parsedData) {
fail();
Espo.Ui.error('Could not parse URL', true);
return;
}
if ((parsedData.error || parsedData.code) && state && parsedData.state !== state) {
fail();
Espo.Ui.error('State mismatch', true);
return;
}
if (parsedData.error) {
fail();
Espo.Ui.error(parsedData.errorDescription || this.translate('Error'), true);
return;
}
if (parsedData.code) {
window.clearInterval(interval);
proxy.close();
resolve({
code: parsedData.code,
});
}
}, 300);
});
}
/**
* @private
* @param {string} url
* @return {?{
* code: ?string,
* state: ?string,
* error: ?string,
* errorDescription: ?string,
* }}
*/
parseWindowUrl(url) {
try {
const params = new URL(url).searchParams;
return {
code: params.get('code'),
state: params.get('state'),
error: params.get('error'),
errorDescription: params.get('errorDescription'),
};
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,234 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import BaseFieldView from 'views/fields/base';
/**
* @type {{
* edit: import('ace-builds').edit,
* require: import('ace-builds').require,
* }}
*/
let ace;
export default class OAuthProviderAuthorizationParamsFieldView extends BaseFieldView {
// language=Handlebars
detailTemplateContent = `
{{#if isNotEmpty}}
<div id="{{containerId}}">{{value}}</div>
{{else}}
{{#if isSet}}
<span class="none-value">{{translate 'None'}}</span>
{{else}}
<span class="loading-value"></span>
{{/if}}
{{/if}}
`
// language=Handlebars
editTemplateContent = `
<div id="{{containerId}}">{{value}}</div>
`
height = 46
maxLineDetailCount = 80
maxLineEditCount = 200
data() {
const data = super.data();
const value = this.model.attributes[this.name];
data.containerId = this.containerId;
data.isNotEmpty = value != null;
data.isSet = value !== undefined;
try {
data.value = value ? JSON.stringify(value, null, ' ') : null;
} catch (e) {
data.value = null;
}
return data;
}
setup() {
super.setup();
this.height = this.options.height || this.params.height || this.height;
this.maxLineDetailCount =
this.options.maxLineDetailCount ||
this.params.maxLineDetailCount ||
this.maxLineDetailCount;
this.maxLineEditCount =
this.options.maxLineEditCount ||
this.params.maxLineEditCount ||
this.maxLineEditCount;
this.containerId = 'editor-' + Math.floor((Math.random() * 10000) + 1).toString();
if (this.mode === this.MODE_EDIT || this.mode === this.MODE_DETAIL) {
this.wait(
this.requireAce()
);
}
this.on('remove', () => {
if (this.editor) {
this.editor.destroy();
}
});
this.validations.push(() => this.validateJson());
}
requireAce() {
return Espo.loader.requirePromise('lib!ace')
.then(lib => {
ace = lib;
const list = [
Espo.loader.requirePromise('lib!ace-ext-language_tools'),
Espo.loader.requirePromise('lib!ace-mode-json'),
];
if (this.getThemeManager().getParam('isDark')) {
list.push(
Espo.loader.requirePromise('lib!ace-theme-tomorrow_night')
);
}
return Promise.all(list);
});
}
afterRender() {
super.afterRender();
this.$editor = this.$el.find('#' + this.containerId);
if (
this.$editor.length &&
(
this.mode === this.MODE_EDIT ||
this.mode === this.MODE_DETAIL ||
this.mode === this.MODE_LIST
)
) {
this.$editor.css('fontSize', 'var(--font-size-base)');
if (this.mode === this.MODE_EDIT) {
this.$editor.css('minHeight', this.height + 'px');
}
const editor = this.editor = ace.edit(this.containerId);
editor.setOptions({fontFamily: 'var(--font-family-monospace)'});
editor.setFontSize('var(--font-size-base)');
editor.container.style.lineHeight = 'var(--line-height-computed)';
editor.renderer.updateFontSize();
editor.setOptions({
maxLines: this.mode === this.MODE_EDIT ? this.maxLineEditCount : this.maxLineDetailCount,
enableLiveAutocompletion: true,
});
if (this.getThemeManager().getParam('isDark')) {
editor.setOptions({
theme: 'ace/theme/tomorrow_night',
});
}
if (this.isEditMode()) {
editor.getSession().on('change', () => {
this.trigger('change', {ui: true});
});
editor.getSession().setUseWrapMode(true);
}
if (this.isReadMode()) {
editor.setReadOnly(true);
editor.renderer.$cursorLayer.element.style.display = 'none';
editor.renderer.setShowGutter(false);
}
editor.setShowPrintMargin(false);
editor.getSession().setUseWorker(false);
editor.commands.removeCommand('find');
editor.setHighlightActiveLine(false);
const Mode = ace.require('ace/mode/json').Mode;
editor.session.setMode(new Mode());
}
}
/**
* @private
* @return {boolean}
*/
validateJson() {
const raw = this.editor.getValue();
if (!raw) {
return false;
}
try {
JSON.parse(raw);
} catch (e) {
const message = this.translate('Not valid');
this.showValidationMessage(message, '.ace_editor');
return true;
}
return false;
}
fetch() {
let value = null;
const raw = this.editor.getValue();
if (!raw) {
return {[this.name]: null};
}
try {
value = JSON.parse(raw);
} catch (e) {}
return {[this.name]: value};
}
}

View File

@@ -51,7 +51,8 @@
"chillerlan/php-qrcode": "^4.4",
"ext-ctype": "*",
"lasserafn/php-initial-avatar-generator": "^4.4",
"tholu/php-cidr-match": "^0.4"
"tholu/php-cidr-match": "^0.4",
"league/oauth2-client": "^2.8"
},
"require-dev": {
"phpunit/phpunit": "^9.5",

278
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c71fbf8961a0a000e8e902a0bf78bd6c",
"content-hash": "c9e886d22a6f5af3773701b37aa6cf97",
"packages": [
{
"name": "async-aws/core",
@@ -1481,6 +1481,215 @@
},
"time": "2024-11-04T11:18:07+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.9.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "d281ed313b989f213357e3be1a179f02196ac99b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
"reference": "d281ed313b989f213357e3be1a179f02196ac99b",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.5.3 || ^2.0.3",
"guzzlehttp/psr7": "^2.7.0",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
"guzzle/client-integration-tests": "3.0.2",
"php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.39 || ^9.6.20",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Jeremy Lindblom",
"email": "jeremeamia@gmail.com",
"homepage": "https://github.com/jeremeamia"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle is a PHP HTTP client library",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.9.2"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
"type": "tidelift"
}
],
"time": "2024-07-24T11:22:20+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
"reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.0.4"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
"type": "tidelift"
}
],
"time": "2024-10-17T10:06:22+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.7.0",
@@ -2750,6 +2959,71 @@
],
"time": "2024-09-21T08:32:55+00:00"
},
{
"name": "league/oauth2-client",
"version": "2.8.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/oauth2-client.git",
"reference": "9df2924ca644736c835fc60466a3a60390d334f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9",
"reference": "9df2924ca644736c835fc60466a3a60390d334f9",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
"php": "^7.1 || >=8.0.0 <8.5.0"
},
"require-dev": {
"mockery/mockery": "^1.3.5",
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "^3.11"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\OAuth2\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alex Bilbie",
"email": "hello@alexbilbie.com",
"homepage": "http://www.alexbilbie.com",
"role": "Developer"
},
{
"name": "Woody Gilk",
"homepage": "https://github.com/shadowhand",
"role": "Contributor"
}
],
"description": "OAuth 2.0 Client Library",
"keywords": [
"Authentication",
"SSO",
"authorization",
"identity",
"idp",
"oauth",
"oauth2",
"single sign on"
],
"support": {
"issues": "https://github.com/thephpleague/oauth2-client/issues",
"source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1"
},
"time": "2025-02-26T04:37:30+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.1.2",
@@ -8988,5 +9262,5 @@
"ext-ctype": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View File

@@ -196,6 +196,10 @@
"src": "node_modules/ace-builds/src-noconflict/mode-html.js",
"dest": "client/lib/ace-mode-html.js"
},
{
"src": "node_modules/ace-builds/src-noconflict/mode-json.js",
"dest": "client/lib/ace-mode-json.js"
},
{
"src": "node_modules/ace-builds/src-noconflict/mode-handlebars.js",
"dest": "client/lib/ace-mode-handlebars.js"

View File

@@ -0,0 +1,41 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
include "../../../bootstrap.php";
use Espo\Core\Application;
use Espo\Core\Application\Runner\Params;
use Espo\Core\ApplicationRunners\EntryPoint;
$app = new Application();
$app->run(
EntryPoint::class,
Params::create()->with('entryPoint', 'oauthCallback')
);