Skip to content

Commit 5b1dd95

Browse files
authored
Merge pull request #655 from code16/password-change
Password change
2 parents 8ba63a9 + 0670504 commit 5b1dd95

File tree

8 files changed

+447
-65
lines changed

8 files changed

+447
-65
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App\Sharp\Profile\Commands;
4+
5+
use Code16\Sharp\Auth\Password\Command\IsChangePasswordCommandTrait;
6+
use Code16\Sharp\EntityList\Commands\SingleInstanceCommand;
7+
use Illuminate\Validation\Rules\Password;
8+
9+
class ChangePasswordCommand extends SingleInstanceCommand
10+
{
11+
use IsChangePasswordCommandTrait;
12+
13+
public function buildCommandConfig(): void
14+
{
15+
$this->configureConfirmPassword()
16+
->configurePasswordRule(
17+
Password::min(8)
18+
->numbers()
19+
->symbols()
20+
->uncompromised()
21+
);
22+
}
23+
24+
protected function executeSingle(array $data): array
25+
{
26+
// We do not really update the password in the context of the demo
27+
// auth()->user()->update([
28+
// 'password' => $data['new_password'],
29+
// ]);
30+
31+
$this->notify('Password updated!');
32+
33+
return $this->reload();
34+
}
35+
}

demo/app/Sharp/Profile/Commands/UpdateProfilePasswordCommand.php

Lines changed: 0 additions & 57 deletions
This file was deleted.

demo/app/Sharp/Profile/ProfileSingleShow.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
namespace App\Sharp\Profile;
44

55
use App\Sharp\Profile\Commands\Activate2faCommand;
6+
use App\Sharp\Profile\Commands\ChangePasswordCommand;
67
use App\Sharp\Profile\Commands\Deactivate2faCommand;
7-
use App\Sharp\Profile\Commands\UpdateProfilePasswordCommand;
88
use Code16\Sharp\Show\Fields\SharpShowPictureField;
99
use Code16\Sharp\Show\Fields\SharpShowTextField;
1010
use Code16\Sharp\Show\Layout\ShowLayout;
@@ -46,12 +46,12 @@ public function buildShowConfig(): void
4646

4747
public function getInstanceCommands(): ?array
4848
{
49-
return array_merge(
50-
[UpdateProfilePasswordCommand::class],
51-
config('sharp.auth.2fa.handler') === 'totp'
49+
return [
50+
ChangePasswordCommand::class,
51+
...sharp()->config()->get('auth.2fa.handler') === 'totp'
5252
? [Activate2faCommand::class, Deactivate2faCommand::class]
53-
: []
54-
);
53+
: [],
54+
];
5555
}
5656

5757
public function findSingle(): array

docs/guide/authentication.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ class My2faNotificationHandler extends Sharp2faNotificationHandler // or Sharp2f
243243

244244
## Forgotten password
245245

246-
You can activate the classic Laravel Breeze workflow of forgotten password with a simple config:
246+
You can activate the classic Laravel workflow of forgotten password with a simple config:
247247

248248
```php
249249
class SharpServiceProvider extends SharpAppServiceProvider
@@ -306,9 +306,66 @@ class SharpServiceProvider extends SharpAppServiceProvider
306306

307307
These customizations will not interfere with any default behavior that you may have implemented for your app, outside Sharp.
308308

309+
## Allow the current user to change his password
310+
311+
Sharp provides a helper trait to quickly build a command that lets the currently authenticated user change his password: `Code16\Sharp\Auth\Password\Command\IsChangePasswordCommandTrait`. Using this trait, you can quickly build a Sharp command, with a few configuration options.
312+
313+
The trait will take care of the form, validation and rate-limiting. Note that:
314+
315+
- This helper is designed for the “current user changes his own password” scenario. If you need admin-managed password resets for other users, implement a different command with the proper authorization checks.
316+
- Persisting the new password is up to you (see example below).
317+
318+
### Configuration and behavior
319+
320+
You can configure the behavior of the command with the following methods (should be called in your `buildCommandConfig()` method):
321+
322+
- `configureConfirmPassword(?bool $confirm = true)`: (false by default) enable password confirmation.
323+
- `configurePasswordRule(Password $rule)`: (default: `Password::min(8)`) change the default password validation rule.
324+
- `configureValidateCurrentPassword(?bool $validate = true)`: (true by default) if true, a `password` field that uses Laravel’s `current_password` rule (which compares against the currently authenticated user’s stored password) is added. Make sure you use Eloquent, and that your `User` model stores a hashed password as usual.
325+
326+
### Full example
327+
328+
```php
329+
use Code16\Sharp\Auth\Password\Command\IsChangePasswordCommandTrait;
330+
// ...
331+
332+
class ChangePasswordCommand extends SingleInstanceCommand
333+
{
334+
use IsChangePasswordCommandTrait;
335+
336+
public function buildCommandConfig(): void
337+
{
338+
$this->configureConfirmPassword()
339+
->configurePasswordRule(
340+
Password::min(8)
341+
->numbers()
342+
->symbols()
343+
->uncompromised()
344+
);
345+
}
346+
347+
protected function executeSingle(array $data): array
348+
{
349+
// The trait handles validation and rate limiting.
350+
351+
auth()->user()->update([
352+
'password' => $data['new_password'], // Considering hashing is done by the model (cast)
353+
]);
354+
355+
$this->notify('Password updated!');
356+
357+
return $this->reload();
358+
}
359+
}
360+
```
361+
362+
::: info
363+
In this example we chose to create a `SingleInstanceCommand`, since it’s a common use-case to attach such a command to a "Profile" single Show Page that could be [placed in the user menu](building-menu.md#add-links-in-the-user-profile-menu), but you can decide to create an `EntityCommand` or even an `InstanceCommand` instead.
364+
:::
365+
309366
## User impersonation (dev only)
310367

311-
At the development stage, it can be useful to replace the login form by a user impersonation. Sharp allows to do that out of the box:
368+
At the development stage, it can be useful to replace the login form by a user impersonation. Sharp allows you to do that out of the box:
312369

313370
```php
314371
class SharpServiceProvider extends SharpAppServiceProvider
@@ -392,3 +449,4 @@ class SharpServiceProvider extends SharpAppServiceProvider
392449
}
393450
}
394451
```
452+

resources/lang/en/auth.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,15 @@
2828
],
2929
],
3030
],
31+
'password_change' => [
32+
'command' => [
33+
'label' => 'Change password...',
34+
'fields' => [
35+
'current_password' => 'Current password',
36+
'new_password' => 'New password',
37+
'new_password_confirm' => 'Confirm new password',
38+
],
39+
'rate_limit_exceeded' => 'You have made too many attempts. Please try again in :seconds seconds.',
40+
],
41+
],
3142
];

resources/lang/fr/auth.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,15 @@
2828
],
2929
],
3030
],
31+
'password_change' => [
32+
'command' => [
33+
'label' => 'Modifier le mot de passe...',
34+
'fields' => [
35+
'current_password' => 'Mot de passe actuel',
36+
'new_password' => 'Nouveau mot de passe',
37+
'new_password_confirm' => 'Confirmer le nouveau mot de passe',
38+
],
39+
'rate_limit_exceeded' => 'Vous avez effectué trop de tentatives. Veuillez réessayer dans :seconds secondes.',
40+
],
41+
],
3142
];
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
namespace Code16\Sharp\Auth\Password\Command;
4+
5+
use Code16\Sharp\Exceptions\Form\SharpApplicativeException;
6+
use Code16\Sharp\Form\Fields\SharpFormTextField;
7+
use Code16\Sharp\Utils\Fields\FieldsContainer;
8+
use Illuminate\Support\Facades\RateLimiter;
9+
use Illuminate\Validation\Rules\Password;
10+
11+
trait IsChangePasswordCommandTrait
12+
{
13+
private bool $confirmPassword = false;
14+
private bool $validateCurrentPassword = true;
15+
private ?Password $passwordRule = null;
16+
17+
public function label(): ?string
18+
{
19+
return trans('sharp::auth.password_change.command.label');
20+
}
21+
22+
public function buildFormFields(FieldsContainer $formFields): void
23+
{
24+
$formFields
25+
->when(
26+
$this->validateCurrentPassword,
27+
fn (FieldsContainer $formFields) => $formFields->addField(
28+
SharpFormTextField::make('password')
29+
->setLabel(trans('sharp::auth.password_change.command.fields.current_password'))
30+
->setInputTypePassword()
31+
)
32+
)
33+
->addField(
34+
SharpFormTextField::make('new_password')
35+
->setLabel(trans('sharp::auth.password_change.command.fields.new_password'))
36+
->setInputTypePassword()
37+
)
38+
->when(
39+
$this->confirmPassword,
40+
fn (FieldsContainer $formFields) => $formFields->addField(
41+
SharpFormTextField::make('new_password_confirmation')
42+
->setLabel(trans('sharp::auth.password_change.command.fields.new_password_confirm'))
43+
->setInputTypePassword()
44+
)
45+
);
46+
}
47+
48+
public function rules(): array
49+
{
50+
$rules = RateLimiter::attempt(
51+
'sharp-password-change-'.auth()->id(),
52+
3,
53+
fn () => [
54+
...$this->validateCurrentPassword
55+
? ['password' => ['required', 'current_password']]
56+
: [],
57+
'new_password' => [
58+
'required',
59+
'string',
60+
$this->passwordRule ?? Password::min(8),
61+
...$this->confirmPassword ? ['confirmed'] : [],
62+
],
63+
],
64+
);
65+
66+
if (! $rules) {
67+
throw new SharpApplicativeException(
68+
trans(
69+
'sharp::auth.password_change.command.rate_limit_exceeded', [
70+
'seconds' => RateLimiter::availableIn('sharp-password-change-'.auth()->id()),
71+
]
72+
)
73+
);
74+
}
75+
76+
return $rules;
77+
}
78+
79+
protected function configureConfirmPassword(?bool $confirmPassword = true): self
80+
{
81+
$this->confirmPassword = $confirmPassword;
82+
83+
return $this;
84+
}
85+
86+
protected function configureValidateCurrentPassword(?bool $validateCurrentPassword = true): self
87+
{
88+
$this->validateCurrentPassword = $validateCurrentPassword;
89+
90+
return $this;
91+
}
92+
93+
protected function configurePasswordRule(Password $passwordRule): self
94+
{
95+
$this->passwordRule = $passwordRule;
96+
97+
return $this;
98+
}
99+
}

0 commit comments

Comments
 (0)