first commit
This commit is contained in:
98
src/Identity/UI/Http/AccountController.php
Normal file
98
src/Identity/UI/Http/AccountController.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http;
|
||||
|
||||
use Netig\Netslim\Identity\Application\AuthServiceInterface;
|
||||
use Netig\Netslim\Identity\Domain\Exception\WeakPasswordException;
|
||||
use Netig\Netslim\Identity\UI\Http\Request\ChangePasswordRequest;
|
||||
use Netig\Netslim\Kernel\Http\Application\Flash\FlashServiceInterface;
|
||||
use Netig\Netslim\Kernel\Http\Application\Session\SessionManagerInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
/**
|
||||
* Gère les actions liées au compte courant, notamment le changement de mot de passe.
|
||||
*/
|
||||
class AccountController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Twig $view,
|
||||
private readonly AuthServiceInterface $authService,
|
||||
private readonly FlashServiceInterface $flash,
|
||||
private readonly SessionManagerInterface $sessionManager,
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
public function showChangePassword(Request $req, Response $res): Response
|
||||
{
|
||||
$referer = $req->getHeaderLine('Referer');
|
||||
$path = parse_url($referer, PHP_URL_PATH);
|
||||
$path = is_string($path) ? $path : '';
|
||||
$backUrl = (str_starts_with($path, '/') && $path !== '/account/password')
|
||||
? $path
|
||||
: AdminHomePath::resolve();
|
||||
|
||||
return $this->view->render($res, '@Identity/account/password-change.twig', [
|
||||
'error' => $this->flash->get('password_error'),
|
||||
'success' => $this->flash->get('password_success'),
|
||||
'back_url' => $backUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide puis applique le changement de mot de passe pour l'utilisateur connecté.
|
||||
*/
|
||||
public function changePassword(Request $req, Response $res): Response
|
||||
{
|
||||
$changePasswordRequest = ChangePasswordRequest::fromRequest($req);
|
||||
$userId = $this->sessionManager->getUserId() ?? 0;
|
||||
|
||||
try {
|
||||
$changePasswordRequest->ensureConfirmed();
|
||||
$this->authService->changePassword(
|
||||
$userId,
|
||||
$changePasswordRequest->currentPassword,
|
||||
$changePasswordRequest->newPassword,
|
||||
);
|
||||
$this->flash->set('password_success', 'Mot de passe modifié avec succès');
|
||||
} catch (WeakPasswordException $e) {
|
||||
$this->flash->set('password_error', $e->getMessage());
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$message = $e->getMessage();
|
||||
if ($message === 'Mot de passe actuel incorrect') {
|
||||
$message = 'Le mot de passe actuel est incorrect';
|
||||
}
|
||||
$this->flash->set('password_error', $message);
|
||||
} catch (\Throwable $e) {
|
||||
$incidentId = $this->logUnexpectedError($req, $e, [
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
$this->flash->set('password_error', "Une erreur inattendue s\'est produite (réf. {$incidentId})");
|
||||
}
|
||||
|
||||
return $res->withHeader('Location', '/account/password')->withStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private function logUnexpectedError(Request $req, \Throwable $e, array $context = []): string
|
||||
{
|
||||
$incidentId = bin2hex(random_bytes(8));
|
||||
|
||||
$this->logger?->error('Account password change failed', $context + [
|
||||
'incident_id' => $incidentId,
|
||||
'route' => (string) $req->getUri()->getPath(),
|
||||
'method' => $req->getMethod(),
|
||||
'exception_class' => $e::class,
|
||||
'exception_message' => $e->getMessage(),
|
||||
'exception' => $e,
|
||||
]);
|
||||
|
||||
return $incidentId;
|
||||
}
|
||||
}
|
||||
25
src/Identity/UI/Http/AdminHomePath.php
Normal file
25
src/Identity/UI/Http/AdminHomePath.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http;
|
||||
|
||||
/**
|
||||
* Résout la destination d'atterrissage du back-office pour le domaine identité.
|
||||
*
|
||||
* Le socle ne suppose pas qu'un module éditorial soit présent : l'application
|
||||
* consommatrice peut donc surcharger cette destination via ADMIN_HOME_PATH.
|
||||
*/
|
||||
final class AdminHomePath
|
||||
{
|
||||
public static function resolve(): string
|
||||
{
|
||||
$path = trim((string) ($_ENV['ADMIN_HOME_PATH'] ?? '/admin'));
|
||||
|
||||
if ($path === '') {
|
||||
return '/admin';
|
||||
}
|
||||
|
||||
return '/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
114
src/Identity/UI/Http/AuthController.php
Normal file
114
src/Identity/UI/Http/AuthController.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http;
|
||||
|
||||
use Netig\Netslim\Identity\Application\AuthServiceInterface;
|
||||
use Netig\Netslim\Identity\UI\Http\Request\LoginRequest;
|
||||
use Netig\Netslim\Kernel\Http\Application\Flash\FlashServiceInterface;
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Request\ClientIpResolver;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
/**
|
||||
* Contrôleur HTTP responsable de l'ouverture et de la fermeture de session.
|
||||
*/
|
||||
class AuthController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Twig $view,
|
||||
private readonly AuthServiceInterface $authService,
|
||||
private readonly FlashServiceInterface $flash,
|
||||
private readonly ClientIpResolver $clientIpResolver,
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
public function showLogin(Request $req, Response $res): Response
|
||||
{
|
||||
if ($this->authService->isLoggedIn()) {
|
||||
return $res->withHeader('Location', AdminHomePath::resolve())->withStatus(302);
|
||||
}
|
||||
|
||||
return $this->view->render($res, '@Identity/login.twig', [
|
||||
'error' => $this->flash->get('login_error'),
|
||||
'success' => $this->flash->get('login_success'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite la soumission du formulaire de connexion et applique la limitation de débit.
|
||||
*/
|
||||
public function login(Request $req, Response $res): Response
|
||||
{
|
||||
$ip = $this->clientIpResolver->resolve($req);
|
||||
|
||||
try {
|
||||
$remainingMinutes = $this->authService->checkRateLimit($ip);
|
||||
|
||||
if ($remainingMinutes > 0) {
|
||||
$this->flash->set(
|
||||
'login_error',
|
||||
"Trop de tentatives. Réessayez dans {$remainingMinutes} minute"
|
||||
. ($remainingMinutes > 1 ? 's' : ''),
|
||||
);
|
||||
|
||||
return $res->withHeader('Location', '/auth/login')->withStatus(302);
|
||||
}
|
||||
|
||||
$loginRequest = LoginRequest::fromRequest($req);
|
||||
$user = $this->authService->authenticate($loginRequest->username, $loginRequest->password);
|
||||
|
||||
if ($user === null) {
|
||||
$this->authService->recordFailure($ip);
|
||||
$this->flash->set('login_error', 'Identifiants invalides');
|
||||
|
||||
return $res->withHeader('Location', '/auth/login')->withStatus(302);
|
||||
}
|
||||
|
||||
$this->authService->resetRateLimit($ip);
|
||||
$this->authService->login($user);
|
||||
} catch (\Throwable $e) {
|
||||
$incidentId = $this->logUnexpectedError($req, $e, [
|
||||
'ip' => $ip,
|
||||
]);
|
||||
$this->flash->set('login_error', "Une erreur inattendue s'est produite (réf. {$incidentId})");
|
||||
|
||||
return $res->withHeader('Location', '/auth/login')->withStatus(302);
|
||||
}
|
||||
|
||||
return $res->withHeader('Location', AdminHomePath::resolve())->withStatus(302);
|
||||
}
|
||||
|
||||
public function logout(Request $req, Response $res): Response
|
||||
{
|
||||
try {
|
||||
$this->authService->logout();
|
||||
} catch (\Throwable $e) {
|
||||
$this->logUnexpectedError($req, $e);
|
||||
}
|
||||
|
||||
return $res->withHeader('Location', '/')->withStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private function logUnexpectedError(Request $req, \Throwable $e, array $context = []): string
|
||||
{
|
||||
$incidentId = bin2hex(random_bytes(8));
|
||||
|
||||
$this->logger?->error('Authentication flow failed', $context + [
|
||||
'incident_id' => $incidentId,
|
||||
'route' => (string) $req->getUri()->getPath(),
|
||||
'method' => $req->getMethod(),
|
||||
'exception_class' => $e::class,
|
||||
'exception_message' => $e->getMessage(),
|
||||
'exception' => $e,
|
||||
]);
|
||||
|
||||
return $incidentId;
|
||||
}
|
||||
}
|
||||
33
src/Identity/UI/Http/AuthRoutes.php
Normal file
33
src/Identity/UI/Http/AuthRoutes.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http;
|
||||
|
||||
use Netig\Netslim\Identity\UI\Http\Middleware\AuthMiddleware;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Slim\App;
|
||||
|
||||
/**
|
||||
* Enregistre les routes HTTP du sous-domaine authentification.
|
||||
*/
|
||||
final class AuthRoutes
|
||||
{
|
||||
/** @param App<ContainerInterface> $app */
|
||||
public static function register(App $app): void
|
||||
{
|
||||
$app->get('/auth/login', [AuthController::class, 'showLogin']);
|
||||
$app->post('/auth/login', [AuthController::class, 'login']);
|
||||
$app->post('/auth/logout', [AuthController::class, 'logout']);
|
||||
|
||||
$app->get('/password/forgot', [PasswordResetController::class, 'showForgot']);
|
||||
$app->post('/password/forgot', [PasswordResetController::class, 'forgot']);
|
||||
$app->get('/password/reset', [PasswordResetController::class, 'showReset']);
|
||||
$app->post('/password/reset', [PasswordResetController::class, 'reset']);
|
||||
|
||||
$app->group('/account', function ($group) {
|
||||
$group->get('/password', [AccountController::class, 'showChangePassword']);
|
||||
$group->post('/password', [AccountController::class, 'changePassword']);
|
||||
})->add(AuthMiddleware::class);
|
||||
}
|
||||
}
|
||||
50
src/Identity/UI/Http/Middleware/AdminMiddleware.php
Normal file
50
src/Identity/UI/Http/Middleware/AdminMiddleware.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http\Middleware;
|
||||
|
||||
use Netig\Netslim\Identity\UI\Http\AdminHomePath;
|
||||
use Netig\Netslim\Kernel\Http\Application\Session\SessionManagerInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||
use Slim\Psr7\Response as SlimResponse;
|
||||
|
||||
/**
|
||||
* Middleware de protection des routes réservées aux administrateurs.
|
||||
*
|
||||
* Intercepte les requêtes et redirige vers la page d'accueil du back-office si l'utilisateur
|
||||
* connecté n'a pas le rôle 'admin'.
|
||||
*
|
||||
* Ce middleware doit être utilisé en complément de AuthMiddleware,
|
||||
* qui vérifie en amont que l'utilisateur est connecté.
|
||||
* Ordre dans la chaîne Slim : ->add($adminMiddleware)->add($authMiddleware)
|
||||
*/
|
||||
final class AdminMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @param SessionManagerInterface $sessionManager Gestionnaire de session (lecture du rôle admin)
|
||||
*/
|
||||
public function __construct(private readonly SessionManagerInterface $sessionManager) {}
|
||||
|
||||
/**
|
||||
* Vérifie le rôle admin avant de transmettre la requête au gestionnaire suivant.
|
||||
*
|
||||
* @param Request $request La requête HTTP entrante
|
||||
* @param RequestHandler $handler Le gestionnaire suivant dans la chaîne de middlewares
|
||||
*
|
||||
* @return Response Une redirection 302 vers la page d'accueil du back-office, ou la réponse normale
|
||||
*/
|
||||
public function process(Request $request, RequestHandler $handler): Response
|
||||
{
|
||||
if (!$this->sessionManager->isAdmin()) {
|
||||
return (new SlimResponse())
|
||||
->withHeader('Location', AdminHomePath::resolve())
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
80
src/Identity/UI/Http/Middleware/AuthMiddleware.php
Normal file
80
src/Identity/UI/Http/Middleware/AuthMiddleware.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http\Middleware;
|
||||
|
||||
use Netig\Netslim\Identity\Domain\Repository\UserRepositoryInterface;
|
||||
use Netig\Netslim\Kernel\Http\Application\Session\SessionManagerInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||
use Slim\Psr7\Response as SlimResponse;
|
||||
|
||||
/**
|
||||
* Middleware de protection des routes réservées aux utilisateurs connectés.
|
||||
*
|
||||
* Intercepte les requêtes entrantes et redirige vers /auth/login
|
||||
* si aucune session utilisateur n'est active.
|
||||
* Doit être appliqué avant AdminMiddleware dans la chaîne.
|
||||
*/
|
||||
final class AuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @param SessionManagerInterface $sessionManager Gestionnaire de session (vérification de l'authentification)
|
||||
* @param UserRepositoryInterface|null $userRepository Dépôt pour valider la version de session
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly SessionManagerInterface $sessionManager,
|
||||
private readonly ?UserRepositoryInterface $userRepository = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Vérifie l'authentification avant de transmettre la requête au gestionnaire suivant.
|
||||
*
|
||||
* @param Request $request La requête HTTP entrante
|
||||
* @param RequestHandler $handler Le gestionnaire suivant dans la chaîne de middlewares
|
||||
*
|
||||
* @return Response Une redirection 302 vers /auth/login, ou la réponse normale
|
||||
*/
|
||||
public function process(Request $request, RequestHandler $handler): Response
|
||||
{
|
||||
if (!$this->sessionManager->isAuthenticated()) {
|
||||
return $this->redirectToLogin();
|
||||
}
|
||||
|
||||
if (!$this->isSessionStillValid()) {
|
||||
$this->sessionManager->destroy();
|
||||
|
||||
return $this->redirectToLogin();
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
private function isSessionStillValid(): bool
|
||||
{
|
||||
if ($this->userRepository === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$userId = $this->sessionManager->getUserId();
|
||||
$sessionVersion = $this->sessionManager->getSessionVersion();
|
||||
|
||||
if ($userId === null || $sessionVersion === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $this->userRepository->findById($userId);
|
||||
|
||||
return $user !== null && $user->getSessionVersion() === $sessionVersion;
|
||||
}
|
||||
|
||||
private function redirectToLogin(): Response
|
||||
{
|
||||
return (new SlimResponse())
|
||||
->withHeader('Location', '/auth/login')
|
||||
->withStatus(302);
|
||||
}
|
||||
}
|
||||
50
src/Identity/UI/Http/Middleware/EditorMiddleware.php
Normal file
50
src/Identity/UI/Http/Middleware/EditorMiddleware.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http\Middleware;
|
||||
|
||||
use Netig\Netslim\Identity\UI\Http\AdminHomePath;
|
||||
use Netig\Netslim\Kernel\Http\Application\Session\SessionManagerInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||
use Slim\Psr7\Response as SlimResponse;
|
||||
|
||||
/**
|
||||
* Middleware de protection des routes réservées aux éditeurs et administrateurs.
|
||||
*
|
||||
* Intercepte les requêtes et redirige vers la page d'accueil du back-office si l'utilisateur
|
||||
* connecté n'a ni le rôle 'editor' ni le rôle 'admin'.
|
||||
*
|
||||
* Ce middleware doit être utilisé en complément de AuthMiddleware,
|
||||
* qui vérifie en amont que l'utilisateur est connecté.
|
||||
* Ordre dans la chaîne Slim : ->add($editorMiddleware)->add($authMiddleware)
|
||||
*/
|
||||
final class EditorMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @param SessionManagerInterface $sessionManager Gestionnaire de session (lecture du rôle)
|
||||
*/
|
||||
public function __construct(private readonly SessionManagerInterface $sessionManager) {}
|
||||
|
||||
/**
|
||||
* Vérifie le rôle (editor ou admin) avant de transmettre la requête au gestionnaire suivant.
|
||||
*
|
||||
* @param Request $request La requête HTTP entrante
|
||||
* @param RequestHandler $handler Le gestionnaire suivant dans la chaîne de middlewares
|
||||
*
|
||||
* @return Response Une redirection 302 vers la page d'accueil du back-office, ou la réponse normale
|
||||
*/
|
||||
public function process(Request $request, RequestHandler $handler): Response
|
||||
{
|
||||
if (!$this->sessionManager->isAdmin() && !$this->sessionManager->isEditor()) {
|
||||
return (new SlimResponse())
|
||||
->withHeader('Location', AdminHomePath::resolve())
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
174
src/Identity/UI/Http/PasswordResetController.php
Normal file
174
src/Identity/UI/Http/PasswordResetController.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http;
|
||||
|
||||
use Netig\Netslim\Identity\Application\AuthServiceInterface;
|
||||
use Netig\Netslim\Identity\Application\PasswordResetServiceInterface;
|
||||
use Netig\Netslim\Identity\Domain\Exception\InvalidResetTokenException;
|
||||
use Netig\Netslim\Identity\Domain\Exception\WeakPasswordException;
|
||||
use Netig\Netslim\Identity\UI\Http\Request\ForgotPasswordRequest;
|
||||
use Netig\Netslim\Identity\UI\Http\Request\ResetPasswordRequest;
|
||||
use Netig\Netslim\Kernel\Http\Application\Flash\FlashServiceInterface;
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Request\ClientIpResolver;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
/**
|
||||
* Expose les écrans et traitements HTTP du parcours de réinitialisation de mot de passe.
|
||||
*/
|
||||
class PasswordResetController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Twig $view,
|
||||
private readonly PasswordResetServiceInterface $passwordResetService,
|
||||
private readonly AuthServiceInterface $authService,
|
||||
private readonly FlashServiceInterface $flash,
|
||||
private readonly ClientIpResolver $clientIpResolver,
|
||||
private readonly string $baseUrl,
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
public function showForgot(Request $req, Response $res): Response
|
||||
{
|
||||
return $this->view->render($res, '@Identity/password-forgot.twig', [
|
||||
'error' => $this->flash->get('reset_error'),
|
||||
'success' => $this->flash->get('reset_success'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function forgot(Request $req, Response $res): Response
|
||||
{
|
||||
$ip = $this->clientIpResolver->resolve($req);
|
||||
$remainingMinutes = $this->authService->checkPasswordResetRateLimit($ip);
|
||||
|
||||
if ($remainingMinutes > 0) {
|
||||
$this->flash->set(
|
||||
'reset_error',
|
||||
"Trop de demandes. Veuillez réessayer dans {$remainingMinutes} minute"
|
||||
. ($remainingMinutes > 1 ? 's' : ''),
|
||||
);
|
||||
|
||||
return $res->withHeader('Location', '/password/forgot')->withStatus(302);
|
||||
}
|
||||
|
||||
$forgotPasswordRequest = ForgotPasswordRequest::fromRequest($req);
|
||||
$this->authService->recordPasswordResetAttempt($ip);
|
||||
|
||||
try {
|
||||
$this->passwordResetService->requestReset($forgotPasswordRequest->email, $this->baseUrl);
|
||||
} catch (\RuntimeException $e) {
|
||||
$incidentId = $this->logUnexpectedError(
|
||||
$req,
|
||||
$e,
|
||||
'Password reset request failed',
|
||||
[
|
||||
'ip' => $ip,
|
||||
'email_hash' => $forgotPasswordRequest->email === '' ? null : hash('sha256', mb_strtolower(trim($forgotPasswordRequest->email))),
|
||||
],
|
||||
);
|
||||
$this->flash->set('reset_error', "Une erreur est survenue. Veuillez réessayer (réf. {$incidentId}).");
|
||||
|
||||
return $res->withHeader('Location', '/password/forgot')->withStatus(302);
|
||||
}
|
||||
|
||||
$this->flash->set(
|
||||
'reset_success',
|
||||
'Si cette adresse est associée à un compte, un email de réinitialisation a été envoyé',
|
||||
);
|
||||
|
||||
return $res->withHeader('Location', '/password/forgot')->withStatus(302);
|
||||
}
|
||||
|
||||
public function showReset(Request $req, Response $res): Response
|
||||
{
|
||||
$token = trim((string) ($req->getQueryParams()['token'] ?? ''));
|
||||
|
||||
if ($token === '') {
|
||||
$this->flash->set('reset_error', 'Lien de réinitialisation manquant');
|
||||
|
||||
return $res->withHeader('Location', '/password/forgot')->withStatus(302);
|
||||
}
|
||||
|
||||
$user = $this->passwordResetService->validateToken($token);
|
||||
|
||||
if ($user === null) {
|
||||
$this->flash->set('reset_error', 'Ce lien est invalide ou a expiré. Veuillez faire une nouvelle demande.');
|
||||
|
||||
return $res->withHeader('Location', '/password/forgot')->withStatus(302);
|
||||
}
|
||||
|
||||
return $this->view->render($res, '@Identity/password-reset.twig', [
|
||||
'token' => $token,
|
||||
'error' => $this->flash->get('reset_error'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de consommer un jeton de réinitialisation et redirige avec le message adapté.
|
||||
*/
|
||||
public function reset(Request $req, Response $res): Response
|
||||
{
|
||||
$resetPasswordRequest = ResetPasswordRequest::fromRequest($req);
|
||||
|
||||
try {
|
||||
$resetPasswordRequest->ensureTokenPresent();
|
||||
$resetPasswordRequest->ensureConfirmed();
|
||||
$this->passwordResetService->resetPassword($resetPasswordRequest->token, $resetPasswordRequest->newPassword);
|
||||
} catch (WeakPasswordException $e) {
|
||||
$this->flash->set('reset_error', $e->getMessage());
|
||||
|
||||
return $res->withHeader('Location', '/password/reset?token=' . urlencode($resetPasswordRequest->token))->withStatus(302);
|
||||
} catch (InvalidResetTokenException) {
|
||||
$this->flash->set('reset_error', 'Ce lien de réinitialisation est invalide ou a expiré');
|
||||
|
||||
return $res->withHeader('Location', '/password/reset?token=' . urlencode($resetPasswordRequest->token))->withStatus(302);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->flash->set('reset_error', $e->getMessage());
|
||||
|
||||
$location = $resetPasswordRequest->token === ''
|
||||
? '/password/forgot'
|
||||
: '/password/reset?token=' . urlencode($resetPasswordRequest->token);
|
||||
|
||||
return $res->withHeader('Location', $location)->withStatus(302);
|
||||
} catch (\Throwable $e) {
|
||||
$incidentId = $this->logUnexpectedError(
|
||||
$req,
|
||||
$e,
|
||||
'Password reset failed',
|
||||
[
|
||||
'token_present' => $resetPasswordRequest->token !== '',
|
||||
],
|
||||
);
|
||||
$this->flash->set('reset_error', "Une erreur inattendue s\'est produite (réf. {$incidentId})");
|
||||
|
||||
return $res->withHeader('Location', '/password/reset?token=' . urlencode($resetPasswordRequest->token))->withStatus(302);
|
||||
}
|
||||
|
||||
$this->flash->set('login_success', 'Mot de passe réinitialisé avec succès. Vous pouvez vous connecter');
|
||||
|
||||
return $res->withHeader('Location', '/auth/login')->withStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private function logUnexpectedError(Request $req, \Throwable $e, string $message, array $context = []): string
|
||||
{
|
||||
$incidentId = bin2hex(random_bytes(8));
|
||||
|
||||
$this->logger?->error($message, $context + [
|
||||
'incident_id' => $incidentId,
|
||||
'route' => (string) $req->getUri()->getPath(),
|
||||
'method' => $req->getMethod(),
|
||||
'exception_class' => $e::class,
|
||||
'exception_message' => $e->getMessage(),
|
||||
'exception' => $e,
|
||||
]);
|
||||
|
||||
return $incidentId;
|
||||
}
|
||||
}
|
||||
43
src/Identity/UI/Http/Request/ChangePasswordRequest.php
Normal file
43
src/Identity/UI/Http/Request/ChangePasswordRequest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http\Request;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Porte les champs nécessaires au changement de mot de passe du compte courant.
|
||||
*/
|
||||
final readonly class ChangePasswordRequest
|
||||
{
|
||||
public function __construct(
|
||||
public string $currentPassword,
|
||||
public string $newPassword,
|
||||
public string $newPasswordConfirm,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(ServerRequestInterface $request): self
|
||||
{
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = (array) $request->getParsedBody();
|
||||
|
||||
return new self(
|
||||
currentPassword: (string) ($data['current_password'] ?? ''),
|
||||
newPassword: (string) ($data['new_password'] ?? ''),
|
||||
newPasswordConfirm: (string) ($data['new_password_confirm'] ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que la confirmation correspond bien au nouveau mot de passe demandé.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function ensureConfirmed(): void
|
||||
{
|
||||
if ($this->newPassword !== $this->newPasswordConfirm) {
|
||||
throw new \InvalidArgumentException('Les mots de passe ne correspondent pas');
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/Identity/UI/Http/Request/CreateUserRequest.php
Normal file
44
src/Identity/UI/Http/Request/CreateUserRequest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http\Request;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Porte les champs utiles à la création d'un utilisateur depuis l'interface admin.
|
||||
*/
|
||||
final readonly class CreateUserRequest
|
||||
{
|
||||
public function __construct(
|
||||
public string $username,
|
||||
public string $email,
|
||||
public string $password,
|
||||
public string $passwordConfirm,
|
||||
public string $role,
|
||||
) {}
|
||||
|
||||
/** @param string[] $assignableRoles */
|
||||
public static function fromRequest(ServerRequestInterface $request, array $assignableRoles): self
|
||||
{
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = (array) $request->getParsedBody();
|
||||
$rawRole = trim((string) ($data['role'] ?? ''));
|
||||
|
||||
return new self(
|
||||
username: trim((string) ($data['username'] ?? '')),
|
||||
email: trim((string) ($data['email'] ?? '')),
|
||||
password: (string) ($data['password'] ?? ''),
|
||||
passwordConfirm: (string) ($data['password_confirm'] ?? ''),
|
||||
role: in_array($rawRole, $assignableRoles, true) ? $rawRole : 'user',
|
||||
);
|
||||
}
|
||||
|
||||
public function ensureConfirmed(): void
|
||||
{
|
||||
if ($this->password !== $this->passwordConfirm) {
|
||||
throw new \InvalidArgumentException('Les mots de passe ne correspondent pas');
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Identity/UI/Http/Request/ForgotPasswordRequest.php
Normal file
23
src/Identity/UI/Http/Request/ForgotPasswordRequest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http\Request;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Représente la demande de réinitialisation de mot de passe.
|
||||
*/
|
||||
final readonly class ForgotPasswordRequest
|
||||
{
|
||||
public function __construct(public string $email) {}
|
||||
|
||||
public static function fromRequest(ServerRequestInterface $request): self
|
||||
{
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = (array) $request->getParsedBody();
|
||||
|
||||
return new self(trim((string) ($data['email'] ?? '')));
|
||||
}
|
||||
}
|
||||
29
src/Identity/UI/Http/Request/LoginRequest.php
Normal file
29
src/Identity/UI/Http/Request/LoginRequest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http\Request;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Représente les champs utiles du formulaire de connexion.
|
||||
*/
|
||||
final readonly class LoginRequest
|
||||
{
|
||||
public function __construct(
|
||||
public string $username,
|
||||
public string $password,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(ServerRequestInterface $request): self
|
||||
{
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = (array) $request->getParsedBody();
|
||||
|
||||
return new self(
|
||||
username: trim((string) ($data['username'] ?? '')),
|
||||
password: (string) ($data['password'] ?? ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
src/Identity/UI/Http/Request/ResetPasswordRequest.php
Normal file
55
src/Identity/UI/Http/Request/ResetPasswordRequest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http\Request;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Porte le jeton et les nouveaux secrets envoyés lors d'une réinitialisation.
|
||||
*/
|
||||
final readonly class ResetPasswordRequest
|
||||
{
|
||||
public function __construct(
|
||||
public string $token,
|
||||
public string $newPassword,
|
||||
public string $newPasswordConfirm,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(ServerRequestInterface $request): self
|
||||
{
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = (array) $request->getParsedBody();
|
||||
|
||||
return new self(
|
||||
token: trim((string) ($data['token'] ?? '')),
|
||||
newPassword: (string) ($data['new_password'] ?? ''),
|
||||
newPasswordConfirm: (string) ($data['new_password_confirm'] ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie qu'un jeton de réinitialisation est bien présent dans la requête.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function ensureTokenPresent(): void
|
||||
{
|
||||
if ($this->token === '') {
|
||||
throw new \InvalidArgumentException('Lien de réinitialisation manquant');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que le mot de passe saisi et sa confirmation correspondent.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function ensureConfirmed(): void
|
||||
{
|
||||
if ($this->newPassword !== $this->newPasswordConfirm) {
|
||||
throw new \InvalidArgumentException('Les mots de passe ne correspondent pas');
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Identity/UI/Http/Request/UpdateUserRoleRequest.php
Normal file
36
src/Identity/UI/Http/Request/UpdateUserRoleRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http\Request;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Porte le rôle cible demandé depuis l'interface d'administration.
|
||||
*/
|
||||
final readonly class UpdateUserRoleRequest
|
||||
{
|
||||
public function __construct(public ?string $role) {}
|
||||
|
||||
/** @param string[] $assignableRoles */
|
||||
public static function fromRequest(ServerRequestInterface $request, array $assignableRoles): self
|
||||
{
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = (array) $request->getParsedBody();
|
||||
$rawRole = trim((string) ($data['role'] ?? ''));
|
||||
|
||||
return new self(
|
||||
role: in_array($rawRole, $assignableRoles, true) ? $rawRole : null,
|
||||
);
|
||||
}
|
||||
|
||||
public function requireRole(): string
|
||||
{
|
||||
if ($this->role === null) {
|
||||
throw new \InvalidArgumentException('Rôle invalide');
|
||||
}
|
||||
|
||||
return $this->role;
|
||||
}
|
||||
}
|
||||
202
src/Identity/UI/Http/UserController.php
Normal file
202
src/Identity/UI/Http/UserController.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http;
|
||||
|
||||
use Netig\Netslim\Identity\Application\UserServiceInterface;
|
||||
use Netig\Netslim\Identity\Domain\Exception\DuplicateEmailException;
|
||||
use Netig\Netslim\Identity\Domain\Exception\DuplicateUsernameException;
|
||||
use Netig\Netslim\Identity\Domain\Exception\InvalidRoleException;
|
||||
use Netig\Netslim\Identity\Domain\Exception\RoleAssignmentNotAllowedException;
|
||||
use Netig\Netslim\Identity\Domain\Exception\WeakPasswordException;
|
||||
use Netig\Netslim\Identity\Domain\Policy\RolePolicy;
|
||||
use Netig\Netslim\Identity\UI\Http\Request\CreateUserRequest;
|
||||
use Netig\Netslim\Identity\UI\Http\Request\UpdateUserRoleRequest;
|
||||
use Netig\Netslim\Kernel\Http\Application\Flash\FlashServiceInterface;
|
||||
use Netig\Netslim\Kernel\Http\Application\Session\SessionManagerInterface;
|
||||
use Netig\Netslim\Kernel\Pagination\Infrastructure\PaginationPresenter;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
/**
|
||||
* Contrôleur HTTP des écrans d'administration des utilisateurs.
|
||||
*/
|
||||
class UserController
|
||||
{
|
||||
private const PER_PAGE = 15;
|
||||
|
||||
private readonly RolePolicy $rolePolicy;
|
||||
|
||||
public function __construct(
|
||||
private readonly Twig $view,
|
||||
private readonly UserServiceInterface $userService,
|
||||
private readonly FlashServiceInterface $flash,
|
||||
private readonly SessionManagerInterface $sessionManager,
|
||||
RolePolicy $rolePolicy,
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {
|
||||
$this->rolePolicy = $rolePolicy;
|
||||
}
|
||||
|
||||
public function index(Request $req, Response $res): Response
|
||||
{
|
||||
$page = PaginationPresenter::resolvePage($req->getQueryParams());
|
||||
$paginated = $this->userService->findPaginated($page, self::PER_PAGE);
|
||||
|
||||
return $this->view->render($res, '@Identity/admin/index.twig', [
|
||||
'users' => $paginated->getItems(),
|
||||
'pagination' => PaginationPresenter::fromRequest($req, $paginated),
|
||||
'currentUserId' => $this->sessionManager->getUserId(),
|
||||
'assignableRoles' => $this->rolePolicy->assignableRoles(),
|
||||
'error' => $this->flash->get('user_error'),
|
||||
'success' => $this->flash->get('user_success'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function showCreate(Request $req, Response $res): Response
|
||||
{
|
||||
return $this->view->render($res, '@Identity/admin/form.twig', [
|
||||
'assignableRoles' => $this->rolePolicy->assignableRoles(),
|
||||
'error' => $this->flash->get('user_error'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $req, Response $res): Response
|
||||
{
|
||||
$createRequest = CreateUserRequest::fromRequest($req, $this->rolePolicy->assignableRoles());
|
||||
|
||||
try {
|
||||
$createRequest->ensureConfirmed();
|
||||
$this->userService->create(
|
||||
$createRequest->username,
|
||||
$createRequest->email,
|
||||
$createRequest->password,
|
||||
$createRequest->role,
|
||||
);
|
||||
$this->flash->set('user_success', "L'utilisateur « {$createRequest->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 $e) {
|
||||
$this->flash->set('user_error', $e->getMessage());
|
||||
} catch (\InvalidArgumentException|InvalidRoleException|RoleAssignmentNotAllowedException $e) {
|
||||
$this->flash->set('user_error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
$incidentId = $this->logUnexpectedError($req, $e, [
|
||||
'actor_user_id' => $this->sessionManager->getUserId(),
|
||||
'target_username' => $createRequest->username,
|
||||
'target_email_hash' => hash('sha256', mb_strtolower(trim($createRequest->email))),
|
||||
]);
|
||||
$this->flash->set('user_error', "Une erreur inattendue s'est produite (réf. {$incidentId})");
|
||||
}
|
||||
|
||||
return $res->withHeader('Location', '/admin/users/create')->withStatus(302);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $args */
|
||||
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é depuis l\'interface');
|
||||
|
||||
return $res->withHeader('Location', '/admin/users')->withStatus(302);
|
||||
}
|
||||
|
||||
$updateUserRoleRequest = UpdateUserRoleRequest::fromRequest($req, $this->rolePolicy->assignableRoles());
|
||||
|
||||
try {
|
||||
$this->userService->updateRole($id, $updateUserRoleRequest->requireRole());
|
||||
$this->flash->set('user_success', "Le rôle de « {$user->getUsername()} » a été mis à jour");
|
||||
} catch (\InvalidArgumentException|InvalidRoleException|RoleAssignmentNotAllowedException $e) {
|
||||
$this->flash->set('user_error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
$incidentId = $this->logUnexpectedError($req, $e, [
|
||||
'actor_user_id' => $this->sessionManager->getUserId(),
|
||||
'target_user_id' => $id,
|
||||
'target_username' => $user->getUsername(),
|
||||
]);
|
||||
$this->flash->set('user_error', "Une erreur inattendue s'est produite (réf. {$incidentId})");
|
||||
}
|
||||
|
||||
return $res->withHeader('Location', '/admin/users')->withStatus(302);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $args */
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->userService->delete($id);
|
||||
$this->flash->set('user_success', "L'utilisateur « {$user->getUsername()} » a été supprimé avec succès");
|
||||
} catch (\Throwable $e) {
|
||||
$incidentId = $this->logUnexpectedError($req, $e, [
|
||||
'actor_user_id' => $this->sessionManager->getUserId(),
|
||||
'target_user_id' => $id,
|
||||
'target_username' => $user->getUsername(),
|
||||
]);
|
||||
$this->flash->set('user_error', "Une erreur inattendue s'est produite (réf. {$incidentId})");
|
||||
}
|
||||
|
||||
return $res->withHeader('Location', '/admin/users')->withStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private function logUnexpectedError(Request $req, \Throwable $e, array $context = []): string
|
||||
{
|
||||
$incidentId = bin2hex(random_bytes(8));
|
||||
|
||||
$this->logger?->error('User administration action failed', $context + [
|
||||
'incident_id' => $incidentId,
|
||||
'route' => (string) $req->getUri()->getPath(),
|
||||
'method' => $req->getMethod(),
|
||||
'exception_class' => $e::class,
|
||||
'exception_message' => $e->getMessage(),
|
||||
'exception' => $e,
|
||||
]);
|
||||
|
||||
return $incidentId;
|
||||
}
|
||||
}
|
||||
28
src/Identity/UI/Http/UserRoutes.php
Normal file
28
src/Identity/UI/Http/UserRoutes.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Netig\Netslim\Identity\UI\Http;
|
||||
|
||||
use Netig\Netslim\Identity\UI\Http\Middleware\AdminMiddleware;
|
||||
use Netig\Netslim\Identity\UI\Http\Middleware\AuthMiddleware;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Slim\App;
|
||||
|
||||
/**
|
||||
* Enregistre les routes HTTP du sous-domaine gestion des comptes.
|
||||
*/
|
||||
final class UserRoutes
|
||||
{
|
||||
/** @param App<ContainerInterface> $app */
|
||||
public static function register(App $app): void
|
||||
{
|
||||
$app->group('/admin/users', function ($group) {
|
||||
$group->get('', [UserController::class, 'index']);
|
||||
$group->get('/create', [UserController::class, 'showCreate']);
|
||||
$group->post('/create', [UserController::class, 'create']);
|
||||
$group->post('/role/{id}', [UserController::class, 'updateRole']);
|
||||
$group->post('/delete/{id}', [UserController::class, 'delete']);
|
||||
})->add(AdminMiddleware::class)->add(AuthMiddleware::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user