- Remove orphaned PUBLIC_ACCESS rule for deleted /api/test route - Remove JWT login firewall (app is session-based only) - Set APP_SECRET placeholder (real value must be in .env.local) - Remove JWT env vars from .env - Add session regeneration on login (prevent session fixation) - Remove Document.path from API serialization groups (prevent path leak) - Restrict health check details to ROLE_ADMIN (anonymes get status only) - Add path traversal guard in DocumentStorageService - Convert CreateProfileCommand password to interactive hidden prompt - Restrict Profile Get endpoint to ROLE_ADMIN - Change api firewall to stateless: false (matches session-based auth) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
105 lines
3.2 KiB
PHP
105 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Command;
|
|
|
|
use App\Entity\Profile;
|
|
use App\Repository\ProfileRepository;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Input\InputArgument;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
|
|
|
use function in_array;
|
|
|
|
#[AsCommand(
|
|
name: 'app:create-profile',
|
|
description: 'Create a new profile with the given credentials',
|
|
)]
|
|
class CreateProfileCommand extends Command
|
|
{
|
|
public function __construct(
|
|
private readonly ProfileRepository $profiles,
|
|
private readonly EntityManagerInterface $em,
|
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this
|
|
->addArgument('firstName', InputArgument::REQUIRED, 'First name')
|
|
->addArgument('lastName', InputArgument::REQUIRED, 'Last name')
|
|
->addOption('email', null, InputOption::VALUE_REQUIRED, 'Email address')
|
|
->addOption('role', null, InputOption::VALUE_REQUIRED, 'Role (ROLE_ADMIN, ROLE_GESTIONNAIRE, ROLE_VIEWER)', 'ROLE_VIEWER')
|
|
;
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$io = new SymfonyStyle($input, $output);
|
|
|
|
$firstName = $input->getArgument('firstName');
|
|
$lastName = $input->getArgument('lastName');
|
|
$email = $input->getOption('email');
|
|
|
|
$password = $io->askHidden('Password');
|
|
if (null === $password || '' === $password) {
|
|
$io->error('Le mot de passe est requis.');
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
$role = $input->getOption('role');
|
|
|
|
$allowedRoles = ['ROLE_ADMIN', 'ROLE_GESTIONNAIRE', 'ROLE_VIEWER', 'ROLE_USER'];
|
|
if (!in_array($role, $allowedRoles, true)) {
|
|
$io->error('Role invalide. Roles autorisés : '.implode(', ', $allowedRoles));
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
if (null !== $email && '' !== $email) {
|
|
$existing = $this->profiles->findOneBy(['email' => $email]);
|
|
if (null !== $existing) {
|
|
$io->error('Un profil avec cet email existe déjà.');
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
}
|
|
|
|
$profile = new Profile();
|
|
$profile->setFirstName($firstName);
|
|
$profile->setLastName($lastName);
|
|
$profile->setRoles([$role]);
|
|
$profile->setIsActive(true);
|
|
|
|
if (null !== $email && '' !== $email) {
|
|
$profile->setEmail($email);
|
|
}
|
|
|
|
$profile->setPassword(
|
|
$this->passwordHasher->hashPassword($profile, $password)
|
|
);
|
|
|
|
$this->em->persist($profile);
|
|
$this->em->flush();
|
|
|
|
$io->success(sprintf(
|
|
'Profil créé : %s %s (ID: %s, Role: %s)',
|
|
$firstName,
|
|
$lastName,
|
|
$profile->getId(),
|
|
$role,
|
|
));
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
}
|