feat: update password
Laravel CI-CD / Tests Unitaires (push) Successful in 1m43s Details
Laravel CI-CD / Deploy with Kamal (push) Successful in 2m21s Details

This commit is contained in:
Leon Morival 2026-05-26 13:48:22 +02:00
parent 835dfcc677
commit b3968a8181
6 changed files with 138 additions and 1 deletions

View File

@ -7,6 +7,7 @@ use App\Http\Requests\ForgotPasswordRequest;
use App\Http\Requests\LoginRequest;
use App\Http\Requests\RegisterRequest;
use App\Http\Requests\ResetPasswordRequest;
use App\Http\Requests\UpdatePasswordRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
@ -255,7 +256,7 @@ class AuthController extends Controller
Storage::disk()->delete($user->avatar_url);
}
$path = $request->file('avatar')->store('avatars', );
$path = $request->file('avatar')->store('avatars');
$data['avatar_url'] = $path;
}
@ -275,6 +276,32 @@ class AuthController extends Controller
return new UserResource($user->fresh());
}
public function updatePassword(UpdatePasswordRequest $request): JsonResponse
{
$user = $request->user();
abort_if($user->isSuspended(), 403, __('api.auth.suspended'));
$currentAccessToken = $user->currentAccessToken();
$user->forceFill([
'password' => Hash::make($request->validated('password')),
'remember_token' => Str::random(60),
])->save();
if ($currentAccessToken instanceof PersonalAccessToken) {
$user->tokens()
->whereKeyNot($currentAccessToken->getKey())
->delete();
} else {
$user->tokens()->delete();
}
return response()->json([
'message' => __('api.auth.password_updated'),
]);
}
public function destroy(Request $request): JsonResponse
{
$user = $request->user();

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePasswordRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'current_password' => ['required', 'string', 'current_password:sanctum'],
'password' => ['required', 'string', 'min:8', 'confirmed', 'different:current_password'],
];
}
protected function prepareForValidation(): void
{
if ($this->has('currentPassword') && ! $this->has('current_password')) {
$this->merge([
'current_password' => $this->input('currentPassword'),
]);
}
if ($this->has('passwordConfirmation') && ! $this->has('password_confirmation')) {
$this->merge([
'password_confirmation' => $this->input('passwordConfirmation'),
]);
}
}
}

View File

@ -13,6 +13,7 @@ return [
'verification_email_failed' => 'Unable to send the verification email right now. Please try again later.',
'password_reset_link_sent' => 'If an account exists for this address, a password reset link has been sent.',
'password_reset' => 'Password reset successfully.',
'password_updated' => 'Password updated successfully.',
'logout' => 'Logged out successfully.',
'unauthenticated' => 'Unauthenticated.',
'deleted' => 'Account deleted successfully.',

View File

@ -13,6 +13,7 @@ return [
'verification_email_failed' => "Impossible d'envoyer l'email de vérification pour le moment. Réessaie plus tard.",
'password_reset_link_sent' => 'Si un compte existe avec cette adresse, un lien de réinitialisation vient dêtre envoyé.',
'password_reset' => 'Mot de passe réinitialisé avec succès.',
'password_updated' => 'Mot de passe modifié avec succès.',
'logout' => 'Déconnecté avec succès.',
'unauthenticated' => 'Non authentifié.',
'deleted' => 'Compte supprimé avec succès.',

View File

@ -35,6 +35,7 @@ Route::prefix('auth')->group(function (): void {
Route::post('logout', [AuthController::class, 'logout']);
Route::get('me', [AuthController::class, 'me']);
Route::patch('me', [AuthController::class, 'update'])->middleware(['verified', 'throttle:content-write']);
Route::patch('me/password', [AuthController::class, 'updatePassword'])->middleware('throttle:account-action');
Route::delete('me', [AuthController::class, 'destroy'])->middleware('throttle:account-action');
});

View File

@ -0,0 +1,63 @@
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
uses(RefreshDatabase::class);
it('updates the authenticated user password and keeps the current api token', function () {
$user = User::factory()->create([
'password' => Hash::make('OldPassword123!'),
]);
$currentToken = $user->createToken('current-device');
$otherToken = $user->createToken('other-device');
$this
->withHeader('Authorization', 'Bearer '.$currentToken->plainTextToken)
->patchJson('/api/auth/me/password', [
'currentPassword' => 'OldPassword123!',
'password' => 'NewPassword123!',
'passwordConfirmation' => 'NewPassword123!',
])
->assertOk()
->assertJsonPath('message', __('api.auth.password_updated'));
expect(Hash::check('NewPassword123!', $user->fresh()->password))->toBeTrue();
$this->assertDatabaseHas('personal_access_tokens', [
'id' => $currentToken->accessToken->getKey(),
]);
$this->assertDatabaseMissing('personal_access_tokens', [
'id' => $otherToken->accessToken->getKey(),
]);
});
it('rejects an invalid current password', function () {
$user = User::factory()->create([
'password' => Hash::make('OldPassword123!'),
]);
$currentToken = $user->createToken('current-device');
$this
->withHeader('Authorization', 'Bearer '.$currentToken->plainTextToken)
->patchJson('/api/auth/me/password', [
'currentPassword' => 'wrong-password',
'password' => 'NewPassword123!',
'passwordConfirmation' => 'NewPassword123!',
])
->assertUnprocessable()
->assertJsonValidationErrors(['current_password']);
expect(Hash::check('OldPassword123!', $user->fresh()->password))->toBeTrue();
});
it('requires authentication to update the password', function () {
$this
->patchJson('/api/auth/me/password', [
'currentPassword' => 'OldPassword123!',
'password' => 'NewPassword123!',
'passwordConfirmation' => 'NewPassword123!',
])
->assertUnauthorized();
});