diff --git a/docs/implementation-examples.md b/docs/implementation-examples.md
index 7728b4d0..3f00d5df 100644
--- a/docs/implementation-examples.md
+++ b/docs/implementation-examples.md
@@ -16,7 +16,7 @@ change one action.
#### Recap
* `UserTheme.php` create theme enum.
* `UserValidator.php` add validation line in `validateUserValues()`.
-* `UserAuthorizationChecker.php` add to the `$grantedUpdateKeys` in `isGrantedToUpdate()`.
+* `UserPermissionVerifier.php` add to the `$grantedUpdateKeys` in `isGrantedToUpdate()`.
* `UserUpdater.php` add `theme` to the authorized update keys in `updateUser()`.
* `UserData.php` add instance variable and populate in constructor.
* `UserFinderRepository.php` add to the class attribute `$fields`.
diff --git a/docs/testing/testing-cheatsheet.md b/docs/testing/testing-cheatsheet.md
index 4864f953..c8c4cf47 100644
--- a/docs/testing/testing-cheatsheet.md
+++ b/docs/testing/testing-cheatsheet.md
@@ -398,7 +398,7 @@ public function testNoteListAction(
'noteUpdatedAt' => (new \DateTime($noteData['updated_at']))->format('d. F Y • H:i'),
'userId' => $noteData['user_id'],
'userFullName' => $userLinkedToNoteRow['first_name'] . ' ' . $userLinkedToNoteRow['surname'],
- // Has to match privilege from NoteAuthorizationGetter.php (rules are in NoteAuthorizationChecker.php)
+ // Has to match privilege from notePrivilegeDeterminer.php (rules are in notePermissionVerifier.php)
'privilege' => $expectedResult['privilege']->value,
];
// Assert response data
diff --git a/public/assets/client/note/client-read-note-event-listener-setup.js b/public/assets/client/note/client-read-note-event-listener-setup.js
index 6afd21eb..8adb89d4 100644
--- a/public/assets/client/note/client-read-note-event-listener-setup.js
+++ b/public/assets/client/note/client-read-note-event-listener-setup.js
@@ -194,7 +194,8 @@ export function addHideNoteBtnEventListener(btn) {
toggleEyeIcon();
// Submit update to server
submitUpdate(
- {hidden: newHiddenValue}, `notes/${noteId}`, `notes/${noteId}`
+ {hidden: newHiddenValue}, `notes/${noteId}`,
+ `notes/${noteId}`, btn.closest('.note-container').id
).then(r => {}).catch(r => {
// Revert eye to how it was before on failure
toggleEyeIcon();
diff --git a/public/assets/client/note/client-read-template-note.html.js b/public/assets/client/note/client-read-template-note.html.js
index 586d40f9..df9c170b 100644
--- a/public/assets/client/note/client-read-template-note.html.js
+++ b/public/assets/client/note/client-read-template-note.html.js
@@ -12,27 +12,27 @@ export function getNoteHtml(note) {
${createdAt}
${/*Show active eye icon if hidden*/ hidden === 1 || hidden === '1' ? `` : /* Else the non-active one if allowed*/ userHasPrivilegeTo(privilege, 'U') ? `
+ >` : /* Else the non-active one if allowed*/ privilege.includes('U') ? `
` : ''}
- ${/*Show delete button */ userHasPrivilegeTo(privilege, 'D') ? `` : ''}
${escapeHtml(userFullName)}
diff --git a/public/assets/client/read/client-read-main.js b/public/assets/client/read/client-read-main.js
index af5c2de8..0bdc9b29 100644
--- a/public/assets/client/read/client-read-main.js
+++ b/public/assets/client/read/client-read-main.js
@@ -81,23 +81,26 @@ document.querySelector('#undelete-client-btn')?.addEventListener('click', () =>
// Toggle personal info edit icons
-let personalInfoEditIconsToggle = document.querySelector('#toggle-personal-info-edit-icons');
let personalInfoContainer = document.querySelector('#client-personal-info-flex-container');
-personalInfoEditIconsToggle.addEventListener('click', () => {
- let personalInfosEditIcons = document.querySelectorAll('#client-personal-info-flex-container div .contenteditable-edit-icon');
- for (let editIcon of personalInfosEditIcons) {
- editIcon.classList.toggle('always-displayed-icon');
- }
-})
+let personalInfoEditIconsToggle = document.querySelector('#toggle-personal-info-edit-icons');
+// PersonalInfoEditIconsToggle is not present if the user doesn't have update permission
+if (personalInfoEditIconsToggle) {
+ personalInfoEditIconsToggle.addEventListener('click', () => {
+ let personalInfosEditIcons = document.querySelectorAll('#client-personal-info-flex-container div .contenteditable-edit-icon');
+ for (let editIcon of personalInfosEditIcons) {
+ editIcon.classList.toggle('always-displayed-icon');
+ }
+ })
// Display toggle btn if screen is touch device https://stackoverflow.com/a/13470899/9013718
-if ('ontouchstart' in window || navigator.msMaxTouchPoints) {
- personalInfoEditIconsToggle.style.display = 'inline-block';
- // Increase right padding to not overlap edit icons
- personalInfoContainer.style.paddingRight = '20px';
-} else {
- personalInfoEditIconsToggle.style.display = 'none';
- personalInfoContainer.style.paddingRight = null;
+ if ('ontouchstart' in window || navigator.msMaxTouchPoints) {
+ personalInfoEditIconsToggle.style.display = 'inline-block';
+ // Increase right padding to not overlap edit icons
+ personalInfoContainer.style.paddingRight = '20px';
+ } else {
+ personalInfoEditIconsToggle.style.display = 'none';
+ personalInfoContainer.style.paddingRight = null;
+ }
}
/**
diff --git a/public/assets/general/ajax/submit-update-data.js b/public/assets/general/ajax/submit-update-data.js
index 73cbffa8..f7983524 100644
--- a/public/assets/general/ajax/submit-update-data.js
+++ b/public/assets/general/ajax/submit-update-data.js
@@ -11,10 +11,10 @@ import {handleFail, removeValidationErrorMessages} from "./ajax-util/fail-handle
* @param {string} route after base path e.g. clients/1
* @param {boolean|string} redirectToRouteIfUnauthenticated true or redirect route url after base path.
* If true, the redirect url is the same as the given route
- *
+ * @param domFieldId field id to display the validation error message for the correct field
* @return Promise with as content server response as JSON
*/
-export function submitUpdate(formFieldsAndValues, route, redirectToRouteIfUnauthenticated = false) {
+export function submitUpdate(formFieldsAndValues, route, redirectToRouteIfUnauthenticated = false, domFieldId = null) {
return new Promise(function (resolve, reject) {
// Make ajax call
let xHttp = new XMLHttpRequest();
@@ -23,7 +23,7 @@ export function submitUpdate(formFieldsAndValues, route, redirectToRouteIfUnauth
// Fail
if (xHttp.status !== 201 && xHttp.status !== 200) {
// Default fail handler
- handleFail(xHttp);
+ handleFail(xHttp, domFieldId);
// reject() only needed if promise is caught with .catch()
reject(JSON.parse(xHttp.responseText));
}
diff --git a/public/assets/general/validation/form-validation.js b/public/assets/general/validation/form-validation.js
index 033cb7c2..906c744b 100644
--- a/public/assets/general/validation/form-validation.js
+++ b/public/assets/general/validation/form-validation.js
@@ -7,6 +7,7 @@
* */
export function displayValidationErrorMessage(fieldName, errorMessage, domFieldId = null) {
let field;
+ console.log('displayValidationErrorMessage', fieldName, errorMessage, domFieldId);
if (domFieldId !== null) {
field = document.querySelector('#' + domFieldId);
} else {
diff --git a/src/Application/Action/Authentication/Ajax/AccountUnlockProcessAction.php b/src/Application/Action/Authentication/Ajax/AccountUnlockProcessAction.php
index cc767a1b..8c434cf0 100644
--- a/src/Application/Action/Authentication/Ajax/AccountUnlockProcessAction.php
+++ b/src/Application/Action/Authentication/Ajax/AccountUnlockProcessAction.php
@@ -31,7 +31,7 @@ public function __invoke(ServerRequest $request, Response $response): Response
// There may be other query params e.g. redirect
if (isset($queryParams['id'], $queryParams['token'])) {
try {
- $userId = $this->accountUnlockTokenVerifier->getUserIdIfUnlockTokenIsValid(
+ $userId = $this->accountUnlockTokenVerifier->verifyUnlockTokenAndGetUserId(
(int)$queryParams['id'],
$queryParams['token']
);
diff --git a/src/Application/Action/Authentication/Ajax/LoginSubmitAction.php b/src/Application/Action/Authentication/Ajax/LoginSubmitAction.php
index d5da56af..48dc35af 100644
--- a/src/Application/Action/Authentication/Ajax/LoginSubmitAction.php
+++ b/src/Application/Action/Authentication/Ajax/LoginSubmitAction.php
@@ -37,13 +37,9 @@ public function __invoke(ServerRequest $request, Response $response): Response
try {
// Throws InvalidCredentialsException if not allowed
- $userId = $this->loginVerifier->getUserIdIfAllowedToLogin(
- $submitValues,
- $submitValues['g-recaptcha-response'] ?? null,
- $queryParams
- );
+ $userId = $this->loginVerifier->verifyLoginAndGetUserId($submitValues, $queryParams);
- // Clear all session data and regenerate session ID
+ // Regenerate session ID
$this->sessionManager->regenerateId();
// Add user to session
$this->session->set('user_id', $userId);
@@ -107,7 +103,7 @@ public function __invoke(ServerRequest $request, Response $response): Response
['email' => $submitValues['email']],
);
} catch (UnableToLoginStatusNotActiveException $unableToLoginException) {
- // When user doesn't have status active
+ // When user status is not "active"
$this->templateRenderer->addPhpViewAttribute('formError', true);
// Add form error message
$this->templateRenderer->addPhpViewAttribute('formErrorMessage', $unableToLoginException->getMessage());
diff --git a/src/Application/Action/Authentication/Ajax/RegisterVerifyProcessAction.php b/src/Application/Action/Authentication/Ajax/RegisterVerifyProcessAction.php
index e1cf0dbb..717736d7 100644
--- a/src/Application/Action/Authentication/Ajax/RegisterVerifyProcessAction.php
+++ b/src/Application/Action/Authentication/Ajax/RegisterVerifyProcessAction.php
@@ -33,7 +33,7 @@ public function __invoke(ServerRequest $request, Response $response): Response
// There may be other query params e.g. redirect
if (isset($queryParams['id'], $queryParams['token'])) {
try {
- $userId = $this->registerTokenVerifier->getUserIdIfRegisterTokenIsValid(
+ $userId = $this->registerTokenVerifier->verifyRegisterTokenAndGetUserId(
(int)$queryParams['id'],
$queryParams['token']
);
diff --git a/src/Application/Action/Client/Page/ClientListPageAction.php b/src/Application/Action/Client/Page/ClientListPageAction.php
index 0c24f47e..41fc9b4d 100644
--- a/src/Application/Action/Client/Page/ClientListPageAction.php
+++ b/src/Application/Action/Client/Page/ClientListPageAction.php
@@ -4,7 +4,7 @@
use App\Application\Responder\TemplateRenderer;
use App\Domain\Authorization\Privilege;
-use App\Domain\Client\Service\Authorization\ClientAuthorizationChecker;
+use App\Domain\Client\Service\Authorization\ClientPermissionVerifier;
use App\Domain\Client\Service\ClientListFilter\ClientListFilterChipProvider;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -14,7 +14,7 @@ final class ClientListPageAction
public function __construct(
private readonly TemplateRenderer $templateRenderer,
private readonly ClientListFilterChipProvider $clientListFilterChipGetter,
- private readonly ClientAuthorizationChecker $clientAuthorizationChecker
+ private readonly ClientPermissionVerifier $clientPermissionVerifier
) {
}
@@ -41,7 +41,7 @@ public function __invoke(
$this->templateRenderer->addPhpViewAttribute('clientListFilters', $clientListFilters);
$this->templateRenderer->addPhpViewAttribute(
'clientCreatePrivilege',
- $this->clientAuthorizationChecker->isGrantedToCreate() ? Privilege::CREATE : Privilege::NONE
+ $this->clientPermissionVerifier->isGrantedToCreate() ? Privilege::CREATE : Privilege::NONE
);
return $this->templateRenderer->render($response, 'client/clients-list.html.php');
diff --git a/src/Application/Action/User/Page/UserListPageAction.php b/src/Application/Action/User/Page/UserListPageAction.php
index 2f912274..7d50f18c 100644
--- a/src/Application/Action/User/Page/UserListPageAction.php
+++ b/src/Application/Action/User/Page/UserListPageAction.php
@@ -3,7 +3,7 @@
namespace App\Application\Action\User\Page;
use App\Application\Responder\TemplateRenderer;
-use App\Domain\User\Service\Authorization\UserAuthorizationChecker;
+use App\Domain\User\Service\Authorization\UserPermissionVerifier;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Exception\HttpForbiddenException;
@@ -12,7 +12,7 @@ final class UserListPageAction
{
public function __construct(
private readonly TemplateRenderer $templateRenderer,
- private readonly UserAuthorizationChecker $userAuthorizationChecker,
+ private readonly UserPermissionVerifier $userPermissionVerifier,
) {
}
@@ -30,7 +30,7 @@ public function __invoke(
ResponseInterface $response,
array $args
): ResponseInterface {
- if ($this->userAuthorizationChecker->isGrantedToRead()) {
+ if ($this->userPermissionVerifier->isGrantedToRead()) {
return $this->templateRenderer->render($response, 'user/user-list.html.php');
}
diff --git a/src/Application/Middleware/PhpViewExtensionMiddleware.php b/src/Application/Middleware/PhpViewExtensionMiddleware.php
index 6c3dd018..063a8a43 100644
--- a/src/Application/Middleware/PhpViewExtensionMiddleware.php
+++ b/src/Application/Middleware/PhpViewExtensionMiddleware.php
@@ -3,7 +3,7 @@
namespace App\Application\Middleware;
use App\Common\JsImportVersionAdder;
-use App\Domain\User\Service\Authorization\UserAuthorizationChecker;
+use App\Domain\User\Service\Authorization\UserPermissionVerifier;
use App\Domain\Utility\Settings;
use Cake\Database\Exception\DatabaseException;
use Odan\Session\SessionInterface;
@@ -28,7 +28,7 @@ public function __construct(
private readonly SessionInterface $session,
private readonly JsImportVersionAdder $jsImportVersionAdder,
Settings $settings,
- private readonly UserAuthorizationChecker $userAuthorizationChecker,
+ private readonly UserPermissionVerifier $userPermissionVerifier,
private readonly RouteParserInterface $routeParser
) {
$this->publicSettings = $settings->get('public');
@@ -60,7 +60,7 @@ public function process(
try {
$this->phpRenderer->addAttribute(
'userListAuthorization',
- $this->userAuthorizationChecker->isGrantedToRead($loggedInUserId + 1, false)
+ $this->userPermissionVerifier->isGrantedToRead($loggedInUserId + 1, false)
);
} catch (DatabaseException $databaseException) {
// Mysql connection not working. Caught here to prevent error page from crashing
diff --git a/src/Application/Middleware/UserAuthenticationMiddleware.php b/src/Application/Middleware/UserAuthenticationMiddleware.php
index 777adfae..1d6cd2ab 100644
--- a/src/Application/Middleware/UserAuthenticationMiddleware.php
+++ b/src/Application/Middleware/UserAuthenticationMiddleware.php
@@ -29,7 +29,7 @@ public function __construct(
}
/**
- * User authentication middleware. Check if user is logged in and if not
+ * User authentication middleware. Check if the user is logged in and if not
* redirect to login page with redirect back query params.
*
* @param ServerRequestInterface $request
@@ -37,10 +37,8 @@ public function __construct(
*
* @return ResponseInterface
*/
- public function process(
- ServerRequestInterface $request,
- RequestHandlerInterface $handler
- ): ResponseInterface {
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
// Check if user is logged in
if (($loggedInUserId = $this->session->get('user_id')) !== null) {
// Check that the user status is active
@@ -55,7 +53,7 @@ public function process(
$response = $this->responseFactory->createResponse();
- // Inform user that he/she has to login first
+ // Inform the user that he/she has to log in before accessing the page
$this->session->getFlash()->add('info', 'Please login to access this page.');
$queryParams = [];
@@ -70,7 +68,7 @@ public function process(
$queryParams['redirect'] = $routeName;
}
- // If it's a JSON request return 401 with the login url and its possible query params
+ // If it's a JSON request, return 401 with the login url and its possible query params
if ($request->getHeaderLine('Content-Type') === 'application/json') {
return $this->jsonResponder->respondWithJson(
$response,
@@ -78,7 +76,7 @@ public function process(
401
);
}
- // If no redirect header is set, and it's not a JSON request, redirect to same url as the request after login
+ // If no redirect header is set, and it's not a JSON request, redirect to the same url as the request after login
$queryParams = ['redirect' => $request->getUri()->getPath()];
return $this->redirectHandler->redirectToRouteName($response, 'login-page', [], $queryParams);
diff --git a/src/Domain/Authentication/Repository/UserRoleFinderRepository.php b/src/Domain/Authentication/Repository/UserRoleFinderRepository.php
index a3296fc5..423508d3 100644
--- a/src/Domain/Authentication/Repository/UserRoleFinderRepository.php
+++ b/src/Domain/Authentication/Repository/UserRoleFinderRepository.php
@@ -54,6 +54,25 @@ public function getUserRoleDataFromUser(int $userId): UserRoleData
return new UserRoleData();
}
+ /**
+ * Get role hierarchy from a user that has status active.
+ *
+ * @param int $userId
+ *
+ * @return int user role hierarchy (lower hierarchy means higher privileged role)
+ */
+ public function getRoleHierarchyByUserId(int $userId): int
+ {
+ $query = $this->queryFactory->selectQuery()
+ ->select(['user_role.hierarchy'])
+ ->from('user')
+ ->leftJoin('user_role', ['user.user_role_id = user_role.id'])
+ ->where(['user.deleted_at IS' => null, 'user.id' => $userId]);
+ $roleResultRow = $query->execute()->fetch('assoc');
+ // If no role found, return highest hierarchy which means lowest privileged role
+ return (int)($roleResultRow['hierarchy'] ?? 1000);
+ }
+
/**
* Get user role hierarchies mapped by name or id if param is true.
*
diff --git a/src/Domain/Authentication/Service/AccountUnlockTokenVerifier.php b/src/Domain/Authentication/Service/AccountUnlockTokenVerifier.php
index f7a9502a..65593a12 100644
--- a/src/Domain/Authentication/Service/AccountUnlockTokenVerifier.php
+++ b/src/Domain/Authentication/Service/AccountUnlockTokenVerifier.php
@@ -30,7 +30,7 @@ public function __construct(
*
* @return int
*/
- public function getUserIdIfUnlockTokenIsValid(int $verificationId, string $token): int
+ public function verifyUnlockTokenAndGetUserId(int $verificationId, string $token): int
{
$verification = $this->verificationTokenFinderRepository->findUserVerification($verificationId);
// Verify given token with token in database
diff --git a/src/Domain/Authentication/Service/AuthenticationLogger.php b/src/Domain/Authentication/Service/AuthenticationLogger.php
new file mode 100644
index 00000000..a494ab00
--- /dev/null
+++ b/src/Domain/Authentication/Service/AuthenticationLogger.php
@@ -0,0 +1,34 @@
+authenticationLoggerRepository->logLoginRequest(
+ $email,
+ $this->userNetworkSessionData->ipAddress,
+ $success,
+ $userId
+ );
+ }
+}
diff --git a/src/Domain/Authentication/Service/LoginNonActiveUserHandler.php b/src/Domain/Authentication/Service/LoginNonActiveUserHandler.php
index 7f20ef21..394b6b53 100644
--- a/src/Domain/Authentication/Service/LoginNonActiveUserHandler.php
+++ b/src/Domain/Authentication/Service/LoginNonActiveUserHandler.php
@@ -26,6 +26,7 @@ public function __construct(
private readonly LocaleHelper $localeHelper,
private readonly SecurityEmailChecker $securityEmailChecker,
private readonly LoggerInterface $logger,
+ private readonly AuthenticationLogger $authenticationLogger,
readonly Settings $settings,
) {
$this->mainContactEmail = $this->settings->get(
@@ -62,6 +63,9 @@ public function handleLoginAttemptFromNonActiveUser(
__('Unable to login at the moment, please check your email inbox for a more detailed message.')
);
+ // Log failed login attempt
+ $this->authenticationLogger->logLoginRequest($dbUser->email, false, $dbUser->id);
+
try {
$userId = $dbUser->id;
$email = $dbUser->email;
diff --git a/src/Domain/Authentication/Service/LoginVerifier.php b/src/Domain/Authentication/Service/LoginVerifier.php
index 0687af35..575bf130 100644
--- a/src/Domain/Authentication/Service/LoginVerifier.php
+++ b/src/Domain/Authentication/Service/LoginVerifier.php
@@ -2,9 +2,7 @@
namespace App\Domain\Authentication\Service;
-use App\Application\Data\UserNetworkSessionData;
use App\Domain\Authentication\Exception\InvalidCredentialsException;
-use App\Domain\Security\Repository\AuthenticationLoggerRepository;
use App\Domain\Security\Service\SecurityLoginChecker;
use App\Domain\User\Enum\UserActivity;
use App\Domain\User\Enum\UserStatus;
@@ -19,51 +17,42 @@ public function __construct(
private readonly UserValidator $userValidator,
private readonly SecurityLoginChecker $loginSecurityChecker,
private readonly UserFinderRepository $userFinderRepository,
- private readonly AuthenticationLoggerRepository $authenticationLoggerRepository,
private readonly LoginNonActiveUserHandler $loginNonActiveUserHandler,
private readonly UserActivityLogger $userActivityLogger,
- private readonly UserNetworkSessionData $ipAddressData,
+ private readonly AuthenticationLogger $authenticationLogger,
) {
}
/**
- * Checks if user is allowed to login.
- * If yes, the user object is returned with id
- * If no, an InvalidCredentialsException is thrown.
+ * Verifies the user's login credentials and returns the user's ID if the login is successful.
*
- * @param array $userLoginValues
- * @param string|null $captcha user captcha response if filled out
- * @param array $queryParams
+ * @param array $userLoginValues An associative array containing the user's login credentials.
+ * Expected keys are 'email' and 'password' and optionally 'g-recaptcha-response'.
+ * @param array $queryParams An associative array containing any additional query parameters.
*
- * @throws TransportExceptionInterface
+ * @throws TransportExceptionInterface If an error occurs while sending an email to a non-active user.
+ * @throws InvalidCredentialsException If the user does not exist or the password is incorrect.
*
- * @return int id
+ * @return int The ID of the user if the login is successful.
*/
- public function getUserIdIfAllowedToLogin(
- array $userLoginValues,
- ?string $captcha = null,
- array $queryParams = []
- ): int {
- // Validate entries coming from client
+ public function verifyLoginAndGetUserId(array $userLoginValues, array $queryParams = []): int
+ {
+ // Validate submitted values
$this->userValidator->validateUserLogin($userLoginValues);
+ $captcha = $userLoginValues['g-recaptcha-response'] ?? null;
- // Perform login security check
+ // Perform login security check before verifying credentials
$this->loginSecurityChecker->performLoginSecurityCheck($userLoginValues['email'], $captcha);
$dbUser = $this->userFinderRepository->findUserByEmail($userLoginValues['email']);
- // Check if user exists
- // Verify if password matches and enter login request
+
+ // Check if user exists and verify if the password is correct
if (isset($dbUser->email, $dbUser->passwordHash)
&& password_verify($userLoginValues['password'], $dbUser->passwordHash)) {
// If password correct and status active, log user in by
if ($dbUser->status === UserStatus::Active) {
- // Insert login success request
- $this->authenticationLoggerRepository->logLoginRequest(
- $dbUser->email,
- $this->ipAddressData->ipAddress,
- true,
- $dbUser->id
- );
+ // Log successful login request
+ $this->authenticationLogger->logLoginRequest($dbUser->email, true, $dbUser->id);
$this->userActivityLogger->logUserActivity(
UserActivity::READ,
@@ -73,26 +62,22 @@ public function getUserIdIfAllowedToLogin(
$dbUser->id
);
- // Return id (not sure if it's better to regenerate session here in service or in action)
+ // Return id
return (int)$dbUser->id;
}
- // If password is correct but the status not verified, send email to user
+ // If the password is correct but the status not verified, send email to user
// captcha needed if email security check requires captcha
$this->loginNonActiveUserHandler->handleLoginAttemptFromNonActiveUser($dbUser, $queryParams, $captcha);
}
- // Password not correct or user not existing - insert login request for ip
- $this->authenticationLoggerRepository->logLoginRequest(
- $userLoginValues['email'],
- $this->ipAddressData->ipAddress,
- false,
- $dbUser->id
- );
+ // Password is not correct or user not existing - insert login request
+ $this->authenticationLogger->logLoginRequest($userLoginValues['email'], false, $dbUser->id);
- // Perform second login request check to display the correct error message to the user if throttle is in place
+ // Perform second login request check after additional verification to display the correct error
+ // message to the user if throttle is in place
$this->loginSecurityChecker->performLoginSecurityCheck($userLoginValues['email'], $captcha);
- // Throw InvalidCred exception if user doesn't exist or wrong password
+ // Throw exception if the user doesn't exist or wrong password
// Vague exception on purpose for security
throw new InvalidCredentialsException($userLoginValues['email']);
}
diff --git a/src/Domain/Authentication/Service/PasswordChanger.php b/src/Domain/Authentication/Service/PasswordChanger.php
index eb03fcd1..dda13b4d 100644
--- a/src/Domain/Authentication/Service/PasswordChanger.php
+++ b/src/Domain/Authentication/Service/PasswordChanger.php
@@ -5,7 +5,7 @@
use App\Domain\Authentication\Exception\ForbiddenException;
use App\Domain\User\Enum\UserActivity;
use App\Domain\User\Repository\UserUpdaterRepository;
-use App\Domain\User\Service\Authorization\UserAuthorizationChecker;
+use App\Domain\User\Service\Authorization\UserPermissionVerifier;
use App\Domain\User\Service\UserValidator;
use App\Domain\UserActivity\Service\UserActivityLogger;
use Psr\Log\LoggerInterface;
@@ -13,7 +13,7 @@
class PasswordChanger
{
public function __construct(
- private readonly UserAuthorizationChecker $userAuthorizationChecker,
+ private readonly UserPermissionVerifier $userPermissionVerifier,
private readonly UserUpdaterRepository $userUpdaterRepository,
private readonly UserValidator $userValidator,
private readonly UserActivityLogger $userActivityLogger,
@@ -36,16 +36,16 @@ public function changeUserPassword(array $userValues, int $userId): bool
// If old password is provided and user is allowed to update password
if (isset($userValues['old_password'])
- && $this->userAuthorizationChecker->isGrantedToUpdate(['password_hash' => 'value'], $userId)
+ && $this->userPermissionVerifier->isGrantedToUpdate(['password_hash' => 'value'], $userId)
) {
// Verify old password; throws validation exception if not correct old password
$this->userValidator->checkIfOldPasswordIsCorrect(['old_password' => $userValues['old_password']], $userId);
} // Else if old password is not set, check if granted to update without it.
if (!isset($userValues['old_password'])
// Check if allowed to update password without the old password
- && (!$this->userAuthorizationChecker->isGrantedToUpdate(['password_without_verification' => 'value'], $userId)
+ && (!$this->userPermissionVerifier->isGrantedToUpdate(['password_without_verification' => 'value'], $userId)
// Check if allowed to update password_hash of that user
- || !$this->userAuthorizationChecker->isGrantedToUpdate(['password_hash' => 'value'], $userId))
+ || !$this->userPermissionVerifier->isGrantedToUpdate(['password_hash' => 'value'], $userId))
) {
throw new ForbiddenException('Not granted to update password without verification.');
}
@@ -64,7 +64,7 @@ public function changeUserPassword(array $userValues, int $userId): bool
*/
private function updateUserPassword(string $newPassword, int $userId): bool
{
- if ($this->userAuthorizationChecker->isGrantedToUpdate(['password_hash' => 'value'], $userId)) {
+ if ($this->userPermissionVerifier->isGrantedToUpdate(['password_hash' => 'value'], $userId)) {
$passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
$updated = $this->userUpdaterRepository->changeUserPassword($passwordHash, $userId);
if ($updated) {
diff --git a/src/Domain/Authentication/Service/PasswordResetterWithToken.php b/src/Domain/Authentication/Service/PasswordResetterWithToken.php
index 403823ae..07aad8ca 100644
--- a/src/Domain/Authentication/Service/PasswordResetterWithToken.php
+++ b/src/Domain/Authentication/Service/PasswordResetterWithToken.php
@@ -31,7 +31,7 @@ public function resetPasswordWithToken(array $passwordResetValues): bool
// Validate passwords BEFORE token verification as it would be set to usedAt even if passwords are not valid
$this->userValidator->validatePasswordReset($passwordResetValues);
// If passwords are valid strings, verify token and set token to used
- $userId = $this->verificationTokenVerifier->getUserIdIfTokenIsValid(
+ $userId = $this->verificationTokenVerifier->verifyTokenAndGetUserId(
$passwordResetValues['id'],
$passwordResetValues['token']
);
diff --git a/src/Domain/Authentication/Service/RegisterTokenVerifier.php b/src/Domain/Authentication/Service/RegisterTokenVerifier.php
index 25f891a7..3d106fc0 100644
--- a/src/Domain/Authentication/Service/RegisterTokenVerifier.php
+++ b/src/Domain/Authentication/Service/RegisterTokenVerifier.php
@@ -30,7 +30,7 @@ public function __construct(
*
* @return int
*/
- public function getUserIdIfRegisterTokenIsValid(int $verificationId, string $token): int
+ public function verifyRegisterTokenAndGetUserId(int $verificationId, string $token): int
{
$verification = $this->verificationTokenFinderRepository->findUserVerification($verificationId);
if ($verification->userId) {
diff --git a/src/Domain/Authentication/Service/VerificationTokenVerifier.php b/src/Domain/Authentication/Service/VerificationTokenVerifier.php
index 558dadcf..6eebb5ad 100644
--- a/src/Domain/Authentication/Service/VerificationTokenVerifier.php
+++ b/src/Domain/Authentication/Service/VerificationTokenVerifier.php
@@ -27,7 +27,7 @@ public function __construct(
*
* @see AccountUnlockTokenVerifier, RegisterTokenVerifier
*/
- public function getUserIdIfTokenIsValid(int $verificationId, string $token): int
+ public function verifyTokenAndGetUserId(int $verificationId, string $token): int
{
$verification = $this->verificationTokenFinderRepository->findUserVerification($verificationId);
diff --git a/src/Domain/Authorization/AuthorizationChecker.php b/src/Domain/Authorization/AuthorizationChecker.php
index 84547283..214ddb69 100644
--- a/src/Domain/Authorization/AuthorizationChecker.php
+++ b/src/Domain/Authorization/AuthorizationChecker.php
@@ -27,13 +27,13 @@ public function __construct(
*/
public function isAuthorizedByRole(UserRole $minimalRequiredRole): bool
{
- $authenticatedUserRoleData = $this->userRoleFinderRepository->getUserRoleDataFromUser(
+ $authenticatedUserRoleHierarchy = $this->userRoleFinderRepository->getRoleHierarchyByUserId(
$this->userNetworkSessionData->userId
);
// Returns array with role name as key and hierarchy as value [role_name => hierarchy_int]
// * Lower hierarchy number means higher privileged role
$userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
- return $authenticatedUserRoleData->hierarchy <= $userRoleHierarchies[$minimalRequiredRole->value];
+ return $authenticatedUserRoleHierarchy <= $userRoleHierarchies[$minimalRequiredRole->value];
}
}
diff --git a/src/Domain/Authorization/Privilege.php b/src/Domain/Authorization/Privilege.php
index ee900fe4..54e04586 100644
--- a/src/Domain/Authorization/Privilege.php
+++ b/src/Domain/Authorization/Privilege.php
@@ -4,31 +4,39 @@
enum Privilege: string
{
- // ? Instead of the function hasPrivilege() privilege loaded via Ajax by the client checks if the letter is contained
- // in the name. For instance if update privilege is required, the client will check if privilege contains "U".
+ // The case values are the first letter of the privilege name.
+ // These are returned on a JSON Ajax request and used by the frontend to determine if the user has
+ // a certain privilege because JS doesn't have access to the hasPrivilege() function.
+ // For instance, if update privilege is required, the client will check if privilege value contains "U".
+
// No rights
case NONE = 'NONE';
- // Allowed to read all entries
+ // Allowed to read
case READ = 'R';
- // Allowed to read and create all entries
+ // Allowed to read and create
case CREATE = 'CR';
- // Allowed only to create (needed when user cannot see hidden note but may create one)
+ // Allowed only to create (needed when the user is not allowed to see hidden notes but may create some)
case ONLY_CREATE = 'C';
- // Allowed to read, create and update all entries
+ // Allowed to read, create and update
case UPDATE = 'CRU';
- // Allowed to read, create, update and delete all entries
+ // Allowed to read, create, update and delete
case DELETE = 'CRUD';
// Allowed to do everything on each note
// case ALL = 'ALL';
/**
- * Check if granted to perform action with given needed rights.
- * Not sure though if it's smart to implement a hierarchical system
- * for CRUD operations or if a collection of privileges would be better.
+ * Checks if the current privilege allows for the required privilege.
*
- * @param Privilege $requiredPrivilege
+ * This method uses a match expression to check if the current privilege ($this) allows for the required privilege.
+ * The match expression checks the required privilege against each possible privilege case.
+ * For each case, it checks if the current privilege is in an array of privileges that allow for the required privilege.
+ * If the current privilege is in the array, the method returns true, indicating that the required privilege is allowed.
+ * If the current privilege is not in the array, the method continues to the next case.
+ * If no cases match the required privilege, the method returns false,
+ * indicating that the required privilege is not allowed.
*
- * @return bool
+ * @param Privilege $requiredPrivilege The required privilege.
+ * @return bool True if the current privilege allows for the required privilege, false otherwise.
*/
public function hasPrivilege(Privilege $requiredPrivilege): bool
{
@@ -36,7 +44,7 @@ public function hasPrivilege(Privilege $requiredPrivilege): bool
// Privilege READ is true if $this is either READ, CREATE, UPDATE or DELETE
self::READ => in_array($this, [self::READ, self::CREATE, self::UPDATE, self::DELETE], true),
self::CREATE => in_array($this, [self::CREATE, self::ONLY_CREATE, self::UPDATE, self::DELETE], true),
- self::ONLY_CREATE => in_array($this, [self::CREATE, self::ONLY_CREATE], true), // should not be used
+ self::ONLY_CREATE => in_array($this, [self::CREATE, self::ONLY_CREATE], true),
self::UPDATE => in_array($this, [self::UPDATE, self::DELETE], true),
self::DELETE => $this === self::DELETE,
self::NONE => true,
diff --git a/src/Domain/Client/Data/ClientReadResult.php b/src/Domain/Client/Data/ClientReadResult.php
index 5375c4af..01f28406 100644
--- a/src/Domain/Client/Data/ClientReadResult.php
+++ b/src/Domain/Client/Data/ClientReadResult.php
@@ -19,8 +19,8 @@ class ClientReadResult extends ClientData
// and a new class ClientReadResultAggregateData could be created extending this one as it contains more attributes
public ?NoteData $mainNoteData = null; // Main note data
- // Client main data privilege (first-, second name, phone, email, location)
- public ?Privilege $mainDataPrivilege = null;
+ // Client personal info privilege (first-, second name, phone, email, location)
+ public ?Privilege $generalPrivilege = null;
public ?Privilege $clientStatusPrivilege = null;
public ?Privilege $assignedUserPrivilege = null;
public ?Privilege $noteCreatePrivilege = null;
@@ -48,7 +48,7 @@ public function jsonSerialize(): array
'notesAmount' => $this->notesAmount,
'mainNoteData' => $this->mainNoteData,
- 'mainDataPrivilege' => $this->mainDataPrivilege?->value,
+ 'personalInfoPrivilege' => $this->generalPrivilege?->value,
'clientStatusPrivilege' => $this->clientStatusPrivilege?->value,
'assignedUserPrivilege' => $this->assignedUserPrivilege?->value,
'noteCreatePrivilege' => $this->noteCreatePrivilege?->value,
diff --git a/src/Domain/Client/Service/Authorization/ClientAuthorizationChecker.php b/src/Domain/Client/Service/Authorization/ClientAuthorizationChecker.php
deleted file mode 100644
index 911ebe5d..00000000
--- a/src/Domain/Client/Service/Authorization/ClientAuthorizationChecker.php
+++ /dev/null
@@ -1,306 +0,0 @@
-loggedInUserId = $this->userNetworkSessionData->userId;
- }
-
- /**
- * Check if authenticated user is allowed to create client.
- *
- * @param ClientData|null $client null if check before actual client creation
- * request otherwise it has to be provided
- *
- * @return bool
- */
- public function isGrantedToCreate(?ClientData $client = null): bool
- {
- if ($this->loggedInUserId) {
- $authenticatedUserRoleData = $this->userRoleFinderRepository->getUserRoleDataFromUser(
- $this->loggedInUserId
- );
- // Returns array with role name as key and hierarchy as value [role_name => hierarchy_int]
- // * Lower hierarchy number means higher privileged role
- $userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
-
- // Newcomer is not allowed to do anything
- // If hierarchy number is greater or equals newcomer it means that user is not allowed
- if ($authenticatedUserRoleData->hierarchy <= $userRoleHierarchies[UserRole::ADVISOR->value]) {
- // Advisor may create clients but can only assign them to himself or leave it unassigned.
- // If $client is null (not provided), advisor is authorized (being used to check if create btn should
- // be displayed in template)
- if ($client === null || $this->isGrantedToAssignUserToClient($client->userId)) {
- // If authenticated user is at least advisor and client user id is himself, or it's a
- // managing_advisor (logic in isGrantedToAssignUserToClient) -> granted to create
- return true;
- }
- }
- }
-
- $this->logger->notice(
- 'User ' . $this->loggedInUserId . ' tried to create client but isn\'t allowed.'
- );
-
- return false;
- }
-
- /**
- * Check if authenticated user is allowed to assign user to client
- * (client id not needed as the same rules applies for new clients and all existing clients)
- * In own function to be used to filter dropdown options for frontend.
- *
- * @param int|null $assignedUserId
- * @param UserRoleData|null $authenticatedUserRoleData optional so that it can be called outside this class
- * @param array|null $userRoleHierarchies optional so that it can be called outside this class
- *
- * @return bool|void
- */
- public function isGrantedToAssignUserToClient(
- ?int $assignedUserId,
- ?UserRoleData $authenticatedUserRoleData = null,
- ?array $userRoleHierarchies = null
- ) {
- if ($this->loggedInUserId) {
- // $authenticatedUserRoleData and $userRoleHierarchies passed as arguments if called inside this class
- if ($authenticatedUserRoleData === null) {
- $authenticatedUserRoleData = $this->userRoleFinderRepository->getUserRoleDataFromUser(
- $this->loggedInUserId
- );
- }
- if ($userRoleHierarchies === null) {
- // Returns array with role name as key and hierarchy as value [role_name => hierarchy_int]
- // * Lower hierarchy number means higher privileged role
- $userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
- }
-
- // If hierarchy number is greater or equals advisor it means that user may create client
- if ($authenticatedUserRoleData->hierarchy <= $userRoleHierarchies[UserRole::ADVISOR->value]) {
- // Advisor may create clients but can only assign them to himself or leave it unassigned
- if ($assignedUserId === $this->loggedInUserId || $assignedUserId === null
- // managing advisor can link user to someone else
- || $authenticatedUserRoleData->hierarchy <= $userRoleHierarchies[UserRole::MANAGING_ADVISOR->value]) {
- // If authenticated user is at least advisor and client user id is authenticated user himself,
- // null (unassigned) or authenticated user is managing_advisor -> granted to assign
- return true;
- }
- }
- }
- }
-
- /**
- * Logic to check if logged-in user is granted to update client.
- *
- * @param array $clientDataToUpdate validated array with as key the column to
- * update and value the new value (or fictive "value").
- * There may be one or multiple entries, depending on what the user wants to update.
- * @param int|null $ownerId user_id linked to client
- * @param bool $log log if forbidden (expected false when function is called for privilege setting)
- *
- * @return bool
- */
- public function isGrantedToUpdate(array $clientDataToUpdate, ?int $ownerId, bool $log = true): bool
- {
- $grantedUpdateKeys = [];
- if ($this->loggedInUserId) {
- $authenticatedUserRoleData = $this->userRoleFinderRepository->getUserRoleDataFromUser(
- $this->loggedInUserId
- );
- // Returns array with role name as key and hierarchy as value [role_name => hierarchy_int]
- // * Lower hierarchy number means higher privileged role
- $userRoleHierarchies = $this->userRoleFinderRepository->getUserRolesHierarchies();
-
- // Roles: newcomer < advisor < managing_advisor < administrator
- // If logged-in hierarchy value is smaller or equal advisor -> granted
- if ($authenticatedUserRoleData->hierarchy <= $userRoleHierarchies['advisor']) {
- // Things that advisor is allowed to change for all client records even when not owner
- // "main_data" is the same as the group of columns that follows it
- if (array_key_exists('main_data', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'main_data';
- }
- // Same as main data but in separate columns to be returned as granted keys
- if (array_key_exists('first_name', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'first_name';
- }
- if (array_key_exists('last_name', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'last_name';
- }
- if (array_key_exists('birthdate', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'birthdate';
- }
- if (array_key_exists('location', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'location';
- }
- if (array_key_exists('phone', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'phone';
- }
- if (array_key_exists('email', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'email';
- }
- if (array_key_exists('sex', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'sex';
- }
- if (array_key_exists('vigilance_level', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'vigilance_level';
- }
- /** Update main note authorization is in @see NoteAuthorizationChecker::isGrantedToUpdate () */
-
- // Everything that owner and managing_advisor is permitted to do
- // advisor may only edit client_status_id if he's owner | managing_advisor and higher is allowed
- if ($this->loggedInUserId === $ownerId
- || $authenticatedUserRoleData->hierarchy <= $userRoleHierarchies['managing_advisor']) {
- // Check if client_status_id is among data to be changed if yes add it to $grantedUpdateKeys array
- if (array_key_exists('client_status_id', $clientDataToUpdate)) {
- $grantedUpdateKeys[] = 'client_status_id';
- }
- }
- // Things that only managing_advisor and higher privileged are allowed to do
- if ($authenticatedUserRoleData->hierarchy <= $userRoleHierarchies['managing_advisor']
- // isGrantedToAssignUserToClient can NOT be used as it expects a real user_id which is not provided
- // in the case where user_id value is the string "value" from (ClientAuthGetter) to check if
- // the authenticated user is allowed to change assigned user (html