urlGenerator->generate('app_login'); } public function authenticate(Request $request): Passport { $email = trim($request->request->getString('_username')); $passwordPlain = trim($request->request->getString('_password')); /** @var UserRepository $userRepository */ $userRepository = $this->entityManager->getRepository(User::class); $user = $userRepository->findOneByNormalizedEmail($email); if (null === $user) { // Legacy fallback for accounts that were not created locally yet: // prove credentials against BPN, then bootstrap the local account. $user = $this->bootstrapUser($email, $passwordPlain); } else { if (true === $user->hasLocalPasswordHash()) { if (false === $this->passwordHasher->isPasswordValid($user, $passwordPlain)) { throw new CustomUserMessageAuthenticationException('Benutzername oder Passwort ist nicht korrekt.'); } } else { // Legacy accounts still carry the old encrypted BPN secret as a md5 hash // in the password column. We validate that old value once, then replace it // with a local Symfony hash on success. $legacyPasswordHash = $this->decryptLegacyPassword($user->getPassword()); if ($legacyPasswordHash !== md5($passwordPlain)) { throw new CustomUserMessageAuthenticationException('Benutzername oder Passwort ist nicht korrekt.'); } $user->setPassword($this->passwordHasher->hashPassword($user, $passwordPlain)); } $this->refreshBpnSnapshot($user); } $user->setLastLoginAt(new \DateTimeImmutable()); $this->entityManager->flush(); $csrfToken = $request->request->getString('_csrf_token'); return new SelfValidatingPassport( new UserBadge($email, fn () => $user), [ new CsrfTokenBadge('authenticate', $csrfToken), ] ); } private function bootstrapUser(string $email, string $passwordPlain): User { try { $personalData = $this->apiClient->getPersonalDataWithPassword($email, $passwordPlain); $crmAttributes = $this->apiClient->getCrmAttributesWithPassword($email, $passwordPlain); } catch (ApiClientException $e) { $message = $this->isConnectionError($e) ? 'Der Server ist momentan nicht erreichbar. Bitte versuche es später erneut.' : $e->getMessage(); throw new CustomUserMessageAuthenticationException($message); } if (!$personalData instanceof PersonalData || !$crmAttributes instanceof CrmAttributes) { throw new CustomUserMessageAuthenticationException('Benutzername oder Passwort ist nicht korrekt.'); } $user = new User($email); $passwordHash = $this->passwordHasher->hashPassword($user, $passwordPlain); $profileComplete = 0 === $this->validator->validate($personalData, null, ['personal_data'])->count(); $this->userBootstrapper->bootstrap( $user, $passwordHash, $personalData->addressId, $personalData->personId, $crmAttributes->roles, $crmAttributes->hotelCodes, $profileComplete, ); $this->entityManager->persist($user); return $user; } private function refreshBpnSnapshot(User $user): void { try { $personalData = $this->apiClient->getPersonalData( $user->getUserIdentifier(), $user->getAddressId(), $user->getPersonId(), ); } catch (ApiClientException $e) { $message = $this->isConnectionError($e) ? 'Der Server ist momentan nicht erreichbar. Bitte versuche es später erneut.' : $e->getMessage(); throw new CustomUserMessageAuthenticationException($message); } if (!$personalData instanceof PersonalData) { throw new CustomUserMessageAuthenticationException('Benutzername oder Passwort ist nicht korrekt.'); } $profileComplete = 0 === $this->validator->validate($personalData, null, ['personal_data'])->count(); $user ->setAddressId($personalData->addressId) ->setPersonId($personalData->personId) ->setProfileComplete($profileComplete) ; try { $crmAttributes = $this->apiClient->getCrmAttributes( $user->getUserIdentifier(), $user->getAddressId(), $user->getPersonId(), ); } catch (ApiClientException $e) { $message = $this->isConnectionError($e) ? 'Der Server ist momentan nicht erreichbar. Bitte versuche es später erneut.' : $e->getMessage(); throw new CustomUserMessageAuthenticationException($message); } if (!$crmAttributes instanceof CrmAttributes) { throw new CustomUserMessageAuthenticationException('Benutzername oder Passwort ist nicht korrekt.'); } $user ->setRoles($crmAttributes->roles) ->setHotelCodes($crmAttributes->hotelCodes) ; } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { $this->authLogger->info('Login', [ 'email' => $token->getUserIdentifier(), ]); $targetPath = $this->getTargetPath($request->getSession(), $firewallName) ?? $this->urlGenerator->generate('app_account'); // When redirecting to admin from an HTMX request, force a full page navigation // to avoid layout issues between frontend (hx-boost) and EasyAdmin if ($request->headers->has('HX-Request') && str_contains($targetPath, '/admin')) { return new HxRedirectResponse($targetPath); } return new RedirectResponse($targetPath); } private function decryptLegacyPassword(?string $storedPassword): string { if (null === $storedPassword || '' === $storedPassword) { throw new CustomUserMessageAuthenticationException('Benutzername oder Passwort ist nicht korrekt.'); } $ciphertext = base64_decode($storedPassword, true); if (false === $ciphertext) { $ciphertext = $storedPassword; } try { return $this->crypt->decrypt($ciphertext); } catch (\Throwable) { throw new CustomUserMessageAuthenticationException('Benutzername oder Passwort ist nicht korrekt.'); } } private function isConnectionError(ApiClientException $e): bool { if ($e instanceof TimeoutException || $e instanceof ImmediateConnectionCloseException) { return true; } return 'Unable to open socket' === $e->getMessage(); } }