feat: configure root redirect
This commit is contained in:
parent
48def71a28
commit
c409b957c2
12 changed files with 375 additions and 4 deletions
|
|
@ -7,6 +7,7 @@ Exemple : un mapping `mcp/graylog/install.sh` peut servir le fichier `install.sh
|
|||
## Fonctionnement
|
||||
|
||||
- `/admin` affiche l’écran de connexion puis l’interface de gestion.
|
||||
- `/` redirige vers l’URL configurée dans `/admin/settings`, ou vers `/admin` si aucune URL n’est configurée.
|
||||
- Un mapping lie un chemin public `.sh` à une URL Git, une référence Git et un chemin de fichier dans le dépôt.
|
||||
- Le bouton `Synchroniser` clone ou met à jour le dépôt, extrait la référence demandée et copie le fichier dans le cache.
|
||||
- Les chemins publics hors `/admin` servent uniquement les scripts déjà synchronisés.
|
||||
|
|
@ -54,11 +55,12 @@ docker compose -f compose.yaml -f compose.dev.yaml run --rm -e APP_ENV=test app
|
|||
## Servir un script
|
||||
|
||||
1. Se connecter sur `/admin`.
|
||||
2. Créer un mapping :
|
||||
2. Optionnel : configurer l’URL de redirection de `/` via `Configuration`.
|
||||
3. Créer un mapping :
|
||||
- Chemin public : `mcp/graylog/install.sh`
|
||||
- Dépôt : `https://forge.lclr.dev/AI/graylog-mcp.git`
|
||||
- Réf. : `main`
|
||||
- Fichier : `install.sh`
|
||||
- Token : optionnel, pour dépôt privé
|
||||
3. Cliquer sur `Synchroniser`.
|
||||
4. Appeler `http://localhost:8080/mcp/graylog/install.sh`.
|
||||
4. Cliquer sur `Synchroniser`.
|
||||
5. Appeler `http://localhost:8080/mcp/graylog/install.sh`.
|
||||
|
|
|
|||
27
migrations/Version20260505081600.php
Normal file
27
migrations/Version20260505081600.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260505081600 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create application settings.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE app_setting (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(190) NOT NULL, value CLOB DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL)');
|
||||
$this->addSql('CREATE UNIQUE INDEX uniq_app_setting_name ON app_setting (name)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE app_setting');
|
||||
}
|
||||
}
|
||||
37
src/Controller/Admin/SettingsController.php
Normal file
37
src/Controller/Admin/SettingsController.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Form\SettingsType;
|
||||
use App\Service\AppSettings;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class SettingsController extends AbstractController
|
||||
{
|
||||
#[Route('/admin/settings', name: 'admin_settings', methods: ['GET', 'POST'])]
|
||||
public function __invoke(Request $request, AppSettings $settings, EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
$form = $this->createForm(SettingsType::class, [
|
||||
'rootRedirectUrl' => $settings->rootRedirectUrl(),
|
||||
]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$data = $form->getData();
|
||||
$settings->setRootRedirectUrl($data['rootRedirectUrl'] ?? null);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Configuration mise à jour.');
|
||||
|
||||
return $this->redirectToRoute('admin_dashboard');
|
||||
}
|
||||
|
||||
return $this->render('admin/settings.html.twig', [
|
||||
'form' => $form,
|
||||
], new Response(status: $form->isSubmitted() ? Response::HTTP_UNPROCESSABLE_ENTITY : Response::HTTP_OK));
|
||||
}
|
||||
}
|
||||
22
src/Controller/HomeController.php
Normal file
22
src/Controller/HomeController.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Service\AppSettings;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class HomeController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'home', methods: ['GET'])]
|
||||
public function __invoke(AppSettings $settings): RedirectResponse
|
||||
{
|
||||
$redirectUrl = $settings->rootRedirectUrl();
|
||||
if ($redirectUrl !== null) {
|
||||
return new RedirectResponse($redirectUrl);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_dashboard');
|
||||
}
|
||||
}
|
||||
87
src/Entity/AppSetting.php
Normal file
87
src/Entity/AppSetting.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\AppSettingRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AppSettingRepository::class)]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_app_setting_name', columns: ['name'])]
|
||||
class AppSetting
|
||||
{
|
||||
public const ROOT_REDIRECT_URL = 'root_redirect_url';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 190)]
|
||||
private string $name = '';
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $value = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private ?DateTimeImmutable $updatedAt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = trim($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(?string $value): self
|
||||
{
|
||||
$value = $value === null ? null : trim($value);
|
||||
$this->value = $value === '' ? null : $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function initializeTimestamps(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt ??= $now;
|
||||
$this->updatedAt ??= $now;
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function refreshUpdatedAt(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
34
src/Form/SettingsType.php
Normal file
34
src/Form/SettingsType.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Url;
|
||||
|
||||
final class SettingsType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('rootRedirectUrl', UrlType::class, [
|
||||
'default_protocol' => null,
|
||||
'required' => false,
|
||||
'constraints' => [
|
||||
new Url(
|
||||
protocols: ['http', 'https'],
|
||||
requireTld: false,
|
||||
message: 'L’URL de redirection doit être une URL http ou https.'
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
30
src/Repository/AppSettingRepository.php
Normal file
30
src/Repository/AppSettingRepository.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AppSetting;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
final class AppSettingRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AppSetting::class);
|
||||
}
|
||||
|
||||
public function getValue(string $name): ?string
|
||||
{
|
||||
return $this->findOneBy(['name' => $name])?->getValue();
|
||||
}
|
||||
|
||||
public function setValue(string $name, ?string $value): AppSetting
|
||||
{
|
||||
$setting = $this->findOneBy(['name' => $name]) ?? (new AppSetting())->setName($name);
|
||||
$setting->setValue($value);
|
||||
|
||||
$this->getEntityManager()->persist($setting);
|
||||
|
||||
return $setting;
|
||||
}
|
||||
}
|
||||
23
src/Service/AppSettings.php
Normal file
23
src/Service/AppSettings.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\AppSetting;
|
||||
use App\Repository\AppSettingRepository;
|
||||
|
||||
final class AppSettings
|
||||
{
|
||||
public function __construct(private readonly AppSettingRepository $settings)
|
||||
{
|
||||
}
|
||||
|
||||
public function rootRedirectUrl(): ?string
|
||||
{
|
||||
return $this->settings->getValue(AppSetting::ROOT_REDIRECT_URL);
|
||||
}
|
||||
|
||||
public function setRootRedirectUrl(?string $url): void
|
||||
{
|
||||
$this->settings->setValue(AppSetting::ROOT_REDIRECT_URL, $url);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,10 @@
|
|||
{% block body %}
|
||||
<main>
|
||||
<h1>Mappings</h1>
|
||||
<p><a href="{{ path('admin_mapping_new') }}">Nouveau mapping</a></p>
|
||||
<p>
|
||||
<a href="{{ path('admin_mapping_new') }}">Nouveau mapping</a>
|
||||
<a href="{{ path('admin_settings') }}">Configuration</a>
|
||||
</p>
|
||||
|
||||
{% for message in app.flashes('success') %}
|
||||
<p>{{ message }}</p>
|
||||
|
|
|
|||
17
templates/admin/settings.html.twig
Normal file
17
templates/admin/settings.html.twig
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Configuration{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<main>
|
||||
<h1>Configuration</h1>
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.rootRedirectUrl, {
|
||||
label: 'URL de redirection de /',
|
||||
help: 'Laisser vide pour rediriger vers /admin.'
|
||||
}) }}
|
||||
<button type="submit">Enregistrer</button>
|
||||
{{ form_end(form) }}
|
||||
</main>
|
||||
{% endblock %}
|
||||
60
tests/Controller/Admin/SettingsControllerTest.php
Normal file
60
tests/Controller/Admin/SettingsControllerTest.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Tests\Controller\Admin;
|
||||
|
||||
use App\Entity\AppSetting;
|
||||
use App\Entity\User;
|
||||
use App\Tests\DatabaseWebTestCase;
|
||||
|
||||
final class SettingsControllerTest extends DatabaseWebTestCase
|
||||
{
|
||||
public function testSettingsRequireAuthentication(): void
|
||||
{
|
||||
$this->client->request('GET', '/admin/settings');
|
||||
|
||||
self::assertResponseRedirects('/admin/login');
|
||||
}
|
||||
|
||||
public function testAdminCanUpdateRootRedirectUrl(): void
|
||||
{
|
||||
$this->loginAsAdmin();
|
||||
|
||||
$crawler = $this->client->request('GET', '/admin/settings');
|
||||
$form = $crawler->selectButton('Enregistrer')->form([
|
||||
'settings[rootRedirectUrl]' => 'https://example.com/installers',
|
||||
]);
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
self::assertResponseRedirects('/admin');
|
||||
|
||||
self::assertSame(
|
||||
'https://example.com/installers',
|
||||
$this->entityManager->getRepository(AppSetting::class)->getValue(AppSetting::ROOT_REDIRECT_URL)
|
||||
);
|
||||
}
|
||||
|
||||
public function testRootRedirectUrlMustBeHttpOrHttps(): void
|
||||
{
|
||||
$this->loginAsAdmin();
|
||||
|
||||
$crawler = $this->client->request('GET', '/admin/settings');
|
||||
$form = $crawler->selectButton('Enregistrer')->form([
|
||||
'settings[rootRedirectUrl]' => 'javascript:alert(1)',
|
||||
]);
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
self::assertNull($this->entityManager->getRepository(AppSetting::class)->getValue(AppSetting::ROOT_REDIRECT_URL));
|
||||
}
|
||||
|
||||
private function loginAsAdmin(): void
|
||||
{
|
||||
$user = (new User())->setUsername('admin')->setPasswordHash('unused');
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->client->loginUser($user);
|
||||
}
|
||||
}
|
||||
29
tests/Controller/HomeControllerTest.php
Normal file
29
tests/Controller/HomeControllerTest.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\AppSetting;
|
||||
use App\Tests\DatabaseWebTestCase;
|
||||
|
||||
final class HomeControllerTest extends DatabaseWebTestCase
|
||||
{
|
||||
public function testRootRedirectsToAdminWhenNoRedirectUrlIsConfigured(): void
|
||||
{
|
||||
$this->client->request('GET', '/');
|
||||
|
||||
self::assertResponseRedirects('/admin');
|
||||
}
|
||||
|
||||
public function testRootRedirectsToConfiguredUrl(): void
|
||||
{
|
||||
$setting = (new AppSetting())
|
||||
->setName(AppSetting::ROOT_REDIRECT_URL)
|
||||
->setValue('https://example.com/installers');
|
||||
$this->entityManager->persist($setting);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->client->request('GET', '/');
|
||||
|
||||
self::assertResponseRedirects('https://example.com/installers');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue