Added login page
This commit is contained in:
@@ -117,8 +117,11 @@ final class Bootstrap
|
|||||||
*/
|
*/
|
||||||
private function registerRoutes(): void
|
private function registerRoutes(): void
|
||||||
{
|
{
|
||||||
$controller = $this->container->get('postController');
|
$postController = $this->container->get('postController');
|
||||||
Routes::register($this->app, $controller);
|
$authController = $this->container->get('authController');
|
||||||
|
$authMiddleware = $this->container->get('authMiddleware');
|
||||||
|
|
||||||
|
Routes::register($this->app, $postController, $authController, $authMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ use Slim\Views\Twig;
|
|||||||
use Slim\Csrf\Guard;
|
use Slim\Csrf\Guard;
|
||||||
use Medoo\Medoo;
|
use Medoo\Medoo;
|
||||||
use App\Controllers\PostController;
|
use App\Controllers\PostController;
|
||||||
|
use App\Controllers\AuthController;
|
||||||
use App\Repositories\PostRepository;
|
use App\Repositories\PostRepository;
|
||||||
|
use App\Repositories\UserRepository;
|
||||||
use App\Services\HtmlSanitizer;
|
use App\Services\HtmlSanitizer;
|
||||||
use App\Services\HtmlPurifierFactory;
|
use App\Services\HtmlPurifierFactory;
|
||||||
use App\Services\CsrfExtension;
|
use App\Services\CsrfExtension;
|
||||||
|
use App\Services\AuthService;
|
||||||
|
use App\Middleware\AuthMiddleware;
|
||||||
use App\Database\Migrator;
|
use App\Database\Migrator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,6 +140,29 @@ final class Container implements ContainerInterface
|
|||||||
return new PostRepository($db);
|
return new PostRepository($db);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$this->factories['userRepository'] = function (): UserRepository {
|
||||||
|
$db = $this->get('database');
|
||||||
|
return new UserRepository($db);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Services d'authentification
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$this->factories['authService'] = function (): AuthService {
|
||||||
|
$userRepository = $this->get('userRepository');
|
||||||
|
return new AuthService($userRepository);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Middlewares
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$this->factories['authMiddleware'] = function (): AuthMiddleware {
|
||||||
|
$authService = $this->get('authService');
|
||||||
|
return new AuthMiddleware($authService);
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Controllers
|
// Controllers
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -147,6 +174,13 @@ final class Container implements ContainerInterface
|
|||||||
$this->get('htmlSanitizer')
|
$this->get('htmlSanitizer')
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$this->factories['authController'] = function (): AuthController {
|
||||||
|
return new AuthController(
|
||||||
|
$this->get('twig'),
|
||||||
|
$this->get('authService')
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
118
src/Controllers/AuthController.php
Normal file
118
src/Controllers/AuthController.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
use App\Services\AuthService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour l'authentification.
|
||||||
|
*/
|
||||||
|
final class AuthController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Twig $view,
|
||||||
|
private AuthService $authService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche la page de connexion.
|
||||||
|
*/
|
||||||
|
public function showLogin(Request $req, Response $res): Response
|
||||||
|
{
|
||||||
|
// Si déjà connecté, rediriger vers admin
|
||||||
|
if ($this->authService->isLoggedIn()) {
|
||||||
|
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->view->render($res, 'pages/login.twig', [
|
||||||
|
'error' => $_SESSION['login_error'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite la soumission du formulaire de connexion.
|
||||||
|
*/
|
||||||
|
public function login(Request $req, Response $res): Response
|
||||||
|
{
|
||||||
|
$data = $req->getParsedBody();
|
||||||
|
$username = trim((string)($data['username'] ?? ''));
|
||||||
|
$password = trim((string)($data['password'] ?? ''));
|
||||||
|
|
||||||
|
$user = $this->authService->authenticate($username, $password);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$_SESSION['login_error'] = 'Identifiants invalides';
|
||||||
|
return $res->withHeader('Location', '/login')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connecter l'utilisateur
|
||||||
|
$this->authService->login($user);
|
||||||
|
unset($_SESSION['login_error']);
|
||||||
|
|
||||||
|
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche la page d'enregistrement.
|
||||||
|
*/
|
||||||
|
public function showRegister(Request $req, Response $res): Response
|
||||||
|
{
|
||||||
|
// Si déjà connecté, rediriger vers admin
|
||||||
|
if ($this->authService->isLoggedIn()) {
|
||||||
|
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->view->render($res, 'pages/register.twig', [
|
||||||
|
'error' => $_SESSION['register_error'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite la soumission du formulaire d'enregistrement.
|
||||||
|
*/
|
||||||
|
public function register(Request $req, Response $res): Response
|
||||||
|
{
|
||||||
|
$data = $req->getParsedBody();
|
||||||
|
$username = trim((string)($data['username'] ?? ''));
|
||||||
|
$email = trim((string)($data['email'] ?? ''));
|
||||||
|
$password = trim((string)($data['password'] ?? ''));
|
||||||
|
$passwordConfirm = trim((string)($data['password_confirm'] ?? ''));
|
||||||
|
|
||||||
|
// Vérifier que les mots de passe correspondent
|
||||||
|
if ($password !== $passwordConfirm) {
|
||||||
|
$_SESSION['register_error'] = 'Les mots de passe ne correspondent pas';
|
||||||
|
return $res->withHeader('Location', '/register')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->authService->register($username, $email, $password);
|
||||||
|
unset($_SESSION['register_error']);
|
||||||
|
|
||||||
|
// Connecter automatiquement après l'enregistrement
|
||||||
|
$user = $this->authService->authenticate($username, $password);
|
||||||
|
if ($user) {
|
||||||
|
$this->authService->login($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
$_SESSION['register_error'] = $e->getMessage();
|
||||||
|
return $res->withHeader('Location', '/register')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déconnecte l'utilisateur.
|
||||||
|
*/
|
||||||
|
public function logout(Request $req, Response $res): Response
|
||||||
|
{
|
||||||
|
$this->authService->logout();
|
||||||
|
return $res->withHeader('Location', '/')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,8 +11,7 @@ final class Migrator
|
|||||||
public static function run(Medoo $db): void
|
public static function run(Medoo $db): void
|
||||||
{
|
{
|
||||||
self::createPostTable($db);
|
self::createPostTable($db);
|
||||||
// self::createCommentsTable($db);
|
self::createUsersTable($db);
|
||||||
// self::createUsersTable($db);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function createPostTable(Medoo $db): void
|
private static function createPostTable(Medoo $db): void
|
||||||
@@ -28,4 +27,18 @@ final class Migrator
|
|||||||
)
|
)
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function createUsersTable(Medoo $db): void
|
||||||
|
{
|
||||||
|
$db->pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/Middleware/AuthMiddleware.php
Normal file
35
src/Middleware/AuthMiddleware.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Middleware;
|
||||||
|
|
||||||
|
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 App\Services\AuthService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware pour protéger les routes admin.
|
||||||
|
* Redirige vers la page de connexion si l'utilisateur n'est pas authentifié.
|
||||||
|
*/
|
||||||
|
final class AuthMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function __construct(private AuthService $authService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
if (!$this->authService->isLoggedIn()) {
|
||||||
|
// Redirection vers la page de connexion
|
||||||
|
$response = new \Slim\Psr7\Response();
|
||||||
|
return $response
|
||||||
|
->withHeader('Location', '/login')
|
||||||
|
->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/Models/User.php
Normal file
125
src/Models/User.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modèle User pour l'authentification.
|
||||||
|
*/
|
||||||
|
final class User
|
||||||
|
{
|
||||||
|
private DateTime $createdAt;
|
||||||
|
private DateTime $updatedAt;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly int $id,
|
||||||
|
private readonly string $username,
|
||||||
|
private readonly string $email,
|
||||||
|
private readonly string $passwordHash,
|
||||||
|
?DateTime $createdAt = null,
|
||||||
|
?DateTime $updatedAt = null,
|
||||||
|
) {
|
||||||
|
$this->createdAt = $createdAt ?? new DateTime();
|
||||||
|
$this->updatedAt = $updatedAt ?? new DateTime();
|
||||||
|
$this->validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une instance depuis un tableau (ligne DB).
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $data
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function fromArray(array $data): self
|
||||||
|
{
|
||||||
|
$id = (int)($data['id'] ?? 0);
|
||||||
|
$username = (string)($data['username'] ?? '');
|
||||||
|
$email = (string)($data['email'] ?? '');
|
||||||
|
$passwordHash = (string)($data['password_hash'] ?? '');
|
||||||
|
|
||||||
|
$createdAt = isset($data['created_at'])
|
||||||
|
? new DateTime($data['created_at'])
|
||||||
|
: new DateTime();
|
||||||
|
|
||||||
|
$updatedAt = isset($data['updated_at'])
|
||||||
|
? new DateTime($data['updated_at'])
|
||||||
|
: new DateTime();
|
||||||
|
|
||||||
|
return new self($id, $username, $email, $passwordHash, $createdAt, $updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide les données de l'utilisateur.
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private function validate(): void
|
||||||
|
{
|
||||||
|
if (empty($this->username) || mb_strlen($this->username) < 3) {
|
||||||
|
throw new \InvalidArgumentException('Le nom d\'utilisateur doit contenir au moins 3 caractères');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_strlen($this->username) > 50) {
|
||||||
|
throw new \InvalidArgumentException('Le nom d\'utilisateur ne peut pas dépasser 50 caractères');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->email) || !filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new \InvalidArgumentException('L\'email n\'est pas valide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->passwordHash)) {
|
||||||
|
throw new \InvalidArgumentException('Le hash du mot de passe ne peut pas être vide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Getters
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
public function getId(): int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUsername(): string
|
||||||
|
{
|
||||||
|
return $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPasswordHash(): string
|
||||||
|
{
|
||||||
|
return $this->passwordHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTime
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTime
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne les données prêtes à persister en DB.
|
||||||
|
*
|
||||||
|
* @return array{username:string,email:string,password_hash:string}
|
||||||
|
*/
|
||||||
|
public function toPersistableArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'username' => $this->username,
|
||||||
|
'email' => $this->email,
|
||||||
|
'password_hash' => $this->passwordHash,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/Repositories/UserRepository.php
Normal file
66
src/Repositories/UserRepository.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use Medoo\Medoo;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final class UserRepository
|
||||||
|
{
|
||||||
|
public function __construct(private Medoo $db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trouve un utilisateur par ID.
|
||||||
|
*/
|
||||||
|
public function findById(int $id): ?User
|
||||||
|
{
|
||||||
|
$row = $this->db->get('users', '*', ['id' => $id]);
|
||||||
|
return $row ? User::fromArray($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trouve un utilisateur par nom d'utilisateur.
|
||||||
|
*/
|
||||||
|
public function findByUsername(string $username): ?User
|
||||||
|
{
|
||||||
|
$row = $this->db->get('users', '*', ['username' => $username]);
|
||||||
|
return $row ? User::fromArray($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trouve un utilisateur par email.
|
||||||
|
*/
|
||||||
|
public function findByEmail(string $email): ?User
|
||||||
|
{
|
||||||
|
$row = $this->db->get('users', '*', ['email' => $email]);
|
||||||
|
return $row ? User::fromArray($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouvel utilisateur.
|
||||||
|
*/
|
||||||
|
public function create(User $user): int
|
||||||
|
{
|
||||||
|
$this->db->insert('users', [
|
||||||
|
'username' => $user->getUsername(),
|
||||||
|
'email' => $user->getEmail(),
|
||||||
|
'password_hash' => $user->getPasswordHash(),
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
return (int)$this->db->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne tous les utilisateurs.
|
||||||
|
*/
|
||||||
|
public function findAll(): array
|
||||||
|
{
|
||||||
|
$rows = $this->db->select('users', '*', ['ORDER' => ['id' => 'DESC']]);
|
||||||
|
return array_map(fn ($row) => User::fromArray($row), $rows ?: []);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,17 +6,45 @@ namespace App;
|
|||||||
|
|
||||||
use Slim\App;
|
use Slim\App;
|
||||||
use App\Controllers\PostController;
|
use App\Controllers\PostController;
|
||||||
|
use App\Controllers\AuthController;
|
||||||
|
use App\Middleware\AuthMiddleware;
|
||||||
|
|
||||||
final class Routes
|
final class Routes
|
||||||
{
|
{
|
||||||
public static function register(App $app, PostController $controller): void
|
public static function register(App $app, PostController $postController, AuthController $authController, AuthMiddleware $authMiddleware): void
|
||||||
{
|
{
|
||||||
$app->get('/', [$controller, 'index']);
|
// ============================================
|
||||||
$app->get('/article/{slug}', [$controller, 'show']);
|
// Routes publiques
|
||||||
$app->get('/admin', [$controller, 'admin']);
|
// ============================================
|
||||||
$app->get('/admin/edit/{id}', [$controller, 'form']);
|
|
||||||
$app->post('/admin/create', [$controller, 'create']);
|
$app->get('/', [$postController, 'index']);
|
||||||
$app->post('/admin/edit/{id}', [$controller, 'update']);
|
$app->get('/article/{slug}', [$postController, 'show']);
|
||||||
$app->post('/admin/delete/{id}', [$controller, 'delete']);
|
|
||||||
|
// ============================================
|
||||||
|
// Routes d'authentification
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$app->get('/login', [$authController, 'showLogin']);
|
||||||
|
$app->post('/login', [$authController, 'login']);
|
||||||
|
|
||||||
|
$app->get('/register', [$authController, 'showRegister']);
|
||||||
|
$app->post('/register', [$authController, 'register']);
|
||||||
|
|
||||||
|
$app->post('/logout', [$authController, 'logout']);
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Routes admin (protégées par authentification)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$adminGroup = $app->group('', function ($group) use ($postController) {
|
||||||
|
$group->get('/admin', [$postController, 'admin']);
|
||||||
|
$group->get('/admin/edit/{id}', [$postController, 'form']);
|
||||||
|
$group->post('/admin/create', [$postController, 'create']);
|
||||||
|
$group->post('/admin/edit/{id}', [$postController, 'update']);
|
||||||
|
$group->post('/admin/delete/{id}', [$postController, 'delete']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Appliquer le middleware d'authentification au groupe admin
|
||||||
|
$adminGroup->add($authMiddleware);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
127
src/Services/AuthService.php
Normal file
127
src/Services/AuthService.php
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Repositories\UserRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service d'authentification.
|
||||||
|
* Gère le hash, la vérification et la création d'utilisateurs.
|
||||||
|
*/
|
||||||
|
final class AuthService
|
||||||
|
{
|
||||||
|
public function __construct(private UserRepository $userRepository)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre un nouvel utilisateur.
|
||||||
|
*
|
||||||
|
* @param string $username
|
||||||
|
* @param string $email
|
||||||
|
* @param string $plainPassword Mot de passe en clair
|
||||||
|
* @return User
|
||||||
|
* @throws \InvalidArgumentException Si la validation échoue
|
||||||
|
*/
|
||||||
|
public function register(string $username, string $email, string $plainPassword): User
|
||||||
|
{
|
||||||
|
$username = trim($username);
|
||||||
|
$email = trim($email);
|
||||||
|
$plainPassword = trim($plainPassword);
|
||||||
|
|
||||||
|
// Vérifier que l'utilisateur n'existe pas déjà
|
||||||
|
if ($this->userRepository->findByUsername($username)) {
|
||||||
|
throw new \InvalidArgumentException('Ce nom d\'utilisateur est déjà pris');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->userRepository->findByEmail($email)) {
|
||||||
|
throw new \InvalidArgumentException('Cet email est déjà utilisé');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valider le mot de passe
|
||||||
|
if (mb_strlen($plainPassword) < 8) {
|
||||||
|
throw new \InvalidArgumentException('Le mot de passe doit contenir au moins 8 caractères');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer l'utilisateur avec le hash du mot de passe
|
||||||
|
$passwordHash = password_hash($plainPassword, PASSWORD_BCRYPT, ['cost' => 12]);
|
||||||
|
$user = new User(0, $username, $email, $passwordHash);
|
||||||
|
|
||||||
|
// Persister en DB
|
||||||
|
$this->userRepository->create($user);
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentifie un utilisateur et retourne son instance si valide.
|
||||||
|
*
|
||||||
|
* @param string $username
|
||||||
|
* @param string $plainPassword
|
||||||
|
* @return User|null
|
||||||
|
*/
|
||||||
|
public function authenticate(string $username, string $plainPassword): ?User
|
||||||
|
{
|
||||||
|
$username = trim($username);
|
||||||
|
$plainPassword = trim($plainPassword);
|
||||||
|
|
||||||
|
$user = $this->userRepository->findByUsername($username);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le mot de passe
|
||||||
|
if (!password_verify($plainPassword, $user->getPasswordHash())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un utilisateur est actuellement connecté.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isLoggedIn(): bool
|
||||||
|
{
|
||||||
|
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'utilisateur actuellement connecté.
|
||||||
|
*
|
||||||
|
* @return User|null
|
||||||
|
*/
|
||||||
|
public function getCurrentUser(): ?User
|
||||||
|
{
|
||||||
|
if (!$this->isLoggedIn()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->userRepository->findById((int)$_SESSION['user_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connecte un utilisateur (crée la session).
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
*/
|
||||||
|
public function login(User $user): void
|
||||||
|
{
|
||||||
|
$_SESSION['user_id'] = $user->getId();
|
||||||
|
$_SESSION['username'] = $user->getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déconnecte l'utilisateur.
|
||||||
|
*/
|
||||||
|
public function logout(): void
|
||||||
|
{
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,66 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Mon Blog{% endblock %}</title>
|
<title>{% block title %}Mon Blog{% endblock %}</title>
|
||||||
{# <link rel="stylesheet" href="/css/main.css"> #}
|
{#
|
||||||
|
<link rel="stylesheet" href="/css/main.css"> #}
|
||||||
<style>
|
<style>
|
||||||
body {font-family: Arial, sans-serif; margin: 2rem;}
|
body {
|
||||||
.post {border-bottom: 1px solid #ccc; padding: 1rem 0;}
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-actions a,
|
.admin-actions a,
|
||||||
.admin-actions form {margin-right: .5rem;}
|
.admin-actions form {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
{# Header commun #}
|
{# Header commun #}
|
||||||
{% include 'partials/_header.twig' %}
|
{% include 'partials/_header.twig' with {'session': _SESSION} %}
|
||||||
|
|
||||||
{# Zone principale – chaque page injecte son contenu #}
|
{# Zone principale – chaque page injecte son contenu #}
|
||||||
<main>
|
<main>
|
||||||
@@ -25,7 +71,8 @@
|
|||||||
{% include 'partials/_footer.twig' %}
|
{% include 'partials/_footer.twig' %}
|
||||||
|
|
||||||
{# Scripts globaux #}
|
{# Scripts globaux #}
|
||||||
{# <script src="/js/app.js"></script> #}
|
{#
|
||||||
|
<script src="/js/app.js"></script> #}
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
47
views/pages/login.twig
Normal file
47
views/pages/login.twig
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block title %}Connexion – Mon Blog{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="auth-container" style="max-width: 400px; margin: 2rem auto;">
|
||||||
|
<h2>Connexion</h2>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger"
|
||||||
|
style="padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; margin-bottom: 1rem; color: #721c24;">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/login">
|
||||||
|
{# Tokens CSRF #}
|
||||||
|
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||||
|
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="username">Nom d'utilisateur<br>
|
||||||
|
<input type="text" id="username" name="username" required autofocus
|
||||||
|
style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="password">Mot de passe<br>
|
||||||
|
<input type="password" id="password" name="password" required
|
||||||
|
style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<button type="submit" class="btn btn-primary"
|
||||||
|
style="padding: 0.75rem 1.5rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
Se connecter
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style="text-align: center; margin-top: 1.5rem;">
|
||||||
|
Pas encore de compte ? <a href="/register">S'enregistrer</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
63
views/pages/register.twig
Normal file
63
views/pages/register.twig
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block title %}Enregistrement – Mon Blog{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="auth-container" style="max-width: 400px; margin: 2rem auto;">
|
||||||
|
<h2>S'enregistrer</h2>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger"
|
||||||
|
style="padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; margin-bottom: 1rem; color: #721c24;">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/register">
|
||||||
|
{# Tokens CSRF #}
|
||||||
|
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||||
|
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="username">Nom d'utilisateur<br>
|
||||||
|
<input type="text" id="username" name="username" required autofocus minlength="3" maxlength="50"
|
||||||
|
style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
</label>
|
||||||
|
<small>Minimum 3 caractères</small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="email">Email<br>
|
||||||
|
<input type="email" id="email" name="email" required
|
||||||
|
style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="password">Mot de passe<br>
|
||||||
|
<input type="password" id="password" name="password" required minlength="8"
|
||||||
|
style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
</label>
|
||||||
|
<small>Minimum 8 caractères</small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="password_confirm">Confirmer le mot de passe<br>
|
||||||
|
<input type="password" id="password_confirm" name="password_confirm" required minlength="8"
|
||||||
|
style="width: 100%; padding: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<button type="submit" class="btn btn-primary"
|
||||||
|
style="padding: 0.75rem 1.5rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
S'enregistrer
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style="text-align: center; margin-top: 1.5rem;">
|
||||||
|
Vous avez déjà un compte ? <a href="/login">Se connecter</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,6 +1,29 @@
|
|||||||
<header>
|
<header style="border-bottom: 1px solid #ccc; padding: 1rem 0; margin-bottom: 2rem;">
|
||||||
<h1>
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<a href="/">Mon Blog</a> |
|
<h1 style="margin: 0;">
|
||||||
<a href="/admin">Admin</a>
|
<a href="/" style="text-decoration: none; color: inherit;">Mon Blog</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
{% if session.user_id is defined and session.user_id %}
|
||||||
|
{# Utilisateur connecté #}
|
||||||
|
<span style="margin-right: 1rem;">
|
||||||
|
Connecté en tant que : <strong>{{ session.username }}</strong>
|
||||||
|
</span>
|
||||||
|
<a href="/admin" style="margin-right: 1rem;">Admin</a>
|
||||||
|
<form method="post" action="/logout" style="display: inline;">
|
||||||
|
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||||
|
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||||
|
<button type="submit"
|
||||||
|
style="background: none; border: none; color: #007bff; cursor: pointer; text-decoration: underline;">
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
{# Utilisateur non connecté #}
|
||||||
|
<a href="/login" style="margin-right: 1rem;">Connexion</a>
|
||||||
|
<a href="/register">S'enregistrer</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
Reference in New Issue
Block a user