chore: remove the seed command and the dev login
This commit is contained in:
1 parent
376251b6d6
commit
dc1822b8cf
7 files changed
-320
No files matched your search
@@ -27,7 +27,6 @@ security:
|
|||||||
# The login surface itself has to stay reachable, or the entry point redirects in a loop.
|
# The login surface itself has to stay reachable, or the entry point redirects in a loop.
|
||||||
- { path: ^/(login|auth|logout)$, roles: PUBLIC_ACCESS }
|
- { path: ^/(login|auth|logout)$, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/auth/, roles: PUBLIC_ACCESS }
|
- { path: ^/auth/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/dev/, roles: PUBLIC_ACCESS }
|
|
||||||
# Before the catch-all, or ROLE_EMPLOYEE would match ^/ first and the admin area would be
|
# Before the catch-all, or ROLE_EMPLOYEE would match ^/ first and the admin area would be
|
||||||
# open to everybody. Order is the whole guard here.
|
# open to everybody. Order is the whole guard here.
|
||||||
- { path: ^/admin, roles: ROLE_ADMIN }
|
- { path: ^/admin, roles: ROLE_ADMIN }
|
||||||
@@ -36,14 +35,3 @@ security:
|
|||||||
# neither of these is refused at login with an explanation rather than let in to an
|
# neither of these is refused at login with an explanation rather than let in to an
|
||||||
# empty screen.
|
# empty screen.
|
||||||
- { path: ^/, roles: [ROLE_EMPLOYEE, ROLE_ADMIN] }
|
- { path: ^/, roles: [ROLE_EMPLOYEE, ROLE_ADMIN] }
|
||||||
|
|
||||||
when@dev:
|
|
||||||
security:
|
|
||||||
firewalls:
|
|
||||||
main:
|
|
||||||
# Until a client is registered on myep, /dev/login lists the seeded users and
|
|
||||||
# logs one in without credentials. The route and the controller are both
|
|
||||||
# dev-only, so this cannot leak into another environment by accident.
|
|
||||||
custom_authenticators:
|
|
||||||
- App\Security\EpAuthenticator
|
|
||||||
- App\Security\DevLoginAuthenticator
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Dev-only stand-in for the OAuth2 login. The controller carries no #[Route] attribute, so
|
|
||||||
# outside dev this route simply does not exist.
|
|
||||||
when@dev:
|
|
||||||
app_dev_login:
|
|
||||||
path: /dev/login
|
|
||||||
controller: App\Controller\Dev\LoginController::index
|
|
||||||
methods: [GET, POST]
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Command;
|
|
||||||
|
|
||||||
use App\Entity\Resource;
|
|
||||||
use App\Entity\ResourceGroup;
|
|
||||||
use App\Entity\User;
|
|
||||||
use App\Enum\ResourceType;
|
|
||||||
use App\Repository\ResourceGroupRepository;
|
|
||||||
use App\Repository\ResourceRepository;
|
|
||||||
use App\Repository\UserRepository;
|
|
||||||
use App\Security\Role;
|
|
||||||
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 office rooms, the desks, the parking spots and a handful of employees.
|
|
||||||
*
|
|
||||||
* Dev only, and idempotent: it matches on the natural key (group and resource label, user email)
|
|
||||||
* so it can be re-run after a migration without duplicating anything.
|
|
||||||
*
|
|
||||||
* The room names and their desk membership are placeholders standing in for the real office
|
|
||||||
* layout.
|
|
||||||
*/
|
|
||||||
#[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 ResourceGroupRepository $groups,
|
|
||||||
private readonly UserRepository $users,
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
|
|
||||||
$created = 0;
|
|
||||||
|
|
||||||
// Four rooms of three desks, in the order they are listed on the plan.
|
|
||||||
$rooms = [];
|
|
||||||
foreach (['Raum Nord', 'Raum Ost', 'Raum Süd', 'Besprechungsraum'] as $position => $label) {
|
|
||||||
[$room, $isNew] = $this->ensureGroup(ResourceType::DESK, $label, $position);
|
|
||||||
$rooms[] = $room;
|
|
||||||
$created += $isNew;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ($i = 1; $i <= 12; ++$i) {
|
|
||||||
$created += $this->ensureResource(
|
|
||||||
ResourceType::DESK,
|
|
||||||
\sprintf('Schreibtisch %02d', $i),
|
|
||||||
$rooms[intdiv($i - 1, 3)],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parking is deliberately left ungrouped: the car park is one area, and it is what keeps
|
|
||||||
// the "a plan without rooms" path exercised by real data.
|
|
||||||
for ($i = 1; $i <= 6; ++$i) {
|
|
||||||
$created += $this->ensureResource(ResourceType::PARKING, \sprintf('Parkplatz %02d', $i));
|
|
||||||
}
|
|
||||||
|
|
||||||
// One administrator on purpose, so the dev environment exercises both roles that open
|
|
||||||
// the app. An admin here is also staff, which is the realistic case.
|
|
||||||
foreach ([
|
|
||||||
['[email protected]', 'Anna Berger', [Role::EMPLOYEE]],
|
|
||||||
['[email protected]', 'Ben Kuhn', [Role::EMPLOYEE, Role::ADMIN]],
|
|
||||||
['[email protected]', 'Clara Dietz', [Role::EMPLOYEE]],
|
|
||||||
['[email protected]', 'David Roth', [Role::EMPLOYEE]],
|
|
||||||
['[email protected]', 'Elif Yılmaz', [Role::EMPLOYEE]],
|
|
||||||
] as [$email, $name, $roles]) {
|
|
||||||
$created += $this->ensureUser($email, $name, $roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->em->flush();
|
|
||||||
|
|
||||||
$io->success(\sprintf('%d new records created.', $created));
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function ensureResource(ResourceType $type, string $label, ?ResourceGroup $group = null): int
|
|
||||||
{
|
|
||||||
$resource = $this->resources->findOneBy(['type' => $type, 'label' => $label]);
|
|
||||||
|
|
||||||
if (null !== $resource) {
|
|
||||||
// Backfill only. A resource seeded before groups existed has no room and would never
|
|
||||||
// be listed under one, but a group somebody moved it into by hand is left alone.
|
|
||||||
if (null !== $group && null === $resource->getGroup()) {
|
|
||||||
$resource->setGroup($group);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$resource = new Resource($type, $label);
|
|
||||||
$resource->setGroup($group);
|
|
||||||
$this->em->persist($resource);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{ResourceGroup, int} the group, and 1 if it was created
|
|
||||||
*/
|
|
||||||
private function ensureGroup(ResourceType $type, string $label, int $position): array
|
|
||||||
{
|
|
||||||
$group = $this->groups->findOneBy(['type' => $type, 'label' => $label]);
|
|
||||||
|
|
||||||
if (null !== $group) {
|
|
||||||
return [$group, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
$group = new ResourceGroup($type, $label, $position);
|
|
||||||
$this->em->persist($group);
|
|
||||||
|
|
||||||
return [$group, 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<string> $roles
|
|
||||||
*/
|
|
||||||
private function ensureUser(string $email, string $name, array $roles): int
|
|
||||||
{
|
|
||||||
if (null !== $this->users->findOneBy(['email' => $email])) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = new User($email, $name);
|
|
||||||
// Mirrors what EpAuthenticator does: whatever myep claims is filtered through
|
|
||||||
// Role::ELIGIBLE, and only those roles are ever stored.
|
|
||||||
$user->setRoles(Role::eligibleOnly($roles));
|
|
||||||
$this->em->persist($user);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Controller\Dev;
|
|
||||||
|
|
||||||
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 LoginController extends AbstractController
|
|
||||||
{
|
|
||||||
public function __construct(private readonly UserRepository $users)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function index(): 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']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?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'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
{% extends 'base.html.twig' %}
|
|
||||||
|
|
||||||
{% block title %}Anmelden · ephub{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div class="min-h-screen flex items-center justify-center px-4">
|
|
||||||
<div class="w-full max-w-md">
|
|
||||||
<img class="h-8 w-auto mx-auto mb-8" src="{{ asset('images/logo_ep.svg') }}" alt="ephub">
|
|
||||||
|
|
||||||
<div class="rounded-md border border-amber-300 bg-amber-50 px-4 py-3 text-sm mb-6">
|
|
||||||
<strong>Nur für die Entwicklung.</strong>
|
|
||||||
Die echte Anmeldung läuft über myep (OAuth2) und wartet auf die Client-Registrierung dort.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="pb-4 text-sm">
|
|
||||||
<a class="underline text-gray-500" href="{{ path('app_login') }}">Zur regulären Anmeldung</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% if users is empty %}
|
|
||||||
<p class="text-sm">
|
|
||||||
Keine Benutzer vorhanden. <code class="font-mono">bin/console app:seed</code> ausführen.
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
<form method="post" class="space-y-2">
|
|
||||||
{% for user in users %}
|
|
||||||
<button type="submit" name="user" value="{{ user.id }}"
|
|
||||||
class="w-full text-left rounded-md border border-gray-200 px-4 py-3 hover:bg-gray-50">
|
|
||||||
<span class="block font-semibold">{{ user.name }}</span>
|
|
||||||
<span class="block text-sm text-gray-500">{{ user.email }}</span>
|
|
||||||
</button>
|
|
||||||
{% endfor %}
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="fixed bottom-4 right-4 z-50 space-y-2">
|
|
||||||
{% include '_partials/_flashes.html.twig' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -18,14 +18,6 @@
|
|||||||
Mit myep anmelden
|
Mit myep anmelden
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if app.environment == 'dev' %}
|
|
||||||
<p class="pt-6 text-sm">
|
|
||||||
<a class="underline text-gray-500" href="{{ path('app_dev_login') }}">
|
|
||||||
Entwickler-Login
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="fixed bottom-4 right-4 z-50 space-y-2">
|
<div class="fixed bottom-4 right-4 z-50 space-y-2">
|
||||||
{% include '_partials/_flashes.html.twig' %}
|
{% include '_partials/_flashes.html.twig' %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user