Skip to content

Commit 421b078

Browse files
committed
Document + add config for current_password
1 parent 6c50e6a commit 421b078

File tree

4 files changed

+123
-11
lines changed

4 files changed

+123
-11
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ protected function executeSingle(array $data): array
2525
{
2626
// We do not really update the password in the context of the demo
2727
// auth()->user()->update([
28-
// 'password' => $data['password'],
28+
// 'password' => $data['new_password'],
2929
// ]);
3030

3131
$this->notify('Password updated!');

docs/guide/authentication.md

Lines changed: 56 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,62 @@ 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 trat 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)`: enable password confirmation (false by default)
323+
- `configurePasswordRule(Password $rule)`: change the default password validation rule (default: `Password::min(8)`)
324+
- `configureValidateCurrentPassword(?bool $validate = true)`: 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 your `User` model stores a hashed password as usual. (true by default)
325+
326+
### 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+
309362
## User impersonation (dev only)
310363

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:
364+
At the development stage, it can be useful to replace the login form by a user impersonation. Sharp allows doing that out of the box:
312365

313366
```php
314367
class SharpServiceProvider extends SharpAppServiceProvider
@@ -392,3 +445,4 @@ class SharpServiceProvider extends SharpAppServiceProvider
392445
}
393446
}
394447
```
448+

src/Auth/Password/Command/IsChangePasswordCommandTrait.php

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
trait IsChangePasswordCommandTrait
1212
{
1313
private bool $confirmPassword = false;
14+
private bool $validateCurrentPassword = true;
1415
private ?Password $passwordRule = null;
1516

1617
public function label(): ?string
@@ -21,10 +22,13 @@ public function label(): ?string
2122
public function buildFormFields(FieldsContainer $formFields): void
2223
{
2324
$formFields
24-
->addField(
25-
SharpFormTextField::make('password')
26-
->setLabel(trans('sharp::auth.password_change.command.fields.current_password'))
27-
->setInputTypePassword()
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+
)
2832
)
2933
->addField(
3034
SharpFormTextField::make('new_password')
@@ -47,10 +51,9 @@ public function rules(): array
4751
'sharp-password-change-'.auth()->id(),
4852
3,
4953
fn () => [
50-
'password' => [
51-
'required',
52-
'current_password',
53-
],
54+
...$this->validateCurrentPassword
55+
? ['password' => ['required', 'current_password']]
56+
: [],
5457
'new_password' => [
5558
'required',
5659
'string',
@@ -80,6 +83,13 @@ protected function configureConfirmPassword(?bool $confirmPassword = true): self
8083
return $this;
8184
}
8285

86+
protected function configureValidateCurrentPassword(?bool $validateCurrentPassword = true): self
87+
{
88+
$this->validateCurrentPassword = $validateCurrentPassword;
89+
90+
return $this;
91+
}
92+
8393
protected function configurePasswordRule(Password $passwordRule): self
8494
{
8595
$this->passwordRule = $passwordRule;

tests/Http/Auth/ChangePasswordCommandTraitTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,54 @@ protected function executeSingle(array $data): array
131131
->assertOk();
132132
});
133133

134+
it('allows to hide the current password field', function () {
135+
fakeShowFor(SinglePersonEntity::class, new class() extends SinglePersonShow
136+
{
137+
public function getInstanceCommands(): ?array
138+
{
139+
return [
140+
'change_password_confirm' => new class() extends SingleInstanceCommand
141+
{
142+
use IsChangePasswordCommandTrait;
143+
144+
public function buildCommandConfig(): void
145+
{
146+
$this->configureValidateCurrentPassword(false);
147+
}
148+
149+
protected function executeSingle(array $data): array
150+
{
151+
return $this->reload();
152+
}
153+
},
154+
];
155+
}
156+
});
157+
158+
// Form does not contain the current password field
159+
$this
160+
->getJson(route('code16.sharp.api.show.command.singleInstance.form', [
161+
'entityKey' => 'single-person',
162+
'commandKey' => 'change_password_confirm',
163+
]))
164+
->assertOk()
165+
->assertJson(function (Assert $json) {
166+
$json
167+
->missing('fields.password')
168+
->where('fields.new_password.key', 'new_password')
169+
->etc();
170+
});
171+
172+
// Succeeds with valid data
173+
$this
174+
->postJson(route('code16.sharp.api.show.command.instance', ['single-person', 'change_password_confirm']), [
175+
'data' => [
176+
'new_password' => 'Password1!',
177+
],
178+
])
179+
->assertOk();
180+
});
181+
134182
it('rate limits after too many attempts and returns a helpful message', function () {
135183
fakeShowFor(SinglePersonEntity::class, new class() extends SinglePersonShow
136184
{

0 commit comments

Comments
 (0)