Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions demo/app/Sharp/Profile/Commands/ChangePasswordCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Sharp\Profile\Commands;

use Code16\Sharp\Auth\Password\Command\IsChangePasswordCommandTrait;
use Code16\Sharp\EntityList\Commands\SingleInstanceCommand;
use Illuminate\Validation\Rules\Password;

class ChangePasswordCommand extends SingleInstanceCommand
{
use IsChangePasswordCommandTrait;

public function buildCommandConfig(): void
{
$this->configureConfirmPassword()
->configurePasswordRule(
Password::min(8)
->numbers()
->symbols()
->uncompromised()
);
}

protected function executeSingle(array $data): array
{
// We do not really update the password in the context of the demo
// auth()->user()->update([
// 'password' => $data['new_password'],
// ]);

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

return $this->reload();
}
}
57 changes: 0 additions & 57 deletions demo/app/Sharp/Profile/Commands/UpdateProfilePasswordCommand.php

This file was deleted.

12 changes: 6 additions & 6 deletions demo/app/Sharp/Profile/ProfileSingleShow.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace App\Sharp\Profile;

use App\Sharp\Profile\Commands\Activate2faCommand;
use App\Sharp\Profile\Commands\ChangePasswordCommand;
use App\Sharp\Profile\Commands\Deactivate2faCommand;
use App\Sharp\Profile\Commands\UpdateProfilePasswordCommand;
use Code16\Sharp\Show\Fields\SharpShowPictureField;
use Code16\Sharp\Show\Fields\SharpShowTextField;
use Code16\Sharp\Show\Layout\ShowLayout;
Expand Down Expand Up @@ -46,12 +46,12 @@ public function buildShowConfig(): void

public function getInstanceCommands(): ?array
{
return array_merge(
[UpdateProfilePasswordCommand::class],
config('sharp.auth.2fa.handler') === 'totp'
return [
ChangePasswordCommand::class,
...sharp()->config()->get('auth.2fa.handler') === 'totp'
? [Activate2faCommand::class, Deactivate2faCommand::class]
: []
);
: [],
];
}

public function findSingle(): array
Expand Down
62 changes: 60 additions & 2 deletions docs/guide/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ class My2faNotificationHandler extends Sharp2faNotificationHandler // or Sharp2f

## Forgotten password

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

```php
class SharpServiceProvider extends SharpAppServiceProvider
Expand Down Expand Up @@ -306,9 +306,66 @@ class SharpServiceProvider extends SharpAppServiceProvider

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

## Allow the current user to change his password

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.

The trait will take care of the form, validation and rate-limiting. Note that:

- 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.
- Persisting the new password is up to you (see example below).

### Configuration and behavior

You can configure the behavior of the command with the following methods (should be called in your `buildCommandConfig()` method):

- `configureConfirmPassword(?bool $confirm = true)`: (false by default) enable password confirmation.
- `configurePasswordRule(Password $rule)`: (default: `Password::min(8)`) change the default password validation rule.
- `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.

### Full example

```php
use Code16\Sharp\Auth\Password\Command\IsChangePasswordCommandTrait;
// ...

class ChangePasswordCommand extends SingleInstanceCommand
{
use IsChangePasswordCommandTrait;

public function buildCommandConfig(): void
{
$this->configureConfirmPassword()
->configurePasswordRule(
Password::min(8)
->numbers()
->symbols()
->uncompromised()
);
}

protected function executeSingle(array $data): array
{
// The trait handles validation and rate limiting.

auth()->user()->update([
'password' => $data['new_password'], // Considering hashing is done by the model (cast)
]);

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

return $this->reload();
}
}
```

::: info
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.
:::

## User impersonation (dev only)

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:
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:

```php
class SharpServiceProvider extends SharpAppServiceProvider
Expand Down Expand Up @@ -392,3 +449,4 @@ class SharpServiceProvider extends SharpAppServiceProvider
}
}
```

11 changes: 11 additions & 0 deletions resources/lang/en/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,15 @@
],
],
],
'password_change' => [
'command' => [
'label' => 'Change password...',
'fields' => [
'current_password' => 'Current password',
'new_password' => 'New password',
'new_password_confirm' => 'Confirm new password',
],
'rate_limit_exceeded' => 'You have made too many attempts. Please try again in :seconds seconds.',
],
],
];
11 changes: 11 additions & 0 deletions resources/lang/fr/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,15 @@
],
],
],
'password_change' => [
'command' => [
'label' => 'Modifier le mot de passe...',
'fields' => [
'current_password' => 'Mot de passe actuel',
'new_password' => 'Nouveau mot de passe',
'new_password_confirm' => 'Confirmer le nouveau mot de passe',
],
'rate_limit_exceeded' => 'Vous avez effectué trop de tentatives. Veuillez réessayer dans :seconds secondes.',
],
],
];
99 changes: 99 additions & 0 deletions src/Auth/Password/Command/IsChangePasswordCommandTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace Code16\Sharp\Auth\Password\Command;

use Code16\Sharp\Exceptions\Form\SharpApplicativeException;
use Code16\Sharp\Form\Fields\SharpFormTextField;
use Code16\Sharp\Utils\Fields\FieldsContainer;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\Rules\Password;

trait IsChangePasswordCommandTrait
{
private bool $confirmPassword = false;
private bool $validateCurrentPassword = true;
private ?Password $passwordRule = null;

public function label(): ?string
{
return trans('sharp::auth.password_change.command.label');
}

public function buildFormFields(FieldsContainer $formFields): void
{
$formFields
->when(
$this->validateCurrentPassword,
fn (FieldsContainer $formFields) => $formFields->addField(
SharpFormTextField::make('password')
->setLabel(trans('sharp::auth.password_change.command.fields.current_password'))
->setInputTypePassword()
)
)
->addField(
SharpFormTextField::make('new_password')
->setLabel(trans('sharp::auth.password_change.command.fields.new_password'))
->setInputTypePassword()
)
->when(
$this->confirmPassword,
fn (FieldsContainer $formFields) => $formFields->addField(
SharpFormTextField::make('new_password_confirmation')
->setLabel(trans('sharp::auth.password_change.command.fields.new_password_confirm'))
->setInputTypePassword()
)
);
}

public function rules(): array
{
$rules = RateLimiter::attempt(
'sharp-password-change-'.auth()->id(),
3,
fn () => [
...$this->validateCurrentPassword
? ['password' => ['required', 'current_password']]
: [],
'new_password' => [
'required',
'string',
$this->passwordRule ?? Password::min(8),
...$this->confirmPassword ? ['confirmed'] : [],
],
],
);

if (! $rules) {
throw new SharpApplicativeException(
trans(
'sharp::auth.password_change.command.rate_limit_exceeded', [
'seconds' => RateLimiter::availableIn('sharp-password-change-'.auth()->id()),
]
)
);
}

return $rules;
}

protected function configureConfirmPassword(?bool $confirmPassword = true): self
{
$this->confirmPassword = $confirmPassword;

return $this;
}

protected function configureValidateCurrentPassword(?bool $validateCurrentPassword = true): self
{
$this->validateCurrentPassword = $validateCurrentPassword;

return $this;
}

protected function configurePasswordRule(Password $passwordRule): self
{
$this->passwordRule = $passwordRule;

return $this;
}
}
Loading
Loading