feat(account): change-password endpoint + Settings modal
CI / test (push) Has been cancelled

PATCH /api/user/password — verifies the current password, enforces
8-char minimum on the new one, and rehashes via the configured
password hasher. Returns 204 on success, 422 with an `error` body
on every validation failure (wrong current, too-short new, missing
fields).

Settings adds a "Change password" link under the Account section
that opens a modal with current/new/confirm fields and posts to the
new endpoint. Confirm-mismatch and submit-disabled wiring is
client-side; backend errors surface inline.

Tests: 4 new controller tests cover success, wrong-current,
short-new, and missing-fields; success path also re-fetches the
user and checks the hash actually changed.
This commit is contained in:
2026-05-09 15:25:54 -04:00
parent bdb717de2e
commit 2adb07518c
12 changed files with 295 additions and 8 deletions
+30
View File
@@ -10,6 +10,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -72,6 +73,35 @@ class UserApiController extends AbstractController
return $this->json(['theme' => $theme]);
}
#[Route('/password', name: 'api_user_password', methods: ['PATCH'])]
public function updatePassword(
Request $request,
EntityManagerInterface $em,
UserPasswordHasherInterface $hasher,
): JsonResponse {
$body = json_decode($request->getContent(), true);
$currentPassword = $body['currentPassword'] ?? null;
$newPassword = $body['newPassword'] ?? null;
if (!is_string($currentPassword) || !is_string($newPassword)) {
return $this->json(['error' => 'Both currentPassword and newPassword are required'], Response::HTTP_UNPROCESSABLE_ENTITY);
}
if (strlen($newPassword) < 8) {
return $this->json(['error' => 'New password must be at least 8 characters'], Response::HTTP_UNPROCESSABLE_ENTITY);
}
/** @var User $user */
$user = $this->getUser();
if (!$hasher->isPasswordValid($user, $currentPassword)) {
return $this->json(['error' => 'Current password is incorrect'], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$user->setPassword($hasher->hashPassword($user, $newPassword));
$em->flush();
return $this->json(null, Response::HTTP_NO_CONTENT);
}
#[Route('/timezone', name: 'api_user_timezone', methods: ['PATCH'])]
public function updateTimezone(Request $request, EntityManagerInterface $em): JsonResponse
{