Skip to content

Commit

Permalink
Merge pull request #223 from amin-ous/upstream/develop/issues/65-68
Browse files Browse the repository at this point in the history
  • Loading branch information
GhaziTriki authored Sep 9, 2022
2 parents 2057994 + f8fe537 commit 4dba295
Show file tree
Hide file tree
Showing 68 changed files with 2,456 additions and 529 deletions.
3 changes: 3 additions & 0 deletions hivelvet-backend/app/config/access-install.ini
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ allow @set_locale = *
; logs routes
allow POST @logs_collect = *

; users routes
allow POST @admin_exists = *

; settings routes
allow GET @presets_collect = *
allow GET @settings_collect = *
Expand Down
3 changes: 3 additions & 0 deletions hivelvet-backend/app/config/routes-install.ini
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ PUT @set_locale : /set-locale/@locale [ajax] = Actions\Account\SetLocale-
; logs routes
POST @logs_collect : /logs = Actions\Logs\Collect->execute

; users routes
POST @admin_exists : /users/collect-admin = Actions\Users\Collect->execute

; settings routes
GET @presets_collect : /collect-presets = Actions\Presets\Collect->execute
GET @settings_collect : /collect-settings = Actions\Settings\Collect->execute
Expand Down
517 changes: 517 additions & 0 deletions hivelvet-backend/app/security/dictionary/en-US.json

Large diffs are not rendered by default.

73 changes: 57 additions & 16 deletions hivelvet-backend/app/src/Actions/Account/ChangePassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@
use Actions\Base as BaseAction;
use Enum\ResetTokenStatus;
use Enum\ResponseCode;
use Enum\UserStatus;
use Models\ResetPasswordToken;
use Models\User;
use Respect\Validation\Validator;
use Utils\SecurityUtils;
use Validation\DataChecker;

/**
* Class ChangePassword.
Expand All @@ -40,28 +44,65 @@ public function execute($f3): void
$password = $form['password'];
$resetToken = new ResetPasswordToken();

$dataChecker = new DataChecker();
$dataChecker->verify($password, Validator::length(8)->setName('password'));

/** @todo : move to locales */
$error_message = 'Password could not be changed';
$response_code = ResponseCode::HTTP_BAD_REQUEST;
if ($resetToken->getByToken($form['token'])) {
if (!$resetToken->dry()) {
$user = new User();
$user = $user->getById($resetToken->user_id);
$user->password = $password;
$resetToken->status = ResetTokenStatus::CONSUMED;

try {
$resetToken->save();
$user->save();
} catch (\Exception $e) {
$message = 'password could not be changed';
$this->logger->error('reset password error : password could not be changed', ['error' => $e->getMessage()]);
$this->renderJson(['message' => $message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);
if ($dataChecker->allValid()) {
$user = new User();
$user = $user->getById($resetToken->user_id);
$resetToken->status = ResetTokenStatus::CONSUMED;

return;
if (SecurityUtils::isGdprCompliant($password)) {
$this->logger->error($error_message, ['error' => 'Only use letters, numbers, and common punctuation characters']);
$this->renderJson(['message' => 'Only use letters, numbers, and common punctuation characters'], $response_code);
} else {
$this->changePassword($user, $password, $resetToken, $error_message, $response_code);
}
} else {
$this->logger->error($error_message, ['errors' => $dataChecker->getErrors()]);
$this->renderJson(['errors' => $dataChecker->getErrors()], ResponseCode::HTTP_UNPROCESSABLE_ENTITY);
}
} else {
$this->logger->error($error_message);
}
}
}

/**
* @param $user
* @param $password
* @param $resetToken
* @param $error_message
* @param $response_code
*
* @throws \JsonException
*/
private function changePassword($user, $password, $resetToken, $error_message, $response_code): void
{
$next = SecurityUtils::credentialsAreCommon($user->username, $user->email, $password, $error_message, $response_code);
if ($user->verifyPassword($password) && $next) {
$this->logger->error($error_message, ['error' => 'New password cannot be the same as your old password']);
$this->renderJson(['message' => 'New password cannot be the same as your old password']);
} elseif ($next) {
try {
$user->password = $password;
$user->status = UserStatus::ACTIVE;
$resetToken->save();
$user->save();
} catch (\Exception $e) {
$message = 'Password could not be changed';
$this->logger->error($error_message, ['error' => $e->getMessage()]);
$this->renderJson(['message' => $message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);

$this->renderJson(['result' => 'success']);
return;
}
} else {
$this->logger->error('reset password error : password could not be changed');

$this->renderJson(['result' => 'success']);
}
}
}
84 changes: 56 additions & 28 deletions hivelvet-backend/app/src/Actions/Account/Login.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,41 +41,69 @@ public function authorise($f3): void
{
$form = $this->getDecodedBody();

$dataChecker = new DataChecker();
$dataChecker = new DataChecker();
$dataChecker->verify($email = $form['email'], Validator::email()->setName('email'));
$dataChecker->verify($form['password'], Validator::length(4)->setName('password'));
$dataChecker->verify($form['password'], Validator::length(8)->setName('password'));

$userInfos = [];
$userInfos = [];
$error_message = 'Could not authenticate user with email';
if ($dataChecker->allValid()) {
$user = new User();
$user = $user->getByEmail($email);
$this->logger->info('Login attempt using email', ['email' => $email]);
// Check if the user exists
if ($user->valid() && UserStatus::ACTIVE === $user->status && $user->verifyPassword($form['password'])) {
// @todo: test UserRole::API !== $user->role->name
// valid credentials
$this->session->authorizeUser($user);
$this->login($form, $email, $userInfos, $dataChecker, $error_message);
} else {
$this->logger->error($error_message, ['errors' => $dataChecker->getErrors()]);
$this->renderJson(['errors' => $dataChecker->getErrors()], ResponseCode::HTTP_UNPROCESSABLE_ENTITY);
}
}

$user->last_login = Time::db();
$user->save();
private function login($form, $email, $userInfos, $dataChecker, $error_message): void
{
$user = new User();
$user = $user->getByEmail($email);
$this->logger->info('Login attempt using email', ['email' => $email]);
// Check if the user exists
if ($user->valid() && UserStatus::ACTIVE === $user->status && $user->verifyPassword($form['password'])) {
// @todo: test UserRole::API !== $user->role->name
// valid credentials
$this->session->authorizeUser($user);

// @todo: store role in redis cache to allow routes
$this->f3->set('role', $user->role->name);
$user->last_login = Time::db();
$user->password_attempts = 3;
$user->save();

// @todo: store locale in user prefs table
// $this->session->set('locale', $user->locale);
$userInfos = [
'username' => $user->username,
'email' => $user->email,
'role' => $user->role->name,
];
$this->logger->info('User successfully logged in', ['email' => $email]);
$this->renderJson($userInfos);
}
}
// @todo: store role in redis cache to allow routes
$this->f3->set('role', $user->role->name);

if (empty($userInfos) || \count($dataChecker->getErrors()) > 0) {
$this->logger->error('Could not authenticate user with email', ['email' => $email]);
// @todo: store locale in user prefs table
// $this->session->set('locale', $user->locale);
$userInfos = [
'username' => $user->username,
'email' => $user->email,
'role' => $user->role->name,
];
$this->logger->info('User successfully logged in', ['email' => $email]);
$this->renderJson($userInfos);
} elseif ($user->valid() && UserStatus::ACTIVE === $user->status && !$user->verifyPassword($form['password']) && $user->password_attempts > 1) {
--$user->password_attempts;
$user->save();
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => "Wrong password. Attempts left : {$user->password_attempts}"], ResponseCode::HTTP_BAD_REQUEST);
} elseif ($user->valid() && 0 === $user->password_attempts || 1 === $user->password_attempts) {
$user->password_attempts = 0;
$user->status = UserStatus::INACTIVE;
$user->save();
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'Your account has been locked because you have reached the maximum number of invalid sign-in attempts. You can contact the administrator or click here to receive an email containing instructions on how to unlock your account'], ResponseCode::HTTP_BAD_REQUEST);
} elseif ($user->valid() && UserStatus::PENDING === $user->status) {
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'Your account is not active. Please contact your administrator'], ResponseCode::HTTP_BAD_REQUEST);
} elseif ($user->valid() && UserStatus::DELETED === $user->status) {
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'Your account has been disabled for violating our terms'], ResponseCode::HTTP_BAD_REQUEST);
} elseif (!$user->valid()) {
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'User does not exist with this email'], ResponseCode::HTTP_BAD_REQUEST);
} elseif (empty($userInfos) || \count($dataChecker->getErrors()) > 0) {
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'Invalid Authentication data'], ResponseCode::HTTP_BAD_REQUEST);
}
}
Expand Down
54 changes: 32 additions & 22 deletions hivelvet-backend/app/src/Actions/Account/Register.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Enum\UserStatus;
use Models\User;
use Respect\Validation\Validator;
use Utils\SecurityUtils;
use Validation\DataChecker;

/**
Expand All @@ -41,39 +42,48 @@ public function signup($f3): void

$dataChecker->verify($form['username'], Validator::length(4)->setName('username'));
$dataChecker->verify($form['email'], Validator::email()->setName('email'));
$dataChecker->verify($form['password'], Validator::length(4)->setName('password'));
$dataChecker->verify($form['confirmPassword'], Validator::length(4)->equals($form['password'])->setName('confirmPassword'));
$dataChecker->verify($form['password'], Validator::length(8)->setName('password'));
$dataChecker->verify($form['confirmPassword'], Validator::length(8)->equals($form['password'])->setName('confirmPassword'));
// @fixme: the agreement must be accepted only if there are terms for the website
// otherwise in the login it should look for available terms of they were not previously available and ask to accept them
$dataChecker->verify($form['agreement'], Validator::trueVal()->setName('agreement'));

/** @todo : move to locales */
$error_message = 'User could not be added';
$response_code = ResponseCode::HTTP_BAD_REQUEST;
if ($dataChecker->allValid()) {
$user = new User();
$error = $user->usernameOrEmailExists($form['username'], $form['email']);
if ($error) {
$this->logger->error('Registration error : user could not be added', ['error' => $error]);
$this->renderJson(['message' => $error], ResponseCode::HTTP_PRECONDITION_FAILED);
$user = new User();
if (!SecurityUtils::isGdprCompliant($form['password'])) {
$this->logger->error($error_message, ['error' => 'Only use letters, numbers, and common punctuation characters']);
$this->renderJson(['message' => 'Only use letters, numbers, and common punctuation characters'], $response_code);
} else {
$user->email = $form['email'];
$user->username = $form['username'];
$user->password = $form['password'];
$user->role_id = 2;
$user->status = UserStatus::PENDING;
$next = SecurityUtils::credentialsAreCommon($form['username'], $form['email'], $form['password'], $error_message, $response_code);
$users = $this->getUsersByUsernameOrEmail($form['username'], $form['email']);
$error = $user->usernameOrEmailExists($form['username'], $form['email'], $users);
if ($error && $next) {
$this->logger->error($error_message, ['error' => $error]);
$this->renderJson(['message' => $error], ResponseCode::HTTP_PRECONDITION_FAILED);
} elseif ($next) {
$user->email = $form['email'];
$user->username = $form['username'];
$user->password = $form['password'];
$user->role_id = 2;
$user->status = UserStatus::PENDING;

try {
$user->save();
} catch (\Exception $e) {
$message = 'user could not be added';
$this->logger->error('Registration error : user could not be added', ['user' => $user->toArray(), 'error' => $e->getMessage()]);
$this->renderJson(['message' => $message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);
try {
$user->save();
} catch (\Exception $e) {
$this->logger->error($error_message, ['user' => $user->toArray(), 'error' => $e->getMessage()]);
$this->renderJson(['message' => $error_message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);

return;
return;
}
$this->logger->info('User successfully registered', ['user' => $user->toArray()]);
$this->renderJson(['result' => 'success', ResponseCode::HTTP_CREATED]);
}
$this->logger->info('user successfully registered', ['user' => $user->toArray()]);
$this->renderJson(['result' => 'success', ResponseCode::HTTP_CREATED]);
}
} else {
$this->logger->error('Registration error', ['errors' => $dataChecker->getErrors()]);
$this->logger->error($error_message, ['errors' => $dataChecker->getErrors()]);
$this->renderJson(['errors' => $dataChecker->getErrors()], ResponseCode::HTTP_UNPROCESSABLE_ENTITY);
}
}
Expand Down
11 changes: 11 additions & 0 deletions hivelvet-backend/app/src/Actions/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,17 @@ protected function getCredentials(): array
return $credentials;
}

protected function getUsersByUsernameOrEmail(string $username, string $email): array
{
$dbname = 'hivelvet';
$user = 'hivelvet';
$secret = 'hivelvet';
$conn = pg_pconnect("host=localhost dbname={$dbname} user={$user} password={$secret}");
$result = pg_query_params($conn, 'SELECT username, email FROM public.users WHERE lower(username) = lower($1) OR lower(email) = lower($2)', [$username, $email]);

return pg_fetch_all($result);
}

private function parseXMLView(string $view = null): string
{
$xmlResponse = new SimpleXMLElement(Template::instance()->render($this->view . '.xml'));
Expand Down
5 changes: 4 additions & 1 deletion hivelvet-backend/app/src/Actions/Core/Install.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function execute($f3, $params): void

$dataChecker->verify($form['username'], Validator::length(4)->setName('username'));
$dataChecker->verify($form['email'], Validator::email()->setName('email'));
$dataChecker->verify($form['password'], Validator::length(4)->setName('password'));
$dataChecker->verify($form['password'], Validator::length(8)->setName('password'));

$dataChecker->verify($form['company_name'], Validator::notEmpty()->setName('company_name'));
$dataChecker->verify($form['company_url'], Validator::url()->setName('company_url'));
Expand Down Expand Up @@ -99,6 +99,7 @@ public function execute($f3, $params): void
$defaultSettings->accent_color = $colors['accent_color'];
$defaultSettings->additional_color = $colors['add_color'];

// @fixme: should not have embedded try/catch here
try {
$defaultSettings->save();
$this->logger->info('Initial application setup : Update settings', ['settings' => $defaultSettings->toArray()]);
Expand All @@ -114,6 +115,7 @@ public function execute($f3, $params): void
$presetSettings->name = $subcategory['name'];
$presetSettings->enabled = $subcategory['status'];

// @fixme: should not have embedded try/catch here
try {
$presetSettings->save();
$this->logger->info('Initial application setup : Add preset settings', ['preset' => $presetSettings->toArray()]);
Expand All @@ -140,6 +142,7 @@ public function execute($f3, $params): void
// assign admin created to role admin
$user->role_id = $roleAdmin->id;

// @fixme: should not have embedded try/catch here
try {
$user->save();
$this->logger->info('Initial application setup : Assign role to administrator user', ['user' => $user->toArray()]);
Expand Down
9 changes: 5 additions & 4 deletions hivelvet-backend/app/src/Actions/Roles/Add.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,24 @@ public function save($f3, $params): void

$dataChecker->verify($form['name'], Validator::notEmpty()->setName('name'));

$error_message = 'Role could not be added';
if ($dataChecker->allValid()) {
$checkRole = new Role();
$role = new Role();
$role->name = $form['name'];
if ($checkRole->nameExists($role->name)) {
$this->logger->error('Role could not be added', ['error' => 'Name already exist']);
$this->renderJson(['errors' => ['name' => 'Name already exist']], ResponseCode::HTTP_PRECONDITION_FAILED);
$this->logger->error($error_message, ['error' => 'Name already exists']);
$this->renderJson(['errors' => ['name' => 'Name already exists']], ResponseCode::HTTP_PRECONDITION_FAILED);
} else {
try {
$result = $role->saveRoleAndPermissions($form['permissions']);
if (!$result) {
$this->renderJson(['errors' => 'role could not be added'], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);
$this->renderJson(['errors' => $error_message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);

return;
}
} catch (\Exception $e) {
$this->logger->error('Role could not be added', ['error' => $e->getMessage()]);
$this->logger->error($error_message, ['error' => $e->getMessage()]);
$this->renderJson(['errors' => $e->getMessage()], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);

return;
Expand Down
Loading

0 comments on commit 4dba295

Please sign in to comment.