feat: add a dev-only login and seed command

This commit is contained in:
fromme committed 2026-09-13 15:18:15 +02:00
1 parent 8ae303d1bc
commit ca57df7437
8 files changed
+325 -26

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Resource;
use App\Entity\ResourceType;
use App\Entity\User;
use App\Repository\ResourceRepository;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\When;
/**
* Seeds the desks, parking spots and a handful of employees.
*
* Dev only, and idempotent: it matches on the natural key (resource label, user email) so it can
* be re-run after a migration without duplicating anything. The resource svgElementId values are
* the contract with templates/dashboard/_floorplan.html.twig -- change one and the other stops
* drawing that resource.
*/
#[When('dev')]
#[AsCommand(name: 'app:seed', description: 'Seeds desks, parking spots and demo employees (dev only)')]
final class SeedCommand extends Command
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly ResourceRepository $resources,
private readonly UserRepository $users,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$created = 0;
for ($i = 1; $i <= 12; ++$i) {
$created += $this->ensureResource(ResourceType::DESK, \sprintf('Schreibtisch %02d', $i), \sprintf('desk-%02d', $i));
}
for ($i = 1; $i <= 6; ++$i) {
$created += $this->ensureResource(ResourceType::PARKING, \sprintf('Parkplatz %02d', $i), \sprintf('parking-%02d', $i));
}
foreach ([
['[email protected]', 'Anna Berger'],
['[email protected]', 'Ben Kuhn'],
['[email protected]', 'Clara Dietz'],
['[email protected]', 'David Roth'],
['[email protected]', 'Elif Yılmaz'],
] as [$email, $name]) {
$created += $this->ensureUser($email, $name);
}
$this->em->flush();
$io->success(\sprintf('%d neue Datensätze angelegt.', $created));
return Command::SUCCESS;
}
private function ensureResource(ResourceType $type, string $label, string $svgElementId): int
{
if (null !== $this->resources->findOneBy(['type' => $type, 'label' => $label])) {
return 0;
}
$this->em->persist(new Resource($type, $label, $svgElementId));
return 1;
}
private function ensureUser(string $email, string $name): int
{
if (null !== $this->users->findOneBy(['email' => $email])) {
return 0;
}
$user = new User($email, $name);
// Mirrors what the OAuth2 authenticator will do: the remote roles claim is filtered
// through a local whitelist, and ROLE_EMPLOYEE is what the access_control gate wants.
$user->setRoles(['ROLE_EMPLOYEE']);
$this->em->persist($user);
return 1;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\HttpFoundation\Response;
/**
* Dev-only "log in as" picker, standing in for OAuth2 against myep until the client is
* registered there (plan step 6).
*
* Deliberately carries no #[Route] attribute: the routes are declared in
* config/routes/dev_login.yaml under `when@dev`, so this cannot be reached in any other
* environment even if the class were somehow registered.
*/
#[When('dev')]
final class DevLoginController extends AbstractController
{
public function __construct(private readonly UserRepository $users)
{
}
public function login(): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('app_dashboard');
}
// POST never reaches here -- DevLoginAuthenticator intercepts it on the firewall.
return $this->render('dev/login.html.twig', [
'users' => $this->users->findBy([], ['name' => 'ASC']),
]);
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
/**
* Gains /auth/init and /auth/check when OAuth2 against myep lands (plan step 6).
*/
final class SecurityController extends AbstractController
{
/**
* Never executed: the firewall's logout listener intercepts this route.
*/
#[Route('/logout', name: 'app_logout', methods: ['GET'])]
public function logout(): never
{
throw new \LogicException('Intercepted by the logout key on the main firewall.');
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Security;
use App\Repository\UserRepository;
use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
/**
* Logs in whoever was picked on /dev/login, with no credentials at all.
*
* Dev only. It deliberately has the same shape the real OAuth2 authenticator will take in plan
* step 6 -- supports() matches one route, authenticate() returns a SelfValidatingPassport -- so
* swapping it out is a deletion rather than a rewrite.
*/
#[When('dev')]
final class DevLoginAuthenticator extends AbstractAuthenticator
{
public function __construct(
private readonly UserRepository $users,
private readonly UrlGeneratorInterface $urlGenerator,
) {
}
public function supports(Request $request): bool
{
return 'app_dev_login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
public function authenticate(Request $request): Passport
{
$id = $request->request->getInt('user');
return new SelfValidatingPassport(new UserBadge((string) $id, function (string $identifier) {
$user = $this->users->find((int) $identifier);
if (null === $user) {
throw new CustomUserMessageAuthenticationException('Unbekannter Benutzer.');
}
return $user;
}));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new RedirectResponse($this->urlGenerator->generate('app_dashboard'));
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$request->getSession()->getFlashBag()->add('error', $exception->getMessageKey());
return new RedirectResponse($this->urlGenerator->generate('app_dev_login'));
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Security;
use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
/**
* Sends an unauthenticated visitor to the dev login picker.
*
* Dev only -- it exists so UI work is not gated on the OAuth2 client registration on myep.
* When the real authenticator lands (plan step 6) the entry point for every environment becomes
* the OAuth2 init route and this class goes away.
*/
#[When('dev')]
final readonly class DevLoginEntryPoint implements AuthenticationEntryPointInterface
{
public function __construct(private UrlGeneratorInterface $urlGenerator)
{
}
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
return new RedirectResponse($this->urlGenerator->generate('app_dev_login'));
}
}