Added login page
This commit is contained in:
@@ -117,8 +117,11 @@ final class Bootstrap
|
||||
*/
|
||||
private function registerRoutes(): void
|
||||
{
|
||||
$controller = $this->container->get('postController');
|
||||
Routes::register($this->app, $controller);
|
||||
$postController = $this->container->get('postController');
|
||||
$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 Medoo\Medoo;
|
||||
use App\Controllers\PostController;
|
||||
use App\Controllers\AuthController;
|
||||
use App\Repositories\PostRepository;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\Services\HtmlSanitizer;
|
||||
use App\Services\HtmlPurifierFactory;
|
||||
use App\Services\CsrfExtension;
|
||||
use App\Services\AuthService;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Database\Migrator;
|
||||
|
||||
/**
|
||||
@@ -136,6 +140,29 @@ final class Container implements ContainerInterface
|
||||
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
|
||||
// ============================================
|
||||
@@ -147,6 +174,13 @@ final class Container implements ContainerInterface
|
||||
$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
|
||||
{
|
||||
self::createPostTable($db);
|
||||
// self::createCommentsTable($db);
|
||||
// self::createUsersTable($db);
|
||||
self::createUsersTable($db);
|
||||
}
|
||||
|
||||
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 App\Controllers\PostController;
|
||||
use App\Controllers\AuthController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
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']);
|
||||
$app->get('/admin', [$controller, 'admin']);
|
||||
$app->get('/admin/edit/{id}', [$controller, 'form']);
|
||||
$app->post('/admin/create', [$controller, 'create']);
|
||||
$app->post('/admin/edit/{id}', [$controller, 'update']);
|
||||
$app->post('/admin/delete/{id}', [$controller, 'delete']);
|
||||
// ============================================
|
||||
// Routes publiques
|
||||
// ============================================
|
||||
|
||||
$app->get('/', [$postController, 'index']);
|
||||
$app->get('/article/{slug}', [$postController, 'show']);
|
||||
|
||||
// ============================================
|
||||
// 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>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Mon Blog{% endblock %}</title>
|
||||
{# <link rel="stylesheet" href="/css/main.css"> #}
|
||||
{#
|
||||
<link rel="stylesheet" href="/css/main.css"> #}
|
||||
<style>
|
||||
body {font-family: Arial, sans-serif; margin: 2rem;}
|
||||
.post {border-bottom: 1px solid #ccc; padding: 1rem 0;}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.post {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{# Header commun #}
|
||||
{% include 'partials/_header.twig' %}
|
||||
{% include 'partials/_header.twig' with {'session': _SESSION} %}
|
||||
|
||||
{# Zone principale – chaque page injecte son contenu #}
|
||||
<main>
|
||||
@@ -25,7 +71,8 @@
|
||||
{% include 'partials/_footer.twig' %}
|
||||
|
||||
{# Scripts globaux #}
|
||||
{# <script src="/js/app.js"></script> #}
|
||||
{#
|
||||
<script src="/js/app.js"></script> #}
|
||||
{% block scripts %}{% endblock %}
|
||||
</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>
|
||||
<h1>
|
||||
<a href="/">Mon Blog</a> |
|
||||
<a href="/admin">Admin</a>
|
||||
</h1>
|
||||
<header style="border-bottom: 1px solid #ccc; padding: 1rem 0; margin-bottom: 2rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h1 style="margin: 0;">
|
||||
<a href="/" style="text-decoration: none; color: inherit;">Mon Blog</a>
|
||||
</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>
|
||||
Reference in New Issue
Block a user