php bin/console cms:admin:path <neu> legt die Verwaltung auf eine andere Adresse. Geprueft wird, dass der Name frei ist und keine Seite verdeckt; der Wert steht in .env.local und fuellt einen Parameter, der als Praefix in jeder Verwaltungsroute steht, in der Firewall, in den Schutzkopfzeilen und in der Statistik. Die robots.txt nennt einen geaenderten Namen nicht - sie ist oeffentlich und waere sonst ein Wegweiser. Beim Standardnamen bleibt die Zeile, weil diese Adresse ohnehin jeder kennt. Die Auffangroute fuer Seiten steht jetzt zuletzt: sie kann einen frei gewaehlten Namen nicht per Regex ausschliessen, und die Reihenfolge ist ohnehin die richtige. Der Befehl raeumt den Zwischenspeicher auf, aber nicht den, in dem er selbst laeuft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
178 lines
6.7 KiB
PHP
178 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Domain\CmsUser;
|
|
use App\Domain\InvalidAccount;
|
|
use App\Security\UserDirectory;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
|
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
|
|
|
/**
|
|
* Accounts. The list and every change to somebody else's account need ROLE_ADMIN; the own password
|
|
* is the one thing every signed in account may change, and only with the current one in hand.
|
|
*/
|
|
final class UserController extends AbstractController
|
|
{
|
|
public function __construct(private UserDirectory $users, private TokenStorageInterface $tokens)
|
|
{
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/benutzer', name: 'admin_users', methods: ['GET'])]
|
|
public function index(): Response
|
|
{
|
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
|
|
|
return $this->render('admin/users.html.twig', [
|
|
'users' => $this->users->all(),
|
|
'roles' => UserDirectory::ROLES,
|
|
'roleHelp' => UserDirectory::ROLE_HELP,
|
|
'minPassword' => UserDirectory::MIN_PASSWORD,
|
|
'maxPassword' => UserDirectory::MAX_PASSWORD,
|
|
'me' => $this->cmsUser()->id,
|
|
]);
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/benutzer/neu', name: 'admin_user_create', methods: ['POST'])]
|
|
public function create(Request $request): Response
|
|
{
|
|
if (null !== $refusal = $this->guard($request)) {
|
|
return $refusal;
|
|
}
|
|
try {
|
|
$this->users->create(
|
|
$request->request->getString('username'),
|
|
$request->request->getString('password'),
|
|
$request->request->getString('role'),
|
|
);
|
|
} catch (InvalidAccount $e) {
|
|
return $this->back($e->getMessage());
|
|
}
|
|
|
|
return $this->back('Das Konto wurde angelegt.', 'success');
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/benutzer/{id}/rolle', name: 'admin_user_role', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
|
public function role(Request $request, string $id): Response
|
|
{
|
|
if (null !== $refusal = $this->guard($request)) {
|
|
return $refusal;
|
|
}
|
|
try {
|
|
$this->users->changeRole($id, $request->request->getString('role'), $this->cmsUser());
|
|
} catch (InvalidAccount $e) {
|
|
return $this->back($e->getMessage());
|
|
}
|
|
|
|
return $this->back('Die Rolle wurde geändert. Offene Sitzungen dieses Kontos sind damit abgemeldet.', 'success');
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/benutzer/{id}/passwort', name: 'admin_user_password', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
|
public function password(Request $request, string $id): Response
|
|
{
|
|
if (null !== $refusal = $this->guard($request)) {
|
|
return $refusal;
|
|
}
|
|
try {
|
|
$this->users->setPassword($id, $request->request->getString('password'));
|
|
} catch (InvalidAccount $e) {
|
|
return $this->back($e->getMessage());
|
|
}
|
|
|
|
return $this->back('Das Passwort wurde gesetzt. Offene Sitzungen dieses Kontos sind damit abgemeldet.', 'success');
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/benutzer/{id}/loeschen', name: 'admin_user_delete', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
|
public function delete(Request $request, string $id): Response
|
|
{
|
|
if (null !== $refusal = $this->guard($request)) {
|
|
return $refusal;
|
|
}
|
|
try {
|
|
$this->users->delete($id, $this->cmsUser());
|
|
} catch (InvalidAccount $e) {
|
|
return $this->back($e->getMessage());
|
|
}
|
|
|
|
return $this->back('Das Konto wurde gelöscht.', 'success');
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/konto', name: 'admin_account', methods: ['GET'])]
|
|
public function account(): Response
|
|
{
|
|
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
|
$me = $this->cmsUser();
|
|
$mine = null;
|
|
foreach ($this->users->all() as $account) {
|
|
if ($account['id'] === $me->id) {
|
|
$mine = $account;
|
|
}
|
|
}
|
|
|
|
return $this->render('admin/account.html.twig', [
|
|
'account' => $mine,
|
|
'roles' => UserDirectory::ROLES,
|
|
'roleHelp' => UserDirectory::ROLE_HELP,
|
|
'minPassword' => UserDirectory::MIN_PASSWORD,
|
|
'maxPassword' => UserDirectory::MAX_PASSWORD,
|
|
]);
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/konto/passwort', name: 'admin_account_password', methods: ['POST'])]
|
|
public function ownPassword(Request $request): Response
|
|
{
|
|
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
|
if (!$this->isCsrfTokenValid('account', $request->request->getString('_csrf_token'))) {
|
|
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.', 'error', 'admin_account');
|
|
}
|
|
$me = $this->cmsUser();
|
|
try {
|
|
$fresh = $this->users->changeOwnPassword(
|
|
$me,
|
|
$request->request->getString('current'),
|
|
$request->request->getString('password'),
|
|
);
|
|
} catch (InvalidAccount $e) {
|
|
return $this->back($e->getMessage(), 'error', 'admin_account');
|
|
}
|
|
|
|
// Keep this session signed in with the new hash. Every other session of the account fails
|
|
// its next refresh against the database and is logged out - which is the point.
|
|
$this->tokens->setToken(new UsernamePasswordToken($fresh, 'admin', $fresh->getRoles()));
|
|
|
|
return $this->back('Das Passwort wurde geändert. Andere offene Sitzungen dieses Kontos sind abgemeldet.', 'success', 'admin_account');
|
|
}
|
|
|
|
/** Every write needs the administrator role and a valid token; the check is the same each time. */
|
|
private function guard(Request $request): ?Response
|
|
{
|
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
|
if (!$this->isCsrfTokenValid('users', $request->request->getString('_csrf_token'))) {
|
|
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function cmsUser(): CmsUser
|
|
{
|
|
$user = $this->getUser();
|
|
if (!$user instanceof CmsUser) {
|
|
throw $this->createAccessDeniedException();
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
private function back(string $message, string $type = 'error', string $route = 'admin_users'): Response
|
|
{
|
|
$this->addFlash($type, $message);
|
|
|
|
return $this->redirectToRoute($route);
|
|
}
|
|
}
|