mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-10 05:37:01 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b0787474e | ||
|
|
8c87f20374 | ||
|
|
ba35115a48 | ||
|
|
069010d0fe | ||
|
|
fe5878fd99 | ||
|
|
839ceea142 | ||
|
|
9cce9d7347 | ||
|
|
69d0dbbf1c | ||
|
|
68ef9ce4ac |
@@ -29,10 +29,14 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
class ApiIndex
|
||||
{
|
||||
public function getActionIndex(): string
|
||||
public function getActionIndex(Request $request, Response $response): void
|
||||
{
|
||||
return "EspoCRM REST API";
|
||||
$response->writeBody(Json::encode("EspoCRM REST API"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,8 +195,12 @@ class Auth
|
||||
throw new BadRequest("Auth: Bad authorization string provided.");
|
||||
}
|
||||
|
||||
/** @var array{string, string} */
|
||||
return explode(':', $stringDecoded, 2);
|
||||
[$username, $password] = explode(':', $stringDecoded, 2);
|
||||
|
||||
$username = trim($username);
|
||||
$password = trim($password);
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
private function handleSecondStepRequired(Response $response, Result $result): void
|
||||
@@ -316,6 +320,14 @@ class Auth
|
||||
$username = $request->getServerParam('PHP_AUTH_USER');
|
||||
$password = $request->getServerParam('PHP_AUTH_PW');
|
||||
|
||||
if (is_string($username)) {
|
||||
$username = trim($username);
|
||||
}
|
||||
|
||||
if (is_string($password)) {
|
||||
$password = trim($password);
|
||||
}
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ class Authentication
|
||||
$result = $this->processTwoFactor($result, $request);
|
||||
|
||||
if ($result->isFail()) {
|
||||
return $this->processFail($result, $data, $request);
|
||||
return $this->processTwoFactorFail($result, $data, $request, $authLogRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,4 +818,22 @@ class Authentication
|
||||
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
private function processTwoFactorFail(
|
||||
Result $result,
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
?AuthLogRecord $authLogRecord
|
||||
): Result {
|
||||
|
||||
if ($authLogRecord) {
|
||||
$authLogRecord
|
||||
->setIsDenied()
|
||||
->setDenialReason(AuthLogRecord::DENIAL_REASON_WRONG_CODE);
|
||||
|
||||
$this->entityManager->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
return $this->processFail($result, $data, $request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
return;
|
||||
}
|
||||
|
||||
$isSecondStep = $request->getHeader('Espo-Authorization-Code') !== null;
|
||||
|
||||
$failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod();
|
||||
$maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber();
|
||||
|
||||
@@ -80,14 +82,21 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$apAddress = $this->util->obtainIpFromRequest($request);
|
||||
$ipAddress = $this->util->obtainIpFromRequest($request);
|
||||
|
||||
$where = [
|
||||
'requestTime>' => $requestTimeFrom->format('U'),
|
||||
'ipAddress' => $apAddress,
|
||||
'isDenied' => true,
|
||||
];
|
||||
|
||||
if ($isSecondStep) {
|
||||
$where['username'] = $data->getUsername();
|
||||
}
|
||||
|
||||
if (!$isSecondStep) {
|
||||
$where['ipAddress'] = $ipAddress;
|
||||
}
|
||||
|
||||
$wasFailed = (bool) $this->entityManager
|
||||
->getRDBRepository(AuthLogRecord::ENTITY_TYPE)
|
||||
->select(['id'])
|
||||
@@ -107,6 +116,12 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Forbidden("Max failed login attempts exceeded for IP address $apAddress.");
|
||||
if ($isSecondStep) {
|
||||
$username = $data->getUsername() ?? '';
|
||||
|
||||
throw new Forbidden("Max failed 2FA login attempts exceeded for username '$username'.");
|
||||
}
|
||||
|
||||
throw new Forbidden("Max failed login attempts exceeded for IP address $ipAddress.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class AuthLogRecord extends Entity
|
||||
public const ENTITY_TYPE = 'AuthLogRecord';
|
||||
|
||||
public const DENIAL_REASON_CREDENTIALS = 'CREDENTIALS';
|
||||
public const DENIAL_REASON_WRONG_CODE = 'WRONG_CODE';
|
||||
public const DENIAL_REASON_INACTIVE_USER = 'INACTIVE_USER';
|
||||
public const DENIAL_REASON_IS_PORTAL_USER = 'IS_PORTAL_USER';
|
||||
public const DENIAL_REASON_IS_NOT_PORTAL_USER = 'IS_NOT_PORTAL_USER';
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Modules\Crm\EntryPoints;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Campaign;
|
||||
use Espo\Modules\Crm\Entities\EmailQueueItem;
|
||||
use Espo\Modules\Crm\Entities\MassEmail;
|
||||
@@ -47,7 +48,8 @@ class CampaignTrackOpened implements EntryPoint
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private LogService $service
|
||||
private LogService $service,
|
||||
private User $user,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -105,7 +107,9 @@ class CampaignTrackOpened implements EntryPoint
|
||||
return;
|
||||
}
|
||||
|
||||
$this->service->logOpened($campaignId, $queueItem);
|
||||
if ($this->user->isSystem()) {
|
||||
$this->service->logOpened($campaignId, $queueItem);
|
||||
}
|
||||
|
||||
header('Content-Type: image/png');
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"options": {
|
||||
"denialReason": {
|
||||
"CREDENTIALS": "Invalid credentials",
|
||||
"WRONG_CODE": "Wrong code",
|
||||
"INACTIVE_USER": "Inactive user",
|
||||
"IS_PORTAL_USER": "Portal user",
|
||||
"IS_NOT_PORTAL_USER": "Not a portal user",
|
||||
|
||||
@@ -81,8 +81,10 @@
|
||||
"view": "views/email/fields/from-address-varchar",
|
||||
"textFilterDisabled": true,
|
||||
"customizationDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail",
|
||||
"filters"
|
||||
]
|
||||
},
|
||||
"to": {
|
||||
"type": "varchar",
|
||||
@@ -97,8 +99,10 @@
|
||||
],
|
||||
"textFilterDisabled": true,
|
||||
"customizationDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail",
|
||||
"filters"
|
||||
]
|
||||
},
|
||||
"cc": {
|
||||
"type": "varchar",
|
||||
@@ -111,8 +115,9 @@
|
||||
],
|
||||
"customizationDisabled": true,
|
||||
"textFilterDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail"
|
||||
]
|
||||
},
|
||||
"bcc": {
|
||||
"type": "varchar",
|
||||
@@ -125,8 +130,9 @@
|
||||
],
|
||||
"customizationDisabled": true,
|
||||
"textFilterDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail"
|
||||
]
|
||||
},
|
||||
"replyTo": {
|
||||
"type": "varchar",
|
||||
@@ -135,8 +141,9 @@
|
||||
"view": "views/email/fields/email-address-varchar",
|
||||
"textFilterDisabled": true,
|
||||
"customizationDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail"
|
||||
]
|
||||
},
|
||||
"personStringData": {
|
||||
"type": "varchar",
|
||||
|
||||
@@ -69,6 +69,10 @@ function init(langSets) {
|
||||
const options = context.options;
|
||||
const view = /** @type {import('view').default} */options.espoView;
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.memo('button.cellParams', () => {
|
||||
return ui.button({
|
||||
className: '',
|
||||
@@ -144,6 +148,12 @@ function init(langSets) {
|
||||
const options = context.options;
|
||||
const view = /** @type {import('view').default} */options.espoView;
|
||||
|
||||
if (!view) {
|
||||
// Prevents an issue with a collapsed modal.
|
||||
// @todo Revise.
|
||||
return;
|
||||
}
|
||||
|
||||
context.memo('button.tableParams', () => {
|
||||
return ui.button({
|
||||
className: '',
|
||||
|
||||
@@ -1253,6 +1253,7 @@ class BaseFieldView extends View {
|
||||
if (isInvalid) {
|
||||
Espo.Ui.error(this.translate('Not valid'));
|
||||
|
||||
// @todo Revise.
|
||||
model.set(prev, {silent: true});
|
||||
|
||||
return;
|
||||
@@ -1273,6 +1274,7 @@ class BaseFieldView extends View {
|
||||
.catch(() => {
|
||||
Espo.Ui.error(this.translate('Error occurred'));
|
||||
|
||||
// @todo Revise.
|
||||
model.set(prev, {silent: true});
|
||||
|
||||
this.reRender();
|
||||
|
||||
@@ -2085,7 +2085,10 @@ class DetailRecordView extends BaseRecordView {
|
||||
return;
|
||||
}
|
||||
|
||||
view.inlineEdit();
|
||||
const initialAttributes = {...view.initialAttributes};
|
||||
|
||||
view.inlineEdit()
|
||||
.then(() => view.initialAttributes = initialAttributes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "8.3.1",
|
||||
"version": "8.3.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "espocrm",
|
||||
"version": "8.3.1",
|
||||
"version": "8.3.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "8.3.1",
|
||||
"version": "8.3.2",
|
||||
"description": "Open-source CRM.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user