Fix registration: auto-login after signup, atomic token consumption
Registration failed visibly because the controller returned successfully (user created, invite consumed) but the JS then called loadGoals() without an authenticated session — causing a 401 that surfaced as an error to the user. - Add Security::login() after user creation so the session is established immediately, matching the documented "registers + auto-logs in" behavior - Wrap user persist and invite consumption in a single DB transaction so the invite token can never be consumed if user creation fails - Add 12 integration tests covering auto-login, locale updates, logout, partial goal updates, invite isolation, and various edge cases (110 assertions total) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9d4c710d2f
commit
b537066a19
2 changed files with 219 additions and 5 deletions
|
|
@ -5,8 +5,10 @@ namespace App\Controller;
|
|||
use App\Entity\User;
|
||||
use App\Repository\InviteRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\JsonLoginAuthenticator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
|
@ -80,6 +82,7 @@ class AuthController extends AbstractController
|
|||
Request $request,
|
||||
InviteRepository $invites,
|
||||
UserPasswordHasherInterface $hasher,
|
||||
Security $security,
|
||||
): JsonResponse {
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$email = trim($data['email'] ?? '');
|
||||
|
|
@ -108,12 +111,21 @@ class AuthController extends AbstractController
|
|||
->setUsername($name ?: null)
|
||||
->setVerified(true);
|
||||
|
||||
$conn = $this->em->getConnection();
|
||||
$conn->beginTransaction();
|
||||
try {
|
||||
$this->em->persist($user);
|
||||
$this->em->flush(); // flush first to get user ID
|
||||
|
||||
$this->em->flush();
|
||||
$invite->setUsedBy($user->getId());
|
||||
$invite->setUsedAt(new \DateTimeImmutable());
|
||||
$this->em->flush();
|
||||
$conn->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$conn->rollBack();
|
||||
return new JsonResponse(['error' => 'Registrierung fehlgeschlagen'], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$security->login($user, JsonLoginAuthenticator::class, 'main');
|
||||
|
||||
return new JsonResponse(['ok' => true, 'email' => $email, 'name' => $name]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -583,4 +583,206 @@ class AppIntegrationTest extends WebTestCase
|
|||
$this->assertArrayHasKey('error', $data);
|
||||
$this->assertSame(400, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testRegisterAutoLogsIn(): void
|
||||
{
|
||||
$creator = $this->createUser('regautologin');
|
||||
$invite = new Invite();
|
||||
$invite->setToken(bin2hex(random_bytes(32)))
|
||||
->setCreatedBy($creator->getId())
|
||||
->setExpiresAt(new \DateTimeImmutable('+7 days'));
|
||||
$this->em->persist($invite);
|
||||
$this->em->flush();
|
||||
|
||||
$this->json($this->client, 'POST', '/api/register', [
|
||||
'email' => 'regauto@test.dudi',
|
||||
'password' => 'passwort99',
|
||||
'name' => 'Auto User',
|
||||
'token' => $invite->getToken(),
|
||||
]);
|
||||
|
||||
$me = $this->json($this->client, 'GET', '/api/me');
|
||||
$this->assertTrue($me['ok']);
|
||||
$this->assertSame('regauto@test.dudi', $me['email']);
|
||||
}
|
||||
|
||||
public function testRegisterUsedTokenRejected(): void
|
||||
{
|
||||
$creator = $this->createUser('regusedtkinviter');
|
||||
$existing = $this->createUser('regusedtkuser');
|
||||
|
||||
$invite = new Invite();
|
||||
$invite->setToken(bin2hex(random_bytes(32)))
|
||||
->setCreatedBy($creator->getId())
|
||||
->setExpiresAt(new \DateTimeImmutable('+7 days'))
|
||||
->setUsedBy($existing->getId())
|
||||
->setUsedAt(new \DateTimeImmutable());
|
||||
$this->em->persist($invite);
|
||||
$this->em->flush();
|
||||
|
||||
$data = $this->json($this->client, 'POST', '/api/register', [
|
||||
'email' => 'regusedtk@test.dudi',
|
||||
'password' => 'passwort99',
|
||||
'token' => $invite->getToken(),
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('error', $data);
|
||||
$this->assertSame(400, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
// ── Locale ────────────────────────────────────────────────────────────────
|
||||
|
||||
public function testUpdateLocale(): void
|
||||
{
|
||||
$user = $this->createUser('locale');
|
||||
$client = $this->authClient($user);
|
||||
|
||||
foreach (['en', 'pl', 'de'] as $locale) {
|
||||
$data = $this->json($client, 'PATCH', '/api/me', ['locale' => $locale]);
|
||||
$this->assertTrue($data['ok'], "ok missing for locale $locale");
|
||||
$this->assertSame($locale, $data['locale']);
|
||||
}
|
||||
|
||||
$this->em->clear();
|
||||
$user = $this->em->find(User::class, $user->getId());
|
||||
$this->assertSame('de', $user->getLocale());
|
||||
}
|
||||
|
||||
public function testUpdateLocaleInvalid(): void
|
||||
{
|
||||
$user = $this->createUser('localeinvalid');
|
||||
$client = $this->authClient($user);
|
||||
$data = $this->json($client, 'PATCH', '/api/me', ['locale' => 'fr']);
|
||||
|
||||
$this->assertArrayHasKey('error', $data);
|
||||
$this->assertSame(400, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
// ── Logout ────────────────────────────────────────────────────────────────
|
||||
|
||||
public function testLogout(): void
|
||||
{
|
||||
$user = $this->createUser('logout');
|
||||
$client = $this->authClient($user);
|
||||
|
||||
$me = $this->json($client, 'GET', '/api/me');
|
||||
$this->assertTrue($me['ok']);
|
||||
|
||||
$this->json($client, 'POST', '/api/logout');
|
||||
|
||||
$this->json($client, 'GET', '/api/me');
|
||||
$this->assertSame(401, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
// ── Name validation ───────────────────────────────────────────────────────
|
||||
|
||||
public function testUpdateNameWhitespaceOnlyIsRejected(): void
|
||||
{
|
||||
$user = $this->createUser('namewhitespace');
|
||||
$client = $this->authClient($user);
|
||||
$data = $this->json($client, 'PATCH', '/api/me', ['name' => ' ']);
|
||||
|
||||
$this->assertArrayHasKey('error', $data);
|
||||
$this->assertSame(400, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
// ── Password reset (additional) ────────────────────────────────────────────
|
||||
|
||||
public function testResetPasswordWrongToken(): void
|
||||
{
|
||||
$user = $this->createUser('resetwrongtk');
|
||||
$selector = bin2hex(random_bytes(12));
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$this->em->getConnection()->executeStatement(
|
||||
'INSERT INTO users_resets (user, selector, token, expires) VALUES (?, ?, ?, ?)',
|
||||
[$user->getId(), $selector, password_hash($token, PASSWORD_BCRYPT), time() + 3600]
|
||||
);
|
||||
|
||||
$data = $this->json($this->client, 'POST', '/api/reset-password', [
|
||||
'selector' => $selector,
|
||||
'token' => 'falschertoken123',
|
||||
'password' => 'neuesPasswort99',
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('error', $data);
|
||||
$this->assertSame(400, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
// ── Invites (additional) ───────────────────────────────────────────────────
|
||||
|
||||
public function testInviteCreateWithoutNote(): void
|
||||
{
|
||||
$user = $this->createUser('invnonote');
|
||||
$client = $this->authClient($user);
|
||||
$data = $this->json($client, 'POST', '/api/invite', []);
|
||||
|
||||
$this->assertArrayHasKey('url', $data);
|
||||
$this->assertStringContainsString('invite=', $data['url']);
|
||||
|
||||
$invites = $this->em->getRepository(Invite::class)->findBy(['createdBy' => $user->getId()]);
|
||||
$this->assertCount(1, $invites);
|
||||
$this->assertNull($invites[0]->getNote());
|
||||
}
|
||||
|
||||
public function testInviteListIsolatedFromOtherUsers(): void
|
||||
{
|
||||
$user1 = $this->createUser('inviso1');
|
||||
$user2 = $this->createUser('inviso2');
|
||||
|
||||
$invite = new Invite();
|
||||
$invite->setToken(bin2hex(random_bytes(32)))
|
||||
->setCreatedBy($user1->getId())
|
||||
->setExpiresAt(new \DateTimeImmutable('+7 days'));
|
||||
$this->em->persist($invite);
|
||||
$this->em->flush();
|
||||
|
||||
$client = $this->authClient($user2);
|
||||
$data = $this->json($client, 'GET', '/api/invites');
|
||||
|
||||
$this->assertSame([], $data);
|
||||
}
|
||||
|
||||
// ── Goals (additional) ─────────────────────────────────────────────────────
|
||||
|
||||
public function testGoalPartialUpdateOnlyName(): void
|
||||
{
|
||||
$user = $this->createUser('goalpartial');
|
||||
$goal = new Goal();
|
||||
$goal->setUserId($user->getId())->setName('Alt')->setUnit('Min')->setDaily(10.0)->setDays(7)->setStart(new \DateTime())->setSets([]);
|
||||
$this->em->persist($goal);
|
||||
$this->em->flush();
|
||||
|
||||
$client = $this->authClient($user);
|
||||
$data = $this->json($client, 'PATCH', '/api/goals/' . $goal->getId(), ['name' => 'Neu']);
|
||||
|
||||
$this->assertTrue($data['ok']);
|
||||
$this->em->refresh($goal);
|
||||
$this->assertSame('Neu', $goal->getName());
|
||||
$this->assertSame('Min', $goal->getUnit());
|
||||
$this->assertSame(10.0, $goal->getDaily());
|
||||
}
|
||||
|
||||
public function testGoalCreateEmptyUnitFallsBackToDefault(): void
|
||||
{
|
||||
$user = $this->createUser('goalemptyunit');
|
||||
$client = $this->authClient($user);
|
||||
$data = $this->json($client, 'POST', '/api/goals', [
|
||||
'name' => 'Plank',
|
||||
'unit' => '',
|
||||
'daily' => 1,
|
||||
'days' => 7,
|
||||
]);
|
||||
|
||||
$this->assertSame('Stück', $data['unit']);
|
||||
}
|
||||
|
||||
public function testGoalDeleteReturnsOkForNonExistentGoal(): void
|
||||
{
|
||||
$user = $this->createUser('goaldelmissing');
|
||||
$client = $this->authClient($user);
|
||||
$data = $this->json($client, 'DELETE', '/api/goals/999999');
|
||||
|
||||
$this->assertTrue($data['ok']);
|
||||
$this->assertSame(200, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue