Skip to content

Commit

Permalink
Add password meter for registration and change password
Browse files Browse the repository at this point in the history
  • Loading branch information
ajibarra committed Apr 19, 2024
1 parent 4bb39ed commit 61a4887
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 13 deletions.
4 changes: 4 additions & 0 deletions config/users.php
Expand Up @@ -78,6 +78,10 @@
// use reCaptcha in login, valid values are false, true
'login' => false,
],
'passwordMeter' => [
'enabled' => true,
'requiredScore' => 3,
],
'Tos' => [
// determines if the user should include tos accepted
'required' => true,
Expand Down
19 changes: 17 additions & 2 deletions src/View/Helper/UserHelper.php
Expand Up @@ -152,6 +152,22 @@ public function addReCaptchaScript(): void
]);
}

public function addPasswordMeterStript(): void
{
$this->Html->script('CakeDC/Users.pswmeter', [
'block' => 'script',
]);
}

public function addPasswordMeter(): string
{
$this->addPasswordMeterStript();
$requiredScore = Configure::read('Users.passwordMeter.requiredScore', 3);
$script = $this->Html->scriptBlock("const requiredScore = $requiredScore", ['defer' => true]);

return $this->Html->tag('div', '', ['id' => 'pswmeter']) . $this->Html->tag('div', '', ['id' => 'pswmeter-message']) . $script;
}

/**
* Add reCaptcha to the form
*
Expand All @@ -174,8 +190,7 @@ public function addReCaptcha(): mixed
if (method_exists($this, $method)) {
try {
$this->Form->unlockField('g-recaptcha-response');
} catch (Exception $e) {
}
} catch (\Exception $e) {}

return $this->{$method}();
}
Expand Down
8 changes: 6 additions & 2 deletions templates/Users/change_password.php
Expand Up @@ -13,15 +13,19 @@
<?= $this->Form->control('password', [
'type' => 'password',
'required' => true,
'id' => 'new-password',
'label' => __d('cake_d_c/users', 'New password')]);
?>
<?php if (\Cake\Core\Configure::read('Users.passwordMeter')) : ?>
<?= $this->User->addPasswordMeter() ?>
<?php endif; ?>
<?= $this->Form->control('password_confirm', [
'type' => 'password',
'required' => true,
'label' => __d('cake_d_c/users', 'Confirm password')]);
?>

</fieldset>
<?= $this->Form->button(__d('cake_d_c/users', 'Submit')); ?>
<?= $this->Form->button(__d('cake_d_c/users', 'Submit'), ['id' => 'btn-submit']); ?>
<?= $this->Form->end() ?>
</div>
</div>
2 changes: 1 addition & 1 deletion templates/Users/profile.php
Expand Up @@ -32,7 +32,7 @@
<p><?= h($user->username) ?></p>
<h6 class="subheader"><?= __d('cake_d_c/users', 'Email') ?></h6>
<p><?= h($user->email) ?></p>
<?= $this->User->socialConnectLinkList($user->social_accounts) ?>
<?= $this->User->socialConnectLinkList($user->social_accounts ?? []) ?>
<?php
if (!empty($user->social_accounts)):
?>
Expand Down
7 changes: 5 additions & 2 deletions templates/Users/register.php
Expand Up @@ -19,7 +19,10 @@
<?php
echo $this->Form->control('username', ['label' => __d('cake_d_c/users', 'Username')]);
echo $this->Form->control('email', ['label' => __d('cake_d_c/users', 'Email')]);
echo $this->Form->control('password', ['label' => __d('cake_d_c/users', 'Password')]);
echo $this->Form->control('password', ['label' => __d('cake_d_c/users', 'Password'), 'id' => 'new-password']);
if (Configure::read('Users.passwordMeter')) {
echo $this->User->addPasswordMeter();
}
echo $this->Form->control('password_confirm', [
'required' => true,
'type' => 'password',
Expand All @@ -35,6 +38,6 @@
}
?>
</fieldset>
<?= $this->User->button(__d('cake_d_c/users', 'Submit')) ?>
<?= $this->User->button(__d('cake_d_c/users', 'Submit'), ['id' => 'btn-submit']) ?>
<?= $this->Form->end() ?>
</div>
Expand Up @@ -80,7 +80,7 @@ public function testRequestResetPasswordPostValidEmail()
$this->assertResponseContains('Please enter the new password');
$this->assertResponseContains('<input type="password" name="password" required="required"');
$this->assertResponseContains('<input type="password" name="password_confirm" required="required"');
$this->assertResponseContains('<button type="submit">Submit</button>');
$this->assertResponseContains('<button id="btn-submit" type="submit">Submit</button>');

$this->post('/users/change-password', [
'password' => '9080706050',
Expand Down
Expand Up @@ -43,13 +43,13 @@ public function testRegister()
$this->assertResponseContains('<legend>Add User</legend>');
$this->assertResponseContains('<input type="text" name="username" required="required"');
$this->assertResponseContains('<input type="email" name="email" required="required"');
$this->assertResponseContains('<input type="password" name="password" required="required"');
$this->assertResponseContains('<input type="password" name="password" id="new-password" required="required"');
$this->assertResponseContains('<input type="password" name="password_confirm" required="required"');
$this->assertResponseContains('<input type="text" name="first_name" id="first-name" maxlength="50"');
$this->assertResponseContains('<input type="text" name="last_name" id="last-name" maxlength="50"');
$this->assertResponseContains('<input type="hidden" name="tos" value="0"');
$this->assertResponseContains('<label for="tos"><input type="checkbox" name="tos" value="1" required="required" id="tos" aria-required="true">Accept TOS conditions?</label>');
$this->assertResponseContains('<button type="submit">Submit</button>');
$this->assertResponseContains('<button id="btn-submit" type="submit">Submit</button>');
}

/**
Expand Down Expand Up @@ -78,13 +78,13 @@ public function testRegisterPostWithErrors()
$this->assertResponseContains('<legend>Add User</legend>');
$this->assertResponseContains('<input type="text" name="username" required="required"');
$this->assertResponseContains('<input type="email" name="email" required="required"');
$this->assertResponseContains('<input type="password" name="password" required="required"');
$this->assertResponseContains('<input type="password" name="password" id="new-password" required="required"');
$this->assertResponseContains('<input type="password" name="password_confirm" required="required"');
$this->assertResponseContains('<input type="text" name="first_name" id="first-name" value="" maxlength="50"');
$this->assertResponseContains('<input type="text" name="last_name" id="last-name" value="" maxlength="50"');
$this->assertResponseContains('<input type="hidden" name="tos" value="0"');
$this->assertResponseContains('<label for="tos"><input type="checkbox" name="tos" value="1" required="required" id="tos" aria-required="true">Accept TOS conditions?</label>');
$this->assertResponseContains('<button type="submit">Submit</button>');
$this->assertResponseContains('<button id="btn-submit" type="submit">Submit</button>');
}

/**
Expand Down
Expand Up @@ -83,7 +83,7 @@ public function testCrud()
$this->assertResponseContains('<form method="post" accept-charset="utf-8" action="/users/change-password/00000000-0000-0000-0000-000000000005">');
$this->assertResponseContains('<input type="password" name="password" required="required"');
$this->assertResponseContains('<input type="password" name="password_confirm" required="required"');
$this->assertResponseContains('<button type="submit">Submit</button>');
$this->assertResponseContains('<button id="btn-submit" type="submit">Submit</button>');

$this->enableSecurityToken();
$this->post('/users/change-password/00000000-0000-0000-0000-000000000005', [
Expand Down
150 changes: 150 additions & 0 deletions webroot/js/pswmeter.js
@@ -0,0 +1,150 @@
/**
* PSWMeter
* @author pascualmj
* @see https://github.com/pascualmj/pswmeter
*/

/**
*
* @param opts
* @returns {{getScore: (function(): number), containerElement: HTMLElement}}
*/
function passwordStrengthMeter(opts) {

// Add styles inside body
const customStyles = document.createElement('style')
document.body.prepend(customStyles)
customStyles.innerHTML = `
${opts.containerElement} {
height: ${opts.height || 4}px;
background-color: #eee;
position: relative;
overflow: hidden;
border-radius: ${opts.borderRadius ? opt.borderRadius.toString() : 2}px;
}
${opts.containerElement} .password-strength-meter-score {
height: inherit;
width: 0%;
transition: .3s ease-in-out;
background: ${opts.colorScore1 || '#ff7700'};
}
${opts.containerElement} .password-strength-meter-score.psms-25 {width: 25%; background: ${opts.colorScore1 || '#ff7700'};}
${opts.containerElement} .password-strength-meter-score.psms-50 {width: 50%; background: ${opts.colorScore2 || '#ffff00'};}
${opts.containerElement} .password-strength-meter-score.psms-75 {width: 75%; background: ${opts.colorScore3 || '#aeff00'};}
${opts.containerElement} .password-strength-meter-score.psms-100 {width: 100%; background: ${opts.colorScore4 || '#00ff00'};}`

// Container Element
const containerElement = document.getElementById(opts.containerElement.slice(1))
containerElement.classList.add('password-strength-meter')

// Score Bar
let scoreBar = document.createElement('div')
scoreBar.classList.add('password-strength-meter-score')

// Append score bar to container element
containerElement.appendChild(scoreBar)

// Password input
const passwordInput = document.getElementById(opts.passwordInput.slice(1))
let passwordInputValue = ''
passwordInput.addEventListener('keyup', function() {
passwordInputValue = this.value
checkPassword()
})

// Chosen Min Length
let pswMinLength = opts.pswMinLength || 8

// Score Message
let scoreMessage = opts.showMessage ? document.getElementById(opts.messageContainer.slice(1)) : null
let messagesList = opts.messagesList === undefined ? ['Empty password', 'Too simple', 'Simple', 'That\'s OK', 'Great password!'] : opts.messagesList
if (scoreMessage) { scoreMessage.textContent = messagesList[0] || 'Empty password'}

// Check Password Function
function checkPassword() {

let score = getScore()
updateScore(score)

}

// Get Score Function
function getScore() {

let score = 0

let regexLower = new RegExp('(?=.*[a-z])')
let regexUpper = new RegExp('(?=.*[A-Z])')
let regexDigits = new RegExp('(?=.*[0-9])')
// For length score print user selection or default value
let regexLength = new RegExp('(?=.{' + pswMinLength + ',})')

if (passwordInputValue.match(regexLower)) { ++score }
if (passwordInputValue.match(regexUpper)) { ++score }
if (passwordInputValue.match(regexDigits)) { ++score }
if (passwordInputValue.match(regexLength)) { ++score }

if (score === 0 && passwordInputValue.length > 0) { ++score }

return score

}

// Show Score Function
function updateScore(score) {
switch(score) {
case 1:
scoreBar.className = 'password-strength-meter-score psms-25'
if (scoreMessage) { scoreMessage.textContent = messagesList[1] || 'Too simple' }
containerElement.dispatchEvent(new Event('onScore1', { bubbles: true }))
break
case 2:
scoreBar.className = 'password-strength-meter-score psms-50'
if (scoreMessage) { scoreMessage.textContent = messagesList[2] || 'Simple' }
containerElement.dispatchEvent(new Event('onScore2', { bubbles: true }))
break
case 3:
scoreBar.className = 'password-strength-meter-score psms-75'
if (scoreMessage) { scoreMessage.textContent = messagesList[3] || 'That\'s OK' }
containerElement.dispatchEvent(new Event('onScore3', { bubbles: true }))
break
case 4:
scoreBar.className = 'password-strength-meter-score psms-100'
if (scoreMessage) { scoreMessage.textContent = messagesList[4] || 'Great password!' }
containerElement.dispatchEvent(new Event('onScore4', { bubbles: true }))
break
default:
scoreBar.className = 'password-strength-meter-score'
if (scoreMessage) { scoreMessage.textContent = messagesList[0] || 'No data' }
containerElement.dispatchEvent(new Event('onScore0', { bubbles: true }))
}
}

// Return anonymous object with properties
return {
containerElement,
getScore
}

}
window.addEventListener("load",init);
function init() {
// Run pswmeter with options
const myPassMeter = passwordStrengthMeter({
containerElement: '#pswmeter',
passwordInput: '#new-password',
showMessage: true,
messageContainer: '#pswmeter-message'
});
for (let i = 0; i < 4; i++) {
myPassMeter.containerElement.addEventListener('onScore' + i, function() {
document.getElementById("btn-submit").disabled = i < requiredScore;
})
}

document.getElementById("new-password").dispatchEvent(new Event("keyup"));
if (myPassMeter.getScore < requiredScore) {
document.getElementById("btn-submit").disabled = true;
}

}

0 comments on commit 61a4887

Please sign in to comment.