Die Verwaltung kann umziehen: /admin ist frei waehlbar
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>
This commit is contained in:
1 parent
9f1ac39251
commit
aaea385422
26 files changed
+293
-45
No files matched your search
@@ -24,7 +24,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
#[Route('/admin')]
|
||||
#[Route('/%kant.admin_path%')]
|
||||
final class EditorController extends AbstractController
|
||||
{
|
||||
public function __construct(private PageRepository $pages, private DocumentValidator $validator, private MenuRepository $menus, private PostRepository $posts, private Theme $theme, private StyleCompiler $styles, private RedirectRepository $redirects, private PartialRepository $partials, private CustomCode $code, private CsrfTokenManagerInterface $csrf)
|
||||
|
||||
@@ -16,7 +16,7 @@ final class FormAdminController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/forms', name: 'admin_forms', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/forms', name: 'admin_forms', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return $this->render('admin/forms.html.twig', [
|
||||
@@ -26,7 +26,7 @@ final class FormAdminController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/forms/{slug}', name: 'admin_form_save', requirements: ['slug' => '[a-z0-9-]{1,80}|neu'], methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/forms/{slug}', name: 'admin_form_save', requirements: ['slug' => '[a-z0-9-]{1,80}|neu'], methods: ['POST'])]
|
||||
public function save(Request $request, string $slug): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -18,7 +18,7 @@ final class MediaController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/media', name: 'admin_media', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/media', name: 'admin_media', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
// The screen renders the images itself, page by page. The dialog stays what it is: the way
|
||||
@@ -31,7 +31,7 @@ final class MediaController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/media/upload', name: 'admin_media_upload', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/media/upload', name: 'admin_media_upload', methods: ['POST'])]
|
||||
public function formUpload(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -57,7 +57,7 @@ final class MediaController extends AbstractController
|
||||
return $this->back($request, sprintf('%d Bild(er) hochgeladen.', $done), 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/media/{id}', name: 'admin_media_save', requirements: ['id' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/media/{id}', name: 'admin_media_save', requirements: ['id' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||||
public function formUpdate(Request $request, string $id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -95,7 +95,7 @@ final class MediaController extends AbstractController
|
||||
return $this->redirectToRoute('admin_media', $query);
|
||||
}
|
||||
|
||||
#[Route('/admin/api/media', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/api/media', methods: ['GET'])]
|
||||
public function list(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -103,8 +103,8 @@ final class MediaController extends AbstractController
|
||||
return $this->json(['items' => $this->media->list(), 'csrfToken' => $this->csrf->getToken('media')->getValue()]);
|
||||
}
|
||||
|
||||
#[Route('/admin/api/media/upload', methods: ['POST'])]
|
||||
#[Route('/admin/api/media/{id}', requirements: ['id' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/api/media/upload', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/api/media/{id}', requirements: ['id' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||||
public function write(Request $request, ?string $id = null): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -17,7 +17,7 @@ final class MenuController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/menus', name: 'admin_menus', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/menus', name: 'admin_menus', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$menus = $this->menus->all();
|
||||
@@ -39,7 +39,7 @@ final class MenuController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/menus/{slot}', name: 'admin_menu_save', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/menus/{slot}', name: 'admin_menu_save', methods: ['POST'])]
|
||||
public function save(Request $request, string $slot): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -25,7 +25,7 @@ use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
* against areas without a second implementation. Areas have no draft and no publication: they are
|
||||
* shared, so a saved change is live. The editor hides the publish button for them.
|
||||
*/
|
||||
#[Route('/admin/partials')]
|
||||
#[Route('/%kant.admin_path%/partials')]
|
||||
final class PartialController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
|
||||
@@ -9,7 +9,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
final class SecurityController extends AbstractController
|
||||
{
|
||||
#[Route('/admin/login', name: 'admin_login', methods: ['GET', 'POST'])]
|
||||
#[Route('/%kant.admin_path%/login', name: 'admin_login', methods: ['GET', 'POST'])]
|
||||
public function login(AuthenticationUtils $auth): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
@@ -19,7 +19,7 @@ final class SecurityController extends AbstractController
|
||||
return $this->render('admin/login.html.twig', ['error' => $auth->getLastAuthenticationError(), 'username' => $auth->getLastUsername()]);
|
||||
}
|
||||
|
||||
#[Route('/admin/logout', name: 'admin_logout', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/logout', name: 'admin_logout', methods: ['POST'])]
|
||||
public function logout(): never
|
||||
{
|
||||
throw new \LogicException('Handled by Symfony Security.');
|
||||
|
||||
@@ -21,6 +21,7 @@ final class SeoController extends AbstractController
|
||||
private SiteSettings $settings,
|
||||
private PostRepository $posts,
|
||||
private string $siteOrigin,
|
||||
private string $adminPath,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -58,7 +59,15 @@ final class SeoController extends AbstractController
|
||||
public function robots(): Response
|
||||
{
|
||||
$settings = $this->settings->load();
|
||||
$lines = ['User-agent: *', 'Disallow: /admin', 'Disallow: /formular/'];
|
||||
// Die Verwaltung wird hier nur genannt, solange sie „admin" heisst - diese Adresse kennt
|
||||
// ohnehin jeder Scanner. Wer sie umbenannt hat, will sie nicht in einer oeffentlichen
|
||||
// Datei wiederfinden: robots.txt ist das erste, was ein Angreifer liest. Vom Indexieren
|
||||
// haelt die Verwaltung ihr eigener Kopf `X-Robots-Tag: noindex` ab.
|
||||
$lines = ['User-agent: *'];
|
||||
if ('admin' === $this->adminPath) {
|
||||
$lines[] = 'Disallow: /admin';
|
||||
}
|
||||
$lines[] = 'Disallow: /formular/';
|
||||
if ('' !== $settings['robotsExtra']) {
|
||||
$lines[] = $settings['robotsExtra'];
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ final class SettingsController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/settings', name: 'admin_settings', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/settings', name: 'admin_settings', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$choosable = [];
|
||||
@@ -44,7 +44,7 @@ final class SettingsController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/settings/company', name: 'admin_settings_save', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/settings/company', name: 'admin_settings_save', methods: ['POST'])]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -60,7 +60,7 @@ final class SettingsController extends AbstractController
|
||||
return $this->back('Die Unternehmensdaten wurden gespeichert.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/settings/redirects', name: 'admin_redirect_save', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/settings/redirects', name: 'admin_redirect_save', methods: ['POST'])]
|
||||
public function saveRedirect(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -92,7 +92,7 @@ final class SettingsController extends AbstractController
|
||||
* The pages are mandatory, the text is not something a CMS can write: what gets created is the
|
||||
* structure with every required item named and marked as still to be filled in.
|
||||
*/
|
||||
#[Route('/admin/settings/rechtliches', name: 'admin_legal_pages', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/settings/rechtliches', name: 'admin_legal_pages', methods: ['POST'])]
|
||||
public function legalPages(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -17,7 +17,7 @@ final class StatisticsController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/statistik', name: 'admin_statistics', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/statistik', name: 'admin_statistics', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -32,7 +32,7 @@ final class StatisticsController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/statistik.csv', name: 'admin_statistics_csv', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/statistik.csv', name: 'admin_statistics_csv', methods: ['GET'])]
|
||||
public function csv(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -19,11 +19,11 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
*/
|
||||
final class SystemController extends AbstractController
|
||||
{
|
||||
public function __construct(private Updater $updater, private Backup $backup)
|
||||
public function __construct(private Updater $updater, private Backup $backup, private string $adminPath)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/system', name: 'admin_system', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/system', name: 'admin_system', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
@@ -38,10 +38,11 @@ final class SystemController extends AbstractController
|
||||
'asked' => $request->query->getBoolean('pruefen'),
|
||||
'steps' => $request->getSession()->remove('update_steps') ?: [],
|
||||
'backups' => $this->backup->all(),
|
||||
'adminPath' => $this->adminPath,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/system/update', name: 'admin_system_update', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/system/update', name: 'admin_system_update', methods: ['POST'])]
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
@@ -69,7 +70,7 @@ final class SystemController extends AbstractController
|
||||
* `cms:restore` is the way, and the list says which backups exist and whether their checksum
|
||||
* still matches.
|
||||
*/
|
||||
#[Route('/admin/system/sicherung', name: 'admin_system_backup', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/system/sicherung', name: 'admin_system_backup', methods: ['POST'])]
|
||||
public function backup(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
@@ -94,7 +95,7 @@ final class SystemController extends AbstractController
|
||||
), 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/system/wartung', name: 'admin_system_maintenance', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/system/wartung', name: 'admin_system_maintenance', methods: ['POST'])]
|
||||
public function maintenance(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
|
||||
@@ -16,7 +16,7 @@ final class ThemeController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/theme', name: 'admin_theme', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/theme', name: 'admin_theme', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$theme = $this->theme->load();
|
||||
@@ -36,7 +36,7 @@ final class ThemeController extends AbstractController
|
||||
* Auf Zuruf, nicht bei einem Seitenaufruf: das dauert Sekunden und braucht eine Verbindung
|
||||
* nach aussen, die ein Besucher nie abwarten soll.
|
||||
*/
|
||||
#[Route('/admin/theme/fonts', name: 'admin_theme_fonts', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/theme/fonts', name: 'admin_theme_fonts', methods: ['POST'])]
|
||||
public function fonts(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -65,7 +65,7 @@ final class ThemeController extends AbstractController
|
||||
return $bytes < 1024 * 1024 ? round($bytes / 1024).' KB' : round($bytes / 1024 / 1024, 1).' MB';
|
||||
}
|
||||
|
||||
#[Route('/admin/theme/save', name: 'admin_theme_save', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/theme/save', name: 'admin_theme_save', methods: ['POST'])]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -22,7 +22,7 @@ final class UserController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer', name: 'admin_users', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/benutzer', name: 'admin_users', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
@@ -37,7 +37,7 @@ final class UserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer/neu', name: 'admin_user_create', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/benutzer/neu', name: 'admin_user_create', methods: ['POST'])]
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
if (null !== $refusal = $this->guard($request)) {
|
||||
@@ -56,7 +56,7 @@ final class UserController extends AbstractController
|
||||
return $this->back('Das Konto wurde angelegt.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer/{id}/rolle', name: 'admin_user_role', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
||||
#[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)) {
|
||||
@@ -71,7 +71,7 @@ final class UserController extends AbstractController
|
||||
return $this->back('Die Rolle wurde geändert. Offene Sitzungen dieses Kontos sind damit abgemeldet.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer/{id}/passwort', name: 'admin_user_password', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
||||
#[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)) {
|
||||
@@ -86,7 +86,7 @@ final class UserController extends AbstractController
|
||||
return $this->back('Das Passwort wurde gesetzt. Offene Sitzungen dieses Kontos sind damit abgemeldet.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer/{id}/loeschen', name: 'admin_user_delete', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
||||
#[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)) {
|
||||
@@ -101,7 +101,7 @@ final class UserController extends AbstractController
|
||||
return $this->back('Das Konto wurde gelöscht.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/konto', name: 'admin_account', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/konto', name: 'admin_account', methods: ['GET'])]
|
||||
public function account(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -122,7 +122,7 @@ final class UserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/konto/passwort', name: 'admin_account_password', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/konto/passwort', name: 'admin_account_password', methods: ['POST'])]
|
||||
public function ownPassword(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
Reference in new issue
Block a user