first commit

This commit is contained in:
julien
2026-03-16 01:47:07 +01:00
commit 8f7e61bda0
185 changed files with 27731 additions and 0 deletions

234
src/User/UserController.php Normal file
View File

@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace App\User;
use App\Shared\Http\FlashServiceInterface;
use App\Shared\Http\SessionManagerInterface;
use App\User\Exception\DuplicateEmailException;
use App\User\Exception\DuplicateUsernameException;
use App\User\Exception\InvalidRoleException;
use App\User\Exception\WeakPasswordException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
/**
* Contrôleur pour la gestion des utilisateurs en administration.
*
* Accessible uniquement aux administrateurs (AdminMiddleware).
* Gère la liste, la création, la modification de rôle et la suppression des comptes.
* Toute la logique de persistance est déléguée à UserService.
*
* Règles de protection communes :
* - Le compte administrateur (role = 'admin') ne peut pas être supprimé ni rétrogradé
* - Un administrateur ne peut pas supprimer son propre compte ni changer son propre rôle
*/
final class UserController
{
/**
* @param Twig $view Moteur de templates Twig
* @param UserServiceInterface $userService Service de gestion des utilisateurs
* @param FlashServiceInterface $flash Service de messages flash
* @param SessionManagerInterface $sessionManager Gestionnaire de session
*/
public function __construct(
private readonly Twig $view,
private readonly UserServiceInterface $userService,
private readonly FlashServiceInterface $flash,
private readonly SessionManagerInterface $sessionManager,
) {
}
/**
* Affiche la liste de tous les utilisateurs.
*
* Passe l'identifiant de l'utilisateur courant à la vue
* pour conditionner l'affichage du bouton de suppression.
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
*
* @return Response La vue admin/users/index.twig
*/
public function index(Request $req, Response $res): Response
{
return $this->view->render($res, 'admin/users/index.twig', [
'users' => $this->userService->findAll(),
'currentUserId' => $this->sessionManager->getUserId(),
'error' => $this->flash->get('user_error'),
'success' => $this->flash->get('user_success'),
]);
}
/**
* Affiche le formulaire de création d'un utilisateur.
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
*
* @return Response La vue admin/users/form.twig
*/
public function showCreate(Request $req, Response $res): Response
{
return $this->view->render($res, 'admin/users/form.twig', [
'error' => $this->flash->get('user_error'),
]);
}
/**
* Traite la soumission du formulaire de création d'utilisateur.
*
* Vérifie que les mots de passe correspondent avant de déléguer
* la création à UserService. En cas d'erreur, redirige vers le
* formulaire avec un message flash.
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
*
* @return Response Redirection vers /admin/users en cas de succès,
* ou vers /admin/users/create en cas d'erreur
*/
public function create(Request $req, Response $res): Response
{
/** @var array<string, mixed> $data */
$data = (array) $req->getParsedBody();
$username = trim((string) ($data['username'] ?? ''));
$email = trim((string) ($data['email'] ?? ''));
$password = trim((string) ($data['password'] ?? ''));
$confirm = trim((string) ($data['password_confirm'] ?? ''));
// Restreindre les rôles assignables depuis le formulaire.
// Le rôle 'admin' est exclu : il ne peut être attribué que directement
// en base de données, pour éviter qu'un admin ne crée d'autres admins
// en manipulant la requête HTTP.
$allowedRoles = [User::ROLE_USER, User::ROLE_EDITOR];
$rawRole = trim((string) ($data['role'] ?? ''));
$role = in_array($rawRole, $allowedRoles, true)
? $rawRole
: User::ROLE_USER;
if ($password !== $confirm) {
$this->flash->set('user_error', 'Les mots de passe ne correspondent pas');
return $res->withHeader('Location', '/admin/users/create')->withStatus(302);
}
try {
$this->userService->createUser($username, $email, $password, $role);
$this->flash->set('user_success', "L'utilisateur « {$username} » a été créé avec succès");
return $res->withHeader('Location', '/admin/users')->withStatus(302);
} catch (DuplicateUsernameException) {
$this->flash->set('user_error', "Ce nom d'utilisateur est déjà pris");
} catch (DuplicateEmailException) {
$this->flash->set('user_error', 'Cette adresse e-mail est déjà utilisée');
} catch (WeakPasswordException) {
$this->flash->set('user_error', 'Le mot de passe doit contenir au moins 8 caractères');
} catch (InvalidRoleException $e) {
$this->flash->set('user_error', $e->getMessage());
} catch (\Throwable) {
$this->flash->set('user_error', "Une erreur inattendue s'est produite");
}
return $res->withHeader('Location', '/admin/users/create')->withStatus(302);
}
/**
* Met à jour le rôle d'un utilisateur.
*
* La modification est refusée dans trois cas :
* - l'utilisateur cible est introuvable
* - l'administrateur connecté tente de modifier son propre rôle
* - l'utilisateur cible est déjà administrateur
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
* @param array<string, string> $args Les paramètres de route (id)
*
* @return Response Redirection vers /admin/users dans tous les cas
*/
public function updateRole(Request $req, Response $res, array $args): Response
{
$id = (int) $args['id'];
$user = $this->userService->findById($id);
if ($user === null) {
$this->flash->set('user_error', 'Utilisateur introuvable');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
if ($id === $this->sessionManager->getUserId()) {
$this->flash->set('user_error', 'Vous ne pouvez pas modifier votre propre rôle');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
if ($user->isAdmin()) {
$this->flash->set('user_error', 'Le rôle d\'un administrateur ne peut pas être modifié');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
$allowedRoles = [User::ROLE_USER, User::ROLE_EDITOR, User::ROLE_ADMIN];
/** @var array<string, mixed> $body */
$body = (array) $req->getParsedBody();
$rawRole = trim((string) ($body['role'] ?? ''));
$role = in_array($rawRole, $allowedRoles, true) ? $rawRole : null;
if ($role === null) {
$this->flash->set('user_error', 'Rôle invalide');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
$this->userService->updateRole($id, $role);
$this->flash->set('user_success', "Le rôle de « {$user->getUsername()} » a été mis à jour");
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
/**
* Supprime un utilisateur.
*
* La suppression est refusée dans trois cas :
* - l'utilisateur cible est introuvable
* - l'utilisateur cible est administrateur (role = 'admin')
* - l'administrateur connecté tente de supprimer son propre compte
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
* @param array<string, string> $args Les paramètres de route (id)
*
* @return Response Redirection vers /admin/users dans tous les cas
*/
public function delete(Request $req, Response $res, array $args): Response
{
$id = (int) $args['id'];
$user = $this->userService->findById($id);
if ($user === null) {
$this->flash->set('user_error', 'Utilisateur introuvable');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
if ($user->isAdmin()) {
$this->flash->set('user_error', 'Le compte administrateur ne peut pas être supprimé');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
if ($id === $this->sessionManager->getUserId()) {
$this->flash->set('user_error', 'Vous ne pouvez pas supprimer votre propre compte');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
$this->userService->delete($id);
$this->flash->set('user_success', "L'utilisateur « {$user->getUsername()} » a été supprimé avec succès");
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
}