From 370ae3438e468f168f5793a720125e6d53943bd1 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 20 Oct 2021 22:29:23 +0200 Subject: [PATCH] new: [user:registration] Added user self-registration feature --- .../InboxProcessors/UserInboxProcessor.php | 24 ++- .../templates/User/Registration.php | 172 +++++++++--------- src/Controller/Component/ACLComponent.php | 2 +- src/Controller/UsersController.php | 38 ++-- .../CerebrateSettingsProvider.php | 10 + templates/Users/login.php | 107 +++++------ templates/Users/register.php | 42 +++++ webroot/css/main.css | 8 + 8 files changed, 244 insertions(+), 159 deletions(-) create mode 100644 templates/Users/register.php diff --git a/libraries/default/InboxProcessors/UserInboxProcessor.php b/libraries/default/InboxProcessors/UserInboxProcessor.php index 074ce13..53312d5 100644 --- a/libraries/default/InboxProcessors/UserInboxProcessor.php +++ b/libraries/default/InboxProcessors/UserInboxProcessor.php @@ -1,5 +1,6 @@ 'E-mail must be valid' ]) ->notEmpty('first_name', 'A first name must be provided') - ->notEmpty('last_name', 'A last name must be provided'); + ->notEmpty('last_name', 'A last name must be provided') + ->add('password', 'password_complexity', [ + 'rule' => function($value, $context) { + if (!preg_match('/^((?=.*\d)|(?=.*\W+))(?![\n])(?=.*[A-Z])(?=.*[a-z]).*$|.{16,}/s', $value) || strlen($value) < 12) { + return false; + } + return true; + }, + 'message' => __('Invalid password. Passwords have to be either 16 character long or 12 character long with 3/4 special groups.') + ]); } public function create($requestData) { $this->validateRequestData($requestData); + $requestData['data']['password'] = (new DefaultPasswordHasher())->hash($requestData['data']['password']); $requestData['title'] = __('User account creation requested for {0}', $requestData['data']['email']); - return parent::create($requestData); + $creationResponse = parent::create($requestData); + $creationResponse['message'] = __('User account creation requested. Please wait for an admin to approve your account.'); + return $creationResponse; } public function getViewVariables($request) @@ -72,6 +85,11 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr 'username' => !empty($request['data']['username']) ? $request['data']['username'] : '', 'role_id' => !empty($request['data']['role_id']) ? $request['data']['role_id'] : '', 'disabled' => !empty($request['data']['disabled']) ? $request['data']['disabled'] : '', + + 'email' => !empty($request['data']['email']) ? $request['data']['email'] : '', + 'first_name' => !empty($request['data']['first_name']) ? $request['data']['first_name'] : '', + 'last_name' => !empty($request['data']['last_name']) ? $request['data']['last_name'] : '', + 'position' => !empty($request['data']['position']) ? $request['data']['position'] : '', ]); return [ 'dropdownData' => $dropdownData, @@ -82,6 +100,7 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr public function process($id, $requestData, $inboxRequest) { + $hashedPassword = $inboxRequest['data']['password']; if ($requestData['individual_id'] == -1) { $individual = $this->Users->Individuals->newEntity([ 'uuid' => $requestData['uuid'], @@ -101,6 +120,7 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr 'role_id' => $requestData['role_id'], 'disabled' => $requestData['disabled'], ]); + $user->set('password', $hashedPassword, ['setter' => false]); // ignore default password hashing as it has already been hashed $user = $this->Users->save($user); if ($user !== false) { diff --git a/libraries/default/InboxProcessors/templates/User/Registration.php b/libraries/default/InboxProcessors/templates/User/Registration.php index ebcc819..2eaffb0 100644 --- a/libraries/default/InboxProcessors/templates/User/Registration.php +++ b/libraries/default/InboxProcessors/templates/User/Registration.php @@ -1,99 +1,88 @@ element('genericElements/Form/genericForm', [ - 'entity' => $userEntity, - 'ajax' => false, - 'raw' => true, - 'data' => [ - 'description' => __('Create user account'), - 'model' => 'User', - 'fields' => [ - [ - 'field' => 'individual_id', - 'type' => 'dropdown', - 'label' => __('Associated individual'), - 'options' => $dropdownData['individual'], - ], - [ - 'field' => 'username', - 'autocomplete' => 'off', - ], - [ - 'field' => 'role_id', - 'type' => 'dropdown', - 'label' => __('Role'), - 'options' => $dropdownData['role'] - ], - [ - 'field' => 'disabled', - 'type' => 'checkbox', - 'label' => 'Disable' - ] +$combinedForm = $this->element('genericElements/Form/genericForm', [ + 'entity' => $userEntity, + 'ajax' => false, + 'raw' => true, + 'data' => [ + 'description' => __('Create user account'), + 'model' => 'User', + 'fields' => [ + [ + 'field' => 'individual_id', + 'type' => 'dropdown', + 'label' => __('Associated individual'), + 'options' => $dropdownData['individual'], ], - 'submit' => [ - 'action' => $this->request->getParam('action') - ] - ] - ]); - - $formIndividual = $this->element('genericElements/Form/genericForm', [ - 'entity' => $individualEntity, - 'ajax' => false, - 'raw' => true, - 'data' => [ - 'description' => __('Create individual'), - 'model' => 'Individual', - 'fields' => [ - [ - 'field' => 'email', - 'autocomplete' => 'off' - ], - [ - 'field' => 'uuid', - 'label' => 'UUID', - 'type' => 'uuid', - 'autocomplete' => 'off' - ], - [ - 'field' => 'first_name', - 'autocomplete' => 'off' - ], - [ - 'field' => 'last_name', - 'autocomplete' => 'off' - ], - [ - 'field' => 'position', - 'autocomplete' => 'off' - ], + [ + 'field' => 'username', + 'autocomplete' => 'off', + ], + [ + 'field' => 'role_id', + 'type' => 'dropdown', + 'label' => __('Role'), + 'options' => $dropdownData['role'] + ], + [ + 'field' => 'disabled', + 'type' => 'checkbox', + 'label' => 'Disable' ], - 'submit' => [ - 'action' => $this->request->getParam('action') - ] - ] - ]); - echo $this->Bootstrap->modal([ - 'title' => __('Register user'), - 'size' => 'lg', - 'type' => 'confirm', - 'bodyHtml' => sprintf('
%s
%s
', - $formUser, - $formIndividual - ), - 'confirmText' => __('Create user'), - 'confirmFunction' => 'submitRegistration' - ]); + sprintf('
%s
', __('Create individual')), + [ + 'field' => 'email', + 'autocomplete' => 'off' + ], + [ + 'field' => 'uuid', + 'label' => 'UUID', + 'type' => 'uuid', + 'autocomplete' => 'off' + ], + [ + 'field' => 'first_name', + 'autocomplete' => 'off' + ], + [ + 'field' => 'last_name', + 'autocomplete' => 'off' + ], + [ + 'field' => 'position', + 'autocomplete' => 'off' + ], + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ] + ] +]); + + +echo $this->Bootstrap->modal([ + 'title' => __('Register user'), + 'size' => 'lg', + 'type' => 'confirm', + 'bodyHtml' => sprintf( + '
%s
', + $combinedForm + ), + 'confirmText' => __('Create user'), + 'confirmFunction' => 'submitRegistration' +]); ?> \ No newline at end of file diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index 1376d96..5feae97 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -254,7 +254,7 @@ class ACLComponent extends Component */ public function setPublicInterfaces(): void { - $this->Authentication->allowUnauthenticated(['login']); + $this->Authentication->allowUnauthenticated(['login', 'register']); } private function checkAccessInternal($controller, $action, $soft): bool diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 2205f08..d2235a6 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -6,6 +6,8 @@ use Cake\Utility\Hash; use Cake\Utility\Text; use Cake\ORM\TableRegistry; use \Cake\Database\Expression\QueryExpression; +use Cake\Http\Exception\UnauthorizedException; +use Cake\Core\Configure; class UsersController extends AppController { @@ -160,19 +162,27 @@ class UsersController extends AppController public function register() { - $this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); - $processor = $this->InboxProcessors->getProcessor('User', 'Registration'); - $data = [ - 'origin' => '127.0.0.1', - 'comment' => 'Hi there!, please create an account', - 'data' => [ - 'username' => 'foobar', - 'email' => 'foobar@admin.test', - 'first_name' => 'foo', - 'last_name' => 'bar', - ], - ]; - $processorResult = $processor->create($data); - return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']); + if (empty(Configure::read('Cerebrate')['security.registration.self-registration'])) { + throw new UnauthorizedException(__('User self-registration is not open.')); + } + if ($this->request->is('post')) { + $data = $this->request->getData(); + $this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); + $processor = $this->InboxProcessors->getProcessor('User', 'Registration'); + $data = [ + 'origin' => $this->request->clientIp(), + 'comment' => '-no comment-', + 'data' => [ + 'username' => $data['username'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'password' => $data['password'], + ], + ]; + $processorResult = $processor->create($data); + return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']); + } + $this->viewBuilder()->setLayout('login'); } } diff --git a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php index d06bd43..d4e02f5 100644 --- a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php +++ b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php @@ -240,6 +240,16 @@ class CerebrateSettingsProvider extends BaseSettingsProvider ] ], 'Security' => [ + 'Registration' => [ + 'Registration' => [ + 'security.registration.self-registration' => [ + 'name' => __('Allow self-registration'), + 'type' => 'boolean', + 'description' => __('Enable the self-registration feature where user can request account creation. Admin can view the request and accept it in the application inbox.'), + 'default' => false, + ], + ] + ], 'Development' => [ 'Debugging' => [ 'security.debug' => [ diff --git a/templates/Users/login.php b/templates/Users/login.php index 7cc35d0..1be50cd 100644 --- a/templates/Users/login.php +++ b/templates/Users/login.php @@ -1,53 +1,58 @@ '; - echo sprintf( - '
%s
', - $this->Html->image('logo-purple.png', [ - 'alt' => __('Cerebrate logo'), - 'width' => 100, 'height' => 100, - 'style' => ['filter: drop-shadow(4px 4px 4px #924da666);'] - ]) - ); - echo sprintf('

%s

', __('Sign in')); - $template = [ - 'inputContainer' => '
{{content}}
', - 'formGroup' => '{{input}}{{label}}', - 'submitContainer' => '
{{content}}
', - ]; - $this->Form->setTemplates($template); - echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login']]); - echo $this->Form->control('username', ['label' => 'Username', 'class' => 'form-control mb-2', 'placeholder' => __('Username')]); - echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]); - echo $this->Form->control(__('Submit'), ['type' => 'submit', 'class' => 'btn btn-primary']); - echo $this->Form->end(); - - if (!empty(Configure::read('keycloak'))) { - echo sprintf('

%s
', __('Or')); - echo $this->Form->create(null, [ - 'url' => Cake\Routing\Router::url([ - 'prefix' => false, - 'plugin' => 'ADmad/SocialAuth', - 'controller' => 'Auth', - 'action' => 'login', - 'provider' => 'keycloak', - '?' => ['redirect' => $this->request->getQuery('redirect')] - ]), - ]); - echo $this->Bootstrap->button([ - 'type' => 'submit', - 'text' => __('Login with Keycloak'), - 'variant' => 'secondary', - 'class' => ['d-block', 'w-100'], - 'image' => [ - 'path' => '/img/keycloak_logo.png', - 'alt' => 'Keycloak' - ] - ]); - echo $this->Form->end(); - } - echo ''; - +use Cake\Core\Configure; ?> - + +
+ %s
', + $this->Html->image('logo-purple.png', [ + 'alt' => __('Cerebrate logo'), + 'width' => 100, 'height' => 100, + 'style' => ['filter: drop-shadow(4px 4px 4px #924da666);'] + ]) + ); + echo sprintf('

%s

', __('Sign up')); + $template = [ + 'inputContainer' => '
{{content}}
', + 'formGroup' => '{{input}}{{label}}', + 'submitContainer' => '
{{content}}
', + ]; + $this->Form->setTemplates($template); + echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login']]); + echo $this->Form->control('username', ['label' => 'Username', 'class' => 'form-control mb-2', 'placeholder' => __('Username')]); + echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]); + echo $this->Form->control(__('Login'), ['type' => 'submit', 'class' => 'btn btn-primary']); + echo $this->Form->end(); + if (!empty(Configure::read('Cerebrate')['security.registration.self-registration'])) { + echo '
'; + echo sprintf('%s %s', __('Doesn\'t have an account?'), __('Sign up')); + echo '
'; + } + + if (!empty(Configure::read('keycloak'))) { + echo sprintf('

%s
', __('Or')); + echo $this->Form->create(null, [ + 'url' => Cake\Routing\Router::url([ + 'prefix' => false, + 'plugin' => 'ADmad/SocialAuth', + 'controller' => 'Auth', + 'action' => 'login', + 'provider' => 'keycloak', + '?' => ['redirect' => $this->request->getQuery('redirect')] + ]), + ]); + echo $this->Bootstrap->button([ + 'type' => 'submit', + 'text' => __('Login with Keycloak'), + 'variant' => 'secondary', + 'class' => ['d-block', 'w-100'], + 'image' => [ + 'path' => '/img/keycloak_logo.png', + 'alt' => 'Keycloak' + ] + ]); + echo $this->Form->end(); + } + ?> + \ No newline at end of file diff --git a/templates/Users/register.php b/templates/Users/register.php new file mode 100644 index 0000000..1831bbe --- /dev/null +++ b/templates/Users/register.php @@ -0,0 +1,42 @@ + + +
+ %s
', + $this->Html->image('logo-purple.png', [ + 'alt' => __('Cerebrate logo'), + 'width' => 100, 'height' => 100, + 'style' => ['filter: drop-shadow(4px 4px 4px #924da666);'] + ]) + ); + echo sprintf('

%s

', __('Sign in')); + $template = [ + 'inputContainer' => '
{{content}}
', + 'formGroup' => '{{label}}{{input}}', + 'submitContainer' => '
{{content}}
', + 'label' => '' + ]; + $this->Form->setTemplates($template); + echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'register']]); + + echo $this->Form->control('username', ['label' => __('Username'), 'class' => 'form-control mb-2']); + echo $this->Form->control('email', ['label' => __('E-mail Address'), 'class' => 'form-control mb-3']); + + echo '
'; + echo '
'; + echo $this->Form->control('first_name', ['label' => __('First Name'), 'class' => 'form-control']); + echo '
'; + echo '
'; + echo $this->Form->control('last_name', ['label' => __('Last Name'), 'class' => 'form-control mb-2']); + echo '
'; + echo '
'; + + echo $this->Form->control('password', ['type' => 'password', 'label' => __('Password'), 'class' => 'form-control mb-4']); + + echo $this->Form->control(__('Sign up'), ['type' => 'submit', 'class' => 'btn btn-primary']); + echo $this->Form->end(); + ?> + \ No newline at end of file diff --git a/webroot/css/main.css b/webroot/css/main.css index a4715a0..def9e0f 100644 --- a/webroot/css/main.css +++ b/webroot/css/main.css @@ -167,4 +167,12 @@ input[type="checkbox"]:disabled.change-cursor { -webkit-mask-size: contain; -webkit-mask-repeat: no-repeat; -webkit-mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='223.995' width='383.98752' viewBox='0 0 383.98752 223.995' role='img'%3E%3Cpath d='m 367.9975,0 h -118.06 c -21.38,0 -32.09,25.85 -16.97,40.97 l 32.4,32.4 -73.37,73.38 -73.37,-73.37 c -12.5,-12.5 -32.76,-12.5 -45.25,0 l -68.69,68.69 c -6.25,6.25 -6.25,16.38 0,22.63 l 22.62,22.62 c 6.25,6.25 16.38,6.25 22.63,0 l 46.06,-46.07 73.37,73.37 c 12.5,12.5 32.76,12.5 45.25,0 l 96,-96 32.4,32.4 c 15.12,15.12 40.97,4.41 40.97,-16.97 V 16 c 0.01,-8.84 -7.15,-16 -15.99,-16 z' /%3E%3C/svg%3E%0A"); +} + +.fs-7 { + font-size: .875rem !important; +} + +.fs-8 { + font-size: .7rem !important; } \ No newline at end of file