Skip to content

Commit 45eaa69

Browse files
committed
admin password reset implemented for #381
1 parent 99738f3 commit 45eaa69

11 files changed

Lines changed: 269 additions & 33 deletions

File tree

public/index.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@
141141
else
142142
return $controller->formUserView($user_id);
143143
});
144+
145+
// User edit password form
146+
$app->get('/forms/users/u/:user_id/password/?', function ($user_id) use ($app) {
147+
$controller = new UF\UserController($app);
148+
$get = $app->request->get();
149+
return $controller->formUserEditPassword($user_id);
150+
});
144151

145152
// User creation form
146153
$app->get('/forms/users/?', function () use ($app) {

public/js/userfrosting.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,14 @@ jQuery.validator.setDefaults({
7777
});
7878

7979
// Process a UserFrosting form, displaying messages from the message stream and executing specified callbacks
80-
function ufFormSubmit(formElement, validators, msgElement, successCallback, msgCallback) {
80+
function ufFormSubmit(formElement, validators, msgElement, successCallback, msgCallback, beforeSubmitCallback) {
8181
formElement.validate({
8282
rules: validators['rules'],
8383
messages : validators['messages'],
8484
submitHandler: function (f, event) {
85+
// Execute any "before submit" callback
86+
if (typeof beforeSubmitCallback !== "undefined")
87+
beforeSubmitCallback();
8588
var form = $(f);
8689
// Set "loading" text for submit button, if it exists, and disable button
8790
var submit_button = form.find("button[type=submit]");
@@ -93,7 +96,7 @@ function ufFormSubmit(formElement, validators, msgElement, successCallback, msgC
9396
// Serialize and post to the backend script in ajax mode
9497
var serializedData = form.find('input, textarea, select').not(':checkbox').serialize();
9598
// Get unchecked checkbox values, set them to 0
96-
form.find('input[type=checkbox]').each(function() {
99+
form.find('input[type=checkbox]:enabled').each(function() {
97100
if ($(this).is(':checked'))
98101
serializedData += "&" + encodeURIComponent(this.name) + "=1";
99102
else

public/js/widget-users.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ function bindUserTableButtons(table) {
2121
userForm('dialog-user-edit', user_id);
2222
});
2323

24+
$(table).find('.js-user-password').click(function() {
25+
var btn = $(this);
26+
var user_id = btn.data('id');
27+
userPasswordForm('dialog-user-password', user_id);
28+
});
29+
2430
$(table).find('.js-user-activate').click(function() {
2531
var btn = $(this);
2632
var user_id = btn.data('id');
@@ -242,6 +248,72 @@ function userForm(box_id, user_id) {
242248
});
243249
}
244250

251+
/**
252+
* Display a modal form for changing a user's password.
253+
*/
254+
function userPasswordForm(box_id, user_id) {
255+
user_id = typeof user_id !== 'undefined' ? user_id : "";
256+
257+
// Delete any existing instance of the form with the same name
258+
if($('#' + box_id).length ) {
259+
$('#' + box_id).remove();
260+
}
261+
262+
var url = site['uri']['public'] + "/forms/users/u/" + user_id + "/password";
263+
264+
// Fetch and render the form
265+
$.ajax({
266+
type: "GET",
267+
url: url,
268+
data: {
269+
box_id: box_id
270+
},
271+
cache: false
272+
})
273+
.fail(function(result) {
274+
// Display errors on failure
275+
$('#userfrosting-alerts').flashAlerts().done(function() {
276+
});
277+
})
278+
.done(function(result) {
279+
// Append the form as a modal dialog to the body
280+
$( "body" ).append(result);
281+
$('#' + box_id).modal('show');
282+
283+
// Enable/disable password fields when switch is toggled
284+
$(".controls-password").find("input[type='password']").prop('disabled', true);
285+
$('#' + box_id).find("input[name='change_password_mode']").click(function() {
286+
var type = $(this).val();
287+
if (type == "link") {
288+
$(".controls-password").find("input[type='password']").prop('disabled', true);
289+
$('#' + box_id).find("input[name='flag_password_reset']").prop('disabled', false);
290+
} else {
291+
$(".controls-password").find("input[type='password']").prop('disabled', false);
292+
$('#' + box_id).find("input[name='flag_password_reset']").prop('disabled', true);
293+
}
294+
});
295+
296+
// Link submission buttons
297+
ufFormSubmit(
298+
$('#' + box_id).find("form"),
299+
validators,
300+
$("#form-alerts"),
301+
function(data, statusText, jqXHR) {
302+
// Reload the page on success
303+
window.location.reload(true);
304+
},
305+
function() {
306+
// Enable radio buttons after submit
307+
$('#' + box_id).find("input[name='change_password_mode']").prop('disabled', false);
308+
},
309+
function() {
310+
// Disable radio buttons before submit
311+
$('#' + box_id).find("input[name='change_password_mode']").prop('disabled', true);
312+
}
313+
);
314+
});
315+
}
316+
245317
// Display user info in a panel
246318
function userDisplay(box_id, user_id) {
247319
user_id = typeof user_id !== 'undefined' ? user_id : "";

userfrosting/controllers/AccountController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,7 @@ public function accountSettings(){
916916
$this->_app->halt(400);
917917
}
918918

919-
// If a new password was specified, hash it
919+
// If a new password was specified, hash it.
920920
if (isset($data['password']))
921921
$data['password'] = Authentication::hashPassword($data['password']);
922922

userfrosting/controllers/UserController.php

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,47 @@ public function formUserEdit($user_id){
345345
]);
346346
}
347347

348+
/**
349+
* Renders the form for editing a user's password.
350+
*
351+
* This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
352+
* This page requires authentication.
353+
* Request type: GET
354+
*/
355+
public function formUserEditPassword($user_id){
356+
// Get the user to edit
357+
$target_user = User::find($user_id);
358+
359+
// Access-controlled resource
360+
if (!$this->_app->user->checkAccess('uri_users') && !$this->_app->user->checkAccess('uri_group_users', ['primary_group_id' => $target_user->primary_group_id])){
361+
$this->_app->notFound();
362+
}
363+
364+
$get = $this->_app->request->get();
365+
366+
// Determine authorized fields
367+
$hidden_fields = [];
368+
369+
if (!$this->_app->user->checkAccess("update_account_setting", ["user" => $target_user, "property" => 'password']))
370+
$hidden_fields[] = 'password';
371+
372+
// Load validator rules
373+
$schema = new \Fortress\RequestSchema($this->_app->config('schema.path') . "/forms/user-update.json");
374+
$this->_app->jsValidator->setSchema($schema);
375+
376+
// This form posts to the same resource as "update user"
377+
$this->_app->render("components/common/user-set-password.twig", [
378+
"box_id" => isset($get['box_id']) ? $get['box_id'] : 'user-set-password',
379+
"box_title" => "Change User Password",
380+
"form_action" => $this->_app->site->uri['public'] . "/users/u/$user_id",
381+
"target_user" => $target_user,
382+
"fields" => [
383+
"hidden" => $hidden_fields
384+
],
385+
"validators" => $this->_app->jsValidator->rules()
386+
]);
387+
}
388+
348389
/**
349390
* Processes the request to create a new user (from the admin controls).
350391
*
@@ -497,7 +538,7 @@ public function updateUser($user_id){
497538
$ms = $this->_app->alerts;
498539

499540
// Get the target user
500-
$target_user = UserLoader::fetch($user_id);
541+
$target_user = User::find($user_id);
501542

502543
// Get the target user's groups
503544
$groups = $target_user->getGroups();
@@ -515,10 +556,17 @@ public function updateUser($user_id){
515556
$ms->addMessageTranslated("danger", "ACCESS_DENIED");
516557
$this->_app->halt(403);
517558
}
518-
559+
519560
// Remove csrf_token
520561
unset($post['csrf_token']);
521-
562+
563+
// Set up Fortress to process the request
564+
$rf = new \Fortress\HTTPRequestFortress($ms, $requestSchema, $post);
565+
566+
if (isset($post['passwordc'])){
567+
unset($post['passwordc']);
568+
}
569+
522570
// Check authorization for submitted fields, if the value has been changed
523571
foreach ($post as $name => $value) {
524572
if ($name == "groups" || (isset($target_user->$name) && $post[$name] != $target_user->$name)){
@@ -532,29 +580,30 @@ public function updateUser($user_id){
532580
$this->_app->halt(400);
533581
}
534582
}
535-
583+
536584
// Check that we are not disabling the master account
537585
if (($target_user->id == $this->_app->config('user_id_master')) && isset($post['flag_enabled']) && $post['flag_enabled'] == "0"){
538586
$ms->addMessageTranslated("danger", "ACCOUNT_DISABLE_MASTER");
539587
$this->_app->halt(403);
540588
}
541-
589+
590+
// Check that the email address is not in use
542591
if (isset($post['email']) && $post['email'] != $target_user->email && UserLoader::exists($post['email'], 'email')){
543592
$ms->addMessageTranslated("danger", "ACCOUNT_EMAIL_IN_USE", $post);
544593
$this->_app->halt(400);
545594
}
546595

547-
// Set up Fortress to process the request
548-
$rf = new \Fortress\HTTPRequestFortress($ms, $requestSchema, $post);
549-
550596
// Sanitize
551597
$rf->sanitize();
552598

553599
// Validate, and halt on validation errors.
554600
if (!$rf->validate()) {
555601
$this->_app->halt(400);
556602
}
557-
603+
604+
// Remove passwordc
605+
$rf->removeFields(['passwordc']);
606+
558607
// Get the filtered data
559608
$data = $rf->data();
560609

@@ -570,6 +619,11 @@ public function updateUser($user_id){
570619
unset($data['groups']);
571620
}
572621

622+
// Hash password
623+
if (isset($data['password'])){
624+
$data['password'] = Authentication::hashPassword($data['password']);
625+
}
626+
573627
// Update the user and generate success messages
574628
foreach ($data as $name => $value){
575629
if ($value != $target_user->$name){
@@ -587,9 +641,39 @@ public function updateUser($user_id){
587641
}
588642
}
589643

590-
$ms->addMessageTranslated("success", "ACCOUNT_DETAILS_UPDATED", ["user_name" => $target_user->user_name]);
591-
$target_user->store();
644+
// If we're generating a password reset, create the corresponding event and shoot off an email
645+
if (isset($data['flag_password_reset']) && ($data['flag_password_reset'] == "1")){
646+
// Recheck auth
647+
if (!$this->_app->user->checkAccess('update_account_setting', ['user' => $target_user, 'property' => 'flag_password_reset'])){
648+
$ms->addMessageTranslated("danger", "ACCESS_DENIED");
649+
$this->_app->halt(403);
650+
}
651+
// New password reset event - bypass any rate limiting
652+
$target_user->newEventPasswordReset();
653+
$target_user->save();
654+
// Email the user asking to confirm this change password request
655+
$twig = $this->_app->view()->getEnvironment();
656+
$template = $twig->loadTemplate("mail/password-reset.twig");
657+
$notification = new Notification($template);
658+
$notification->fromWebsite(); // Automatically sets sender and reply-to
659+
$notification->addEmailRecipient($target_user->email, $target_user->display_name, [
660+
"user" => $target_user,
661+
"request_date" => date("Y-m-d H:i:s")
662+
]);
663+
664+
try {
665+
$notification->send();
666+
} catch (\Exception\phpmailerException $e){
667+
$ms->addMessageTranslated("danger", "MAIL_ERROR");
668+
error_log('Mailer Error: ' . $e->errorMessage());
669+
$this->_app->halt(500);
670+
}
671+
672+
$ms->addMessageTranslated("success", "FORGOTPASS_REQUEST_SENT", ["user_name" => $target_user->user_name]);
673+
}
592674

675+
$ms->addMessageTranslated("success", "ACCOUNT_DETAILS_UPDATED", ["user_name" => $target_user->user_name]);
676+
$target_user->save();
593677
}
594678

595679
/**

userfrosting/locale/en_US.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,13 @@
123123

124124
// Forgot Password
125125
$lang = array_merge($lang,array(
126-
"FORGOTPASS_INVALID_TOKEN" => "Your activation token is not valid",
126+
"FORGOTPASS_INVALID_TOKEN" => "Your secret token is not valid",
127127
"FORGOTPASS_OLD_TOKEN" => "Token past expiration time",
128128
"FORGOTPASS_COULD_NOT_UPDATE" => "Couldn't update password",
129-
"FORGOTPASS_NEW_PASS_EMAIL" => "We have emailed you a new password",
130129
"FORGOTPASS_REQUEST_CANNED" => "Lost password request cancelled",
131130
"FORGOTPASS_REQUEST_EXISTS" => "There is already an outstanding lost password request on this account",
132-
"FORGOTPASS_REQUEST_SUCCESS" => "We have emailed you instructions on how to regain access to your account"
131+
"FORGOTPASS_REQUEST_SENT" => "A password reset link has been emailed to the address on file for user '{{user_name}}'",
132+
"FORGOTPASS_REQUEST_SUCCESS" => "We have emailed you instructions on how to regain access to your account"
133133
));
134134

135135
// Mail

userfrosting/schema/forms/user-update.json

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,34 @@
4646
"message" : "VALIDATE_INTEGER"
4747
}
4848
}
49+
},
50+
"password" : {
51+
"validators" : {
52+
"length" : {
53+
"min" : 8,
54+
"max" : 50,
55+
"message" : "ACCOUNT_PASS_CHAR_LIMIT"
56+
}
57+
},
58+
"sanitizers" : {
59+
"raw" : {}
60+
}
61+
},
62+
"passwordc" : {
63+
"validators" : {
64+
"matches" : {
65+
"field" : "password",
66+
"message" : "ACCOUNT_PASS_MISMATCH"
67+
},
68+
"length" : {
69+
"min" : 8,
70+
"max" : 50,
71+
"message" : "ACCOUNT_PASS_CHAR_LIMIT"
72+
}
73+
},
74+
"sanitizers" : {
75+
"raw" : {}
76+
}
4977
},
5078
"groups" : {
5179
"validators" : {
@@ -73,5 +101,15 @@
73101
"message" : "VALIDATE_BOOLEAN"
74102
}
75103
}
76-
}
104+
},
105+
"flag_password_reset" : {
106+
"validators" : {
107+
"member_of" : {
108+
"values" : [
109+
"0", "1"
110+
],
111+
"message" : "VALIDATE_BOOLEAN"
112+
}
113+
}
114+
}
77115
}

userfrosting/templates/themes/default/components/common/js-snippets/user-table-columns.twig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@
7272
<i class="fa fa-edit"></i> Edit user
7373
</a>
7474
</li>
75+
<li>
76+
<a href="#" data-id="{{user.id}}" class="js-user-password" data-target="#dialog-user-password" data-toggle="modal">
77+
<i class="fa fa-key"></i> Change password
78+
</a>
79+
</li>
7580
<li>
7681
{{#ifCond user.flag_enabled 1 }}
7782
<a href="#" data-id="{{user.id}}" class="js-user-disable">

0 commit comments

Comments
 (0)