feat: demotion of user accounts via crm

This commit is contained in:
Björn Fromme
2026-08-10 13:01:01 +02:00
parent b01c2e84c7
commit b39a78da82
11 changed files with 240 additions and 3 deletions
+31
View File
@@ -218,6 +218,37 @@ class UserDataHandler
$this->entityManager->flush();
}
/**
* Blocks a user the CRM no longer grants anything in this application.
*
* The granted roles are deliberately kept: they stay visible for review and are what
* makes the user reappear in the administrative list, where a super admin can unblock
* them. Only the privilege-free markers are dropped, as they no longer reflect the CRM.
* Regaining a CRM role does not unblock the account, that is a manual decision.
*/
public function disableForRevokedCrmRoles(User $user): void
{
// an existing block may be a disciplinary one and must never be overwritten
if (true === $user->isDisabled()) {
return;
}
$this->refreshPendingRoles($user, []);
$user
->setDisabledAt(new \DateTimeImmutable())
->setDisabledReason('Für deinen Account liegt in BusPro keine Berechtigung mehr vor.')
->setDisabledReasonInternal('Automatisch gesperrt: keine Rollen in BusPro.')
;
$this->entityManager->flush();
$this->logger->info('Disable user without CRM roles', [
'user_id' => $user->getId(),
'user_email' => $user->getEmail(),
]);
}
/**
* Keeps the pending markers in sync with the administrative roles claimed in the CRM.
* The markers grant no privileges, so tracking them on every login is safe: only a
@@ -38,6 +38,7 @@ class EditController extends AbstractController
'roles' => $user->getRoles(),
'super_admin' => $user->isSuperAdmin(),
'hotel_codes' => $user->getHotelCodes(),
'disabled' => $user->isDisabled(),
]);
return $this->redirectToRoute('app_admin_system_user_index');
+21
View File
@@ -387,6 +387,27 @@ class User implements UserInterface, TimestampableEntityInterface
return null !== $this->disabledAt;
}
/**
* Blocks or unblocks the account, keeping the reasons in sync. Unchanged state is a
* no-op so that saving an unrelated change does not reset the timestamp.
*/
public function setDisabled(bool $disabled): static
{
if ($disabled === $this->isDisabled()) {
return $this;
}
if (false === $disabled) {
return $this
->setDisabledAt(null)
->setDisabledReason(null)
->setDisabledReasonInternal(null)
;
}
return $this->setDisabledAt(new \DateTimeImmutable());
}
public function getDisabledReason(): ?string
{
return $this->disabledReason;
+17
View File
@@ -6,6 +6,7 @@ use App\Config\HouseCatalog;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
@@ -32,6 +33,22 @@ class UserType extends AbstractType
'required' => false,
'help' => 'Setzt die Rolle Admin voraus.',
])
// must stay ahead of the reason: properties are written in field order and
// unblocking clears the reasons
->add('disabled', CheckboxType::class, [
'label' => 'Account gesperrt',
'required' => false,
'help' => 'Gesperrte Benutzer:innen können sich nicht anmelden.',
])
->add('disabledReason', TextareaType::class, [
'label' => 'Begründung',
'required' => false,
'help' => 'Wird bei der Anmeldung angezeigt.',
'attr' => [
'data-controller' => 'textarea-autosize',
'data-action' => 'textarea-autosize#resize',
],
])
;
// hotel codes already assigned to the user may predate the catalog, so they are
+20 -1
View File
@@ -124,12 +124,31 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
// Determine teamer status from CRM attributes
$isTeamer = $crmAttributes->isTeamer();
// Everything the CRM grants this person here: pending markers plus ROLE_TEAMER
$claimedRoles = $this
->userDataHandler
->collectRoles($crmAttributes)
;
// Check if user is already present in local database
$user = $this
->userDataHandler
->findLocalUser($profileResponse)
;
// BusPro knows this person but grants them nothing in this application, so they
// are no user of it: never create an account, block an existing one. Returning
// the blocked user lets the UserChecker explain why the login was refused.
if ([] === $claimedRoles) {
if (null === $user) {
return null;
}
$this->userDataHandler->disableForRevokedCrmRoles($user);
return $user;
}
// Update existing user's teamer data and return it, leaving roles and hotel
// codes alone: they are imported once on creation and managed manually after
if (null !== $user) {
@@ -152,7 +171,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
->userDataHandler
->createLocalUser(
$profileResponse,
$this->userDataHandler->collectRoles($crmAttributes),
$claimedRoles,
$isTeamer,
$crmSelections,
$crmAttributes->getHotelCodes(),
+1 -1
View File
@@ -21,7 +21,7 @@ class UserChecker implements UserCheckerInterface
if ([] === array_intersect($user->getRoles(), array_keys(User::ROLES))) {
if ([] !== $user->getPendingRoles()) {
throw new CustomUserMessageAccountStatusException('Deine Rolle wurde noch nicht freigeschaltet.');
throw new CustomUserMessageAccountStatusException('Dein Account wurde noch nicht freigeschaltet.');
}
throw new CustomUserMessageAccountStatusException('Keine gültige Rolle zugewiesen.');