feat: email the administrators when a role nomination appears

This commit is contained in:
2026-09-13 12:50:01 +02:00
parent fab89dead6
commit b41820c39d
12 changed files with 458 additions and 36 deletions
+33 -11
View File
@@ -12,12 +12,14 @@ use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\PersonalData;
use App\Entity\User;
use App\Htmx\HxRedirectResponse;
use App\Message\RoleNominationMessage;
use App\Service\ProfileCompletenessChecker;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
@@ -54,6 +56,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
private readonly ProfileCompletenessChecker $completenessChecker,
private readonly LoggerInterface $authLogger,
private readonly EmployeeDomainMatcher $employeeDomainMatcher,
private readonly MessageBusInterface $messageBus,
) {
}
@@ -120,7 +123,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
->setProfileComplete($this->completenessChecker->isComplete($personalData))
;
$this->syncFromCrm($user, $crmAttributes);
$nominated = $this->syncFromCrm($user, $crmAttributes);
// Registered only once it is fully populated: syncFromCrm() logs on a channel that writes
// to the database, and an account already managed at that point would be flushed
@@ -129,14 +132,24 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
$this->entityManager->persist($user);
$this->entityManager->flush();
// After the flush, deliberately: a first login has no id before it, and the transport is
// Doctrine-backed, so a message queued ahead of a failing flush would announce a
// nomination that was never stored.
if ([] !== $nominated) {
$this->messageBus->dispatch(new RoleNominationMessage((int) $user->getId(), $nominated));
}
return $user;
}
/**
* Writes back what the CRM currently claims: the roles per Role::sync() and the hotel codes
* verbatim. Both replace what is stored, which is what makes BusPro the source of truth.
*
* @return string[] the roles this login newly nominated the account for — the roles
* themselves, not their markers, and empty whenever nothing changed
*/
private function syncFromCrm(User $user, CrmAttributes $crmAttributes): void
private function syncFromCrm(User $user, CrmAttributes $crmAttributes): array
{
$previousRoles = $user->getRoles();
@@ -151,7 +164,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
// An existing account keeps everything it has. A brand new one still needs a role,
// and an empty claim set is exactly what Role::sync() answers with the fallback.
if ([] !== Role::assignedOnly($previousRoles)) {
return;
return [];
}
}
@@ -169,16 +182,25 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
->setHotelCodes(array_values(array_unique($crmAttributes->hotelCodes)))
;
$nominated = array_diff(Role::pendingOnly($user->getRoles()), Role::pendingOnly($previousRoles));
$nominated = array_values(array_diff(
Role::pendingOnly($user->getRoles()),
Role::pendingOnly($previousRoles),
));
if ([] !== $nominated) {
// The CRM claims an administrative role for somebody who does not hold it. It grants
// nothing until an administrator approves it in /admin/user.
$this->authLogger->info('Nominated for administrative roles by the BPN CRM', [
'email' => $user->getEmail(),
'roles' => array_values($nominated),
]);
if ([] === $nominated) {
return [];
}
// The CRM claims an administrative role for somebody who does not hold it. It grants
// nothing until an administrator approves it in /admin/user.
$this->authLogger->info('Nominated for administrative roles by the BPN CRM', [
'email' => $user->getEmail(),
'roles' => $nominated,
]);
// Only the newly appeared markers reach this point, so a repeat login with a nomination
// still standing announces nothing. That difference is the whole de-duplication.
return array_keys(Role::nominatedFrom($nominated));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response