}
/**
- * Get the next unused uf_id value, since the standalone UF doesn't
- * have id's (it uses OpenIDs, which go in a different field).
+ * Get the next unused uf_id value
*
* @deprecated
* @return int
class CRM_Standaloneusers_Page_ChangePassword extends CRM_Core_Page {
public function run() {
-
- // Example: Assign a variable for use in a template
- if (!defined('CIVICRM_HIBP_URL')) {
- define('CIVICRM_HIBP_URL', 'https://api.pwnedpasswords.com/range/');
- }
$this->assign('hibp', CIVICRM_HIBP_URL);
$this->assign('loggedInUserID', CRM_Utils_System::getLoggedInUfID());
Civi::service('angularjs.loader')->addModules('crmChangePassword');
--- /dev/null
+<?php
+use CRM_Standaloneusers_ExtensionUtil as E;
+
+use Civi\Standalone\Security;
+
+/**
+ * Provide the send password reset / reset password page.
+ * URL: /civicrm/login/password[?token=xxxx]
+ *
+ * If called with ?token=xxxx then it's the latter.
+ */
+class CRM_Standaloneusers_Page_ResetPassword extends CRM_Core_Page {
+
+ public function run() {
+
+ $this->assign('hibp', CIVICRM_HIBP_URL);
+ // $this->assign('loggedInUserID', CRM_Utils_System::getLoggedInUfID());
+ Civi::service('angularjs.loader')->addModules('crmResetPassword');
+
+ // If we have a password reset token, validate it without 'spending' it.
+ $token = CRM_Utils_Request::retrieveValue('token', 'String',NULL, FALSE, $method = 'GET');
+ if ($token) {
+ if (!Security::singleton()->checkPasswordResetToken($token, FALSE)) {
+ $token = 'invalid';
+ }
+ $this->assign('token', $token);
+ }
+
+ parent::run();
+ }
+
+}
->execute();
$result['success'] = 1;
- $result[] = ['success' => 1];
+ \Civi::log()->info("Changed password for user {userID} via User.PasswordReset", compact('userID'));
}
}
<?php
namespace Civi\Api4\Action\User;
+// @todo
+// URL is (a) just theh path in the emails.
+// clicking button on form with proper token does nothing.
+// should redirect to login on success
+//
+
use Civi;
use Civi\Api4\Generic\Result;
use API_Exception;
$token = static::updateToken($user['id']);
list($domainFromName, $domainFromEmail) = \CRM_Core_BAO_Domain::getNameAndEmail(TRUE);
- $resetUrlPlaintext = \CRM_Utils_System::url('civicrm/password-reset', ['token' => $token], TRUE, NULL, FALSE, TRUE);
+ // xxx this is not generating https://blah - just the path. Why?
+ $resetUrlPlaintext = \CRM_Utils_System::url('civicrm/login/password', ['token' => $token], TRUE, NULL, FALSE);
$resetUrlHtml = htmlspecialchars($resetUrlPlaintext);
// The template_params are used in the template like {$resetUrlHtml} and {$resetUrlHtml}
$params = [
* Permissions are wide on this but are checked in validateValues.
*/
public static function permissions() {
+ $x=1;
return [
'default' => ['access CiviCRM'],
- 'PasswordReset' => ['access password resets'],
- 'SendPasswordreset' => ['access password resets'],
+ 'passwordReset' => ['access password resets'],
+ 'sendPasswordReset' => ['access password resets'],
];
}
angular.module('crmChangePassword', CRM.angRequires('crmChangePassword'));
- // "crmChangePassword" displays the status of a queue
- // Example usage: <div crm-change-password ></div>
- // If "queue" is omitted, then inherit `CRM.vars.crmChangePassword.default`.
angular.module('crmChangePassword').component('crmChangePassword', {
templateUrl: '~/crmChangePassword/crmChangePassword.html',
bindings: {
}
else {
hash = hash.toUpperCase();
- hashPrefix = hash.substring(0, 5);
+ let hashPrefix = hash.substring(0, 5);
return fetch(ctrl.hibp + hashPrefix)
.then(r => r.text())
.then(hibpResult => {
--- /dev/null
+<?php
+return [
+ 'ext' => 'standaloneusers',
+ 'js' => [
+ 'ang/crmResetPassword.js',
+ // 'ang/crmQueueMonitor/*.js',
+ // 'ang/crmQueueMonitor/*/*.js',
+ ],
+ // 'css' => ['ang/crmQueueMonitor.css'],
+ 'partials' => ['ang/crmResetPassword'],
+ 'requires' => ['crmUi', 'crmUtil', 'api4'],
+ 'basePages' => ['civicrm/login/password'],
+ 'exports' => [
+ // Export the module as an [E]lement
+ 'crm-reset-password' => 'E',
+ ],
+];
--- /dev/null
+(function (angular) {
+ "use strict";
+
+ angular.module('crmResetPassword', CRM.angRequires('crmResetPassword'));
+
+ angular.module('crmResetPassword').component('crmResetPassword', {
+ templateUrl: '~/crmResetPassword/crmResetPassword.html',
+ bindings: {
+ // things listed here become properties on the controller using values from attributes.
+ hibp: '@',
+ token: '@'
+ },
+ controller: function($scope, $timeout, crmApi4) {
+ var ts = $scope.ts = CRM.ts(null),
+ ctrl = this;
+
+ console.log('init crmResetPassword component starting');
+ // $onInit gets run after the this controller is called, and after the bindings have been applied.
+ // this.$onInit = function() { console.log('user', ctrl.userId); };
+ ctrl.formSubmitted = false;
+ ctrl.newPassword = '';
+ ctrl.newPasswordAgain = '';
+ ctrl.identifier = '';
+ ctrl.busy = '';
+ ctrl.pwnd = false;
+ ctrl.resetSuccessfullySubmitted = false;
+
+ let updateAngular = (prop, newVal) => {
+ $timeout(() => {
+ console.log("Setting", prop, "to", newVal);
+ ctrl[prop] = newVal;
+ }, 0);
+ };
+ ctrl.sendPasswordReset = () => {
+ updateAngular('busy', ts('Just a moment...'));
+ updateAngular('formSubmitted', true);
+ if (!ctrl.identifier) {
+ alert(ts('Please provide your username/email.'));
+ return;
+ }
+ crmApi4('User', 'SendPasswordReset', { identifier: ctrl.identifier })
+ .then(r => {
+ updateAngular('busy', '');
+ updateAngular('resetSuccessfullySubmitted', true);
+ })
+ .catch(e => {
+ updateAngular('busy', ts('Sorry, something went wrong. Please contact your site administrators.'));
+ });
+ };
+
+ ctrl.attemptChange = () => {
+ updateAngular('busy', '');
+ updateAngular('formSubmitted', true);
+ updateAngular('pwnd', false);
+ if (ctrl.newPassword.length < 8) {
+ alert(ts("Passwords under 8 characters are simply not secure. Ideally you should use a secure password generator."));
+ return;
+ }
+ if (ctrl.newPassword != ctrl.newPasswordAgain) {
+ alert(ts("Passwords do not match"));
+ return;
+ }
+
+ let promises = Promise.resolve(null);
+ if (ctrl.hibp) {
+ promises = promises.then(() => {
+ updateAngular('busy', ts('Checking password is not known to have been involved in data breach...'));
+ return sha1(ctrl.newPassword)
+ .then(hash => {
+ if (!hash.match(/^[a-f0-9]+$/)) {
+ updateAngular('busy', ts('Could not check. Is your browser up-to-date?'));
+ }
+ else {
+ hash = hash.toUpperCase();
+ let hashPrefix = hash.substring(0, 5);
+ return fetch(ctrl.hibp + hashPrefix)
+ .then(r => r.text())
+ .then(hibpResult => {
+ if (hibpResult &&
+ hibpResult.split(/\r\n/).find(line => hashPrefix + line.replace(/:\d+$/, '') === hash)) {
+ // e.g. Password123
+ updateAngular('pwn', true);
+ return;
+ }
+ updateAngular('busy', '');
+ })
+ .catch( () => {
+ updateAngular('busy', ts('Could not perform check; service error.'));
+ });
+ }
+ });
+ });
+ }
+
+ promises = promises.then(() => {
+ updateAngular('busy', ctrl.busy + ts('Changing...'));
+ updateAngular('formSubmitted', true);
+ // Now submit api request.
+ return crmApi4('User', 'PasswordReset', {
+ token: ctrl.token,
+ password: ctrl.newPassword,
+ })
+ .then(r => {
+ updateAngular('busy', ts('Password successfully updated. Redirecting to login...'));
+ $timeout(() => {
+ window.location = '/civicrm/login'
+ ctrl[prop] = newVal;
+ }, 1300);
+ })
+ .catch(e => {
+ updateAngular('token', 'invalid');
+ });
+ });
+ };
+
+ // Generate SHA-1 digest for given text. Returns Promise
+ function sha1(message) {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(message);
+ // const hashBuffer =
+ return crypto.subtle.digest('SHA-1', data)
+ .then(hashBuffer => {
+ const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
+ const hashHex = hashArray
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join(""); // convert bytes to hex string
+ return hashHex;
+ });
+ }
+ }
+ });
+})(angular);
--- /dev/null
+<div>
+ <!-- without a token, we offer for them to generate one. -->
+ <form name="requestLink" crm-ui-id-scope
+ ng-if="!$ctrl.formSubmitted && !$ctrl.token">
+
+ <div crm-ui-field="{name: 'identifier', title: ts('Enter the username or email on your account')}">
+ <input
+ crm-ui-id="identifier"
+ name="identifier"
+ ng-model="$ctrl.identifier"
+ class="crm-form-text"
+ type=text
+ />
+ </div>
+
+ <button ng-click="$ctrl.sendPasswordReset()" >{{ts('Send Password Reset')}}</button>
+ </form>
+ <div ng-if="$ctrl.resetSuccessfullySubmitted" >
+ <p>{{ts('Thanks. If your username/email matched an active account, we will email you with a special link to provide a new password.')}}</p>
+ <p>{{ts('The link must be used within an hour, and can only be used once.')}}</p>
+ </div>
+
+ <div ng-if="$ctrl.busy" >{{$ctrl.busy}}</div>
+
+ <!-- without a token, we offer for them to generate one. -->
+ <form ng-if="$ctrl.token && $ctrl.token !== 'invalid' && !$ctrl.formSubmitted" name="resetPassword" crm-ui-id-scope >
+ <div ng=if="$ctrl.token !== 'invalid'" >
+ <div crm-ui-field="{name: 'newPassword', title: ts('Enter a new password')}">
+ <input
+ crm-ui-id="newPassword"
+ name="newPassword"
+ ng-model="$ctrl.newPassword"
+ class="crm-form-text"
+ type=password
+ />
+ </div>
+
+ <div crm-ui-field="{name: 'newPasswordAgain', title: ts('Re-enter new password')}">
+ <input
+ crm-ui-id="newPasswordAgain"
+ name="newPasswordAgain"
+ ng-model="$ctrl.newPasswordAgain"
+ class="crm-form-text"
+ type=password
+ />
+ <span class="crm-error" ng-show="$ctrl.newPasswordAgain && $ctrl.newPassword && $ctrl.newPassword !== $ctrl.newPasswordAgain">
+ {{ts('Passwords do not match')}}
+ </span>
+ </div>
+
+ <button ng-click="$ctrl.attemptChange()" ng-disabled="$ctrl.formSubmitted">{{ts('Change Password')}}</button>
+ </div>
+ </form>
+
+ <div ng-if="$ctrl.token === 'invalid'" >
+ <p>{{ts("This password reset link has expired or is otherwise invalid.")}}</p>
+ <p><a href ng-click="$ctrl.token=''" >{{ts('Send new password reset link')}}</a></p>
+ </div>
+
+</div>
+
<?php
+// Define default URL to haveibeenpwned service. Set this empty in settings to disable.
+if (!defined('CIVICRM_HIBP_URL')) {
+ define('CIVICRM_HIBP_URL', 'https://api.pwnedpasswords.com/range/');
+}
+
require_once 'standaloneusers.civix.php';
// phpcs:disable
use CRM_Standaloneusers_ExtensionUtil as E;
<div class="message warning" style="display:none;" id="anonAccessDenied">{ts}You may need to login to access that.{/ts}</div>
<form>
<div>
- <label for="usernameInput" class="form-label">Username</label>
- <input type="password" class="form-control" id="usernameInput" >
+ <label for="usernameInput" name=username class="form-label">Username</label>
+ <input type="text" class="form-control" id="usernameInput" >
</div>
<div>
<label for="passwordInput" class="form-label">Password</label>
--- /dev/null
+<crm-angular-js modules="crmResetPassword">
+ <crm-reset-password
+ hibp="{$hibp|escape}"
+ token="{$token|escape}" ></crm-reset-password>
+</crm-angular-js>
<title>Change Password</title>
<access_arguments>access CiviCRM</access_arguments>
</item>
+ <item>
+ <path>civicrm/login/password</path>
+ <page_callback>CRM_Standaloneusers_Page_ResetPassword</page_callback>
+ <title>Reset Password</title>
+ <access_arguments>access password resets</access_arguments>
+ </item>
</menu>
'view event info',
'register for events',
'access password resets',
+ 'authenticate with password', //xxx?
],
],
[
$params = [
'cms_name' => $e->getModel()->extras['adminUser'],
'cms_pass' => $e->getModel()->extras['adminPass'],
- 'email' => $adminEmail,
+ 'email' => $adminEmail,
'notify' => FALSE,
- 'contactID' => $contactID,
+ 'contact_id' => $contactID,
];
$userID = \CRM_Core_BAO_CMSUser::create($params, 'email');