ajout projet cms elyes
This commit is contained in:
4
cms_simplifie/.env
Normal file
4
cms_simplifie/.env
Normal file
@@ -0,0 +1,4 @@
|
||||
DB_HOST=db
|
||||
DB_NAME=cms_simplifie
|
||||
DB_USER=cms
|
||||
DB_PASS=cmspass
|
||||
63
cms_simplifie/.htaccess
Normal file
63
cms_simplifie/.htaccess
Normal file
@@ -0,0 +1,63 @@
|
||||
# ===================================================================
|
||||
# Sécurité de base
|
||||
# ===================================================================
|
||||
ServerSignature Off
|
||||
Options -Indexes
|
||||
FileETag None
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
|
||||
|
||||
|
||||
# Bloque fichiers cachés (.env, .git, etc.) et configs/dev
|
||||
<FilesMatch "^(\.|composer\.(json|lock)|package(-lock)?\.json|Dockerfile|docker-compose\.yml|.*\.env|.*\.sql|.*\.md)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Forcer une page 404 propre
|
||||
ErrorDocument 404 /page404.php
|
||||
|
||||
# Empêche l'accès direct aux dossiers non publics
|
||||
RewriteEngine On
|
||||
RewriteRule ^inc/ - [F,L,NC]
|
||||
RewriteRule ^sql/ - [F,L,NC]
|
||||
|
||||
|
||||
# Méthodes HTTP autorisées (GET/POST/HEAD)
|
||||
<LimitExcept GET POST HEAD>
|
||||
Require all denied
|
||||
</LimitExcept>
|
||||
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
# Empêche le framing (clickjacking)
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
|
||||
# Empêche le MIME sniffing
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
|
||||
# Politique du Referer
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# Permissions API (désactive tout par défaut)
|
||||
Header always set Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), usb=(), interest-cohort=()"
|
||||
|
||||
# Politique de ressources cross-origin
|
||||
Header always set Cross-Origin-Resource-Policy "same-origin"
|
||||
Header always set Cross-Origin-Opener-Policy "same-origin"
|
||||
|
||||
|
||||
|
||||
# Content Security Policy (CSP)
|
||||
# - autorise CSS en ligne
|
||||
# - pas d'exec JS hormis le fichier local .js (je n'ai pas utilisé de code javascript donc le site ne devrait jamais utiliser de code js )
|
||||
Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; img-src 'self' data:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'"
|
||||
|
||||
|
||||
</IfModule>
|
||||
|
||||
|
||||
# Redirection sécurisé: racine -> /public
|
||||
RewriteCond %{REQUEST_URI} ^/$
|
||||
RewriteRule ^$ public/ [R=302,L]
|
||||
|
||||
7
cms_simplifie/Dockerfile
Normal file
7
cms_simplifie/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM php:8.2-apache
|
||||
RUN docker-php-ext-install pdo pdo_mysql
|
||||
RUN a2enmod rewrite
|
||||
RUN apt-get update && apt-get install -y tzdata
|
||||
WORKDIR /var/www/html
|
||||
COPY . /var/www/html
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
19
cms_simplifie/README_DOCKER.md
Normal file
19
cms_simplifie/README_DOCKER.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# CMS Simplifié
|
||||
|
||||
## Pour lancer
|
||||
|
||||
docker compose up -d --build
|
||||
|
||||
- Site : http://localhost:8080/public/index.php
|
||||
|
||||
## Compte admin
|
||||
Identifiant :admin
|
||||
Mdp :VH515f6frrv11e651
|
||||
- meme le mot de passe de l'admin est haché
|
||||
|
||||
## Notes
|
||||
- PHP + Apache + mysql avec requete preparer
|
||||
- Sécurité basique : CSRF sur POST, password_hash/verify, PDO préparé
|
||||
- Mise en place des sécurités pour eviter les attaques par XSS grace a la fonction e (échapper)
|
||||
- Uniquement le dossier public est exposé : mise en place des sécurité par le fichier .htaccess
|
||||
- Pas d'execution javascript en ligne ou insérer malicieusement. seul le code dans le fichier.js peut etre executer mais j'en ai pas donc le site ne devrait recevoir de code js
|
||||
76
cms_simplifie/admin/articles_create.php
Normal file
76
cms_simplifie/admin/articles_create.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
// Inclusion des fichiers de config, BDD et fonctions
|
||||
require_once __DIR__ . '/../inc/config.php';
|
||||
require_once __DIR__ . '/../inc/db.php';
|
||||
require_once __DIR__ . '/../inc/functions.php';
|
||||
|
||||
// Vérifie que l'utilisateur est connecté, sinon redirection
|
||||
require_login();
|
||||
|
||||
$errors = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Vérifie la validité du jeton CSRF pour éviter les attaques
|
||||
verify_csrf();
|
||||
|
||||
// Récupération et nettoyage des champs du formulaire
|
||||
$titre = trim($_POST['titre'] ?? '');
|
||||
$contenu = trim($_POST['contenu'] ?? '');
|
||||
|
||||
// Vérifie si les champs obligatoires sont remplis
|
||||
if ($titre === '' || $contenu === '') {
|
||||
$errors[] = "Tous les champs sont obligatoires.";
|
||||
}
|
||||
|
||||
// Si pas d'erreurs, insertion de l'article dans la base
|
||||
if (!$errors) {
|
||||
$stmt = $pdo->prepare("INSERT INTO articles (user_id, titre, contenu, date_creation) VALUES (:uid, :titre, :contenu, NOW())");
|
||||
$stmt->execute([
|
||||
'uid'=>current_user_id(), // ID de l'auteur connecté
|
||||
'titre'=>$titre,
|
||||
'contenu'=>$contenu
|
||||
]);
|
||||
|
||||
// Redirection vers le tableau de bord après publication
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Inclusion du header
|
||||
require_once __DIR__ . '/../inc/header.php';
|
||||
?>
|
||||
|
||||
<h2>Écrire un article</h2>
|
||||
|
||||
<!-- Affiche les erreurs éventuelles -->
|
||||
<?php if ($errors): ?>
|
||||
<div class="error">
|
||||
<?php foreach ($errors as $e): ?>
|
||||
<p><?= e($e) ?></p>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Formulaire de création d'article -->
|
||||
<form method="post">
|
||||
<?= csrf_input() ?> <!-- Jeton CSRF caché -->
|
||||
|
||||
<div class="form-group">
|
||||
<label>Titre</label>
|
||||
<input type="text" name="titre" value="<?= e($_POST['titre'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Contenu</label>
|
||||
<textarea name="contenu"><?= e($_POST['contenu'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Bouton publier et annuler -->
|
||||
<button class="btn primary" type="submit">Publier</button>
|
||||
<a class="btn" href="/admin/dashboard.php">Annuler</a>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
// Inclusion du footer
|
||||
require_once __DIR__ . '/../inc/footer.php';
|
||||
?>
|
||||
60
cms_simplifie/admin/articles_delete.php
Normal file
60
cms_simplifie/admin/articles_delete.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
// Charge la config, la connexion BDD et les fonctions
|
||||
require_once __DIR__ . '/../inc/config.php';
|
||||
require_once __DIR__ . '/../inc/db.php';
|
||||
require_once __DIR__ . '/../inc/functions.php';
|
||||
|
||||
// Accès réservé aux utilisateurs connectés
|
||||
require_login();
|
||||
|
||||
// Récupère l'ID de l'article depuis l'URL et le valide
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if ($id <= 0) { header('Location: /admin/dashboard.php'); exit; }
|
||||
|
||||
// Charge les infos de l'article (pour vérifier droits + afficher le titre)
|
||||
$stmt = $pdo->prepare("SELECT id, user_id, titre FROM articles WHERE id = :id");
|
||||
$stmt->execute(['id'=>$id]);
|
||||
$article = $stmt->fetch();
|
||||
if (!$article) { header('Location: /admin/dashboard.php'); exit; }
|
||||
|
||||
// Contrôle d'autorisation : admin ou auteur du post
|
||||
if (!is_admin() && (int)$article['user_id'] !== current_user_id()) {
|
||||
http_response_code(403); // interdit
|
||||
echo "<div class='card error'><p>Accès refusé.</p></div>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Traitement du formulaire de confirmation (méthode POST + CSRF)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf(); // vérifie le jeton CSRF
|
||||
|
||||
// Si l'utilisateur confirme, on supprime l'article
|
||||
if (isset($_POST['confirm']) && $_POST['confirm'] === 'yes') {
|
||||
$del = $pdo->prepare("DELETE FROM articles WHERE id = :id");
|
||||
$del->execute(['id'=>$id]);
|
||||
}
|
||||
|
||||
// Retour au tableau de bord dans tous les cas
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Affichage
|
||||
require_once __DIR__ . '/../inc/header.php';
|
||||
?>
|
||||
<h2>Supprimer l'article</h2>
|
||||
<div class="card">
|
||||
<!-- Rappel du titre pour confirmation -->
|
||||
<p>Supprimer <strong><?= e($article['titre']) ?></strong> ?</p>
|
||||
|
||||
<!-- Formulaire de confirmation (protégé CSRF) -->
|
||||
<form method="post" style="display:inline">
|
||||
<?= csrf_input() ?> <!-- jeton CSRF -->
|
||||
<input type="hidden" name="confirm" value="yes">
|
||||
<button class="btn danger" type="submit">Oui, supprimer</button>
|
||||
</form>
|
||||
|
||||
<!-- Lien d'annulation -->
|
||||
<a class="btn" href="/admin/dashboard.php">Annuler</a>
|
||||
</div>
|
||||
<?php require_once __DIR__ . '/../inc/footer.php'; ?>
|
||||
82
cms_simplifie/admin/articles_edit.php
Normal file
82
cms_simplifie/admin/articles_edit.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
// Charge la configuration, la connexion BDD et les fonctions
|
||||
require_once __DIR__ . '/../inc/config.php';
|
||||
require_once __DIR__ . '/../inc/db.php';
|
||||
require_once __DIR__ . '/../inc/functions.php';
|
||||
|
||||
// Restreint l'accès : uniquement pour utilisateurs connectés
|
||||
require_login();
|
||||
|
||||
// Récupère et valide l'ID de l'article passé en GET
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if ($id <= 0) { header('Location: /admin/dashboard.php'); exit; }
|
||||
|
||||
// Récupère l'article ciblé
|
||||
$stmt = $pdo->prepare("SELECT id, user_id, titre, contenu FROM articles WHERE id = :id");
|
||||
$stmt->execute(['id'=>$id]);
|
||||
$article = $stmt->fetch();
|
||||
if (!$article) { header('Location: /admin/dashboard.php'); exit; }
|
||||
|
||||
// Vérifie les droits : admin ou auteur de l'article
|
||||
if (!is_admin() && (int)$article['user_id'] !== current_user_id()) {
|
||||
http_response_code(403); // interdit
|
||||
echo "<div class='card error'><p>Accès refusé.</p></div>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Gestion du formulaire d'édition
|
||||
$errors = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf(); // protège contre les attaques CSRF
|
||||
|
||||
// Nettoyage et validation des champs
|
||||
$titre = trim($_POST['titre'] ?? '');
|
||||
$contenu = trim($_POST['contenu'] ?? '');
|
||||
if ($titre === '' || $contenu === '') { $errors[] = "Tous les champs sont obligatoires."; }
|
||||
|
||||
// Si ok, mise à jour de l'article en BDD
|
||||
if (!$errors) {
|
||||
$stmt = $pdo->prepare("UPDATE articles SET titre=:titre, contenu=:contenu WHERE id=:id");
|
||||
$stmt->execute(['titre'=>$titre,'contenu'=>$contenu,'id'=>$id]);
|
||||
|
||||
// Retour au tableau de bord après sauvegarde
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Affichage de la page (header + formulaire + footer)
|
||||
require_once __DIR__ . '/../inc/header.php';
|
||||
?>
|
||||
<h2>Modifier l'article #<?= (int)$article['id'] ?></h2>
|
||||
|
||||
<?php if ($errors): ?>
|
||||
<div class="error">
|
||||
<?php foreach ($errors as $e): ?>
|
||||
<p><?= e($e) ?></p> <!-- messages d'erreur échappés -->
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post">
|
||||
<?= csrf_input() ?> <!-- champ caché CSRF -->
|
||||
|
||||
<div class="form-group">
|
||||
<label>Titre</label>
|
||||
<!-- Pré-remplit avec la valeur existante -->
|
||||
<input type="text" name="titre" value="<?= e($_POST['titre'] ?? $article['titre']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Contenu</label>
|
||||
<!-- Pré-remplit avec la saisie en cours ou la valeur existante -->
|
||||
<textarea name="contenu"><?= e($_POST['contenu'] ?? $article['contenu']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<button class="btn primary" type="submit">Enregistrer</button>
|
||||
<a class="btn" href="/admin/dashboard.php">Annuler</a>
|
||||
</form>
|
||||
|
||||
|
||||
<?php require_once __DIR__ . '/../inc/footer.php'; ?>
|
||||
82
cms_simplifie/admin/dashboard.php
Normal file
82
cms_simplifie/admin/dashboard.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
// Charge la config, la connexion BDD et les fonctions
|
||||
require_once __DIR__ . '/../inc/config.php';
|
||||
require_once __DIR__ . '/../inc/db.php';
|
||||
require_once __DIR__ . '/../inc/functions.php';
|
||||
|
||||
// Restreint l'accès au tableau de bord aux utilisateurs connectés
|
||||
require_login();
|
||||
|
||||
// Affiche le header
|
||||
require_once __DIR__ . '/../inc/header.php';
|
||||
|
||||
// Calcule le nombre d'articles : tous si admin, sinon seulement ceux de l'utilisateur
|
||||
if (is_admin()) {
|
||||
$countArticles = (int)$pdo->query("SELECT COUNT(*) FROM articles")->fetchColumn();
|
||||
} else {
|
||||
$stmtCount = $pdo->prepare("SELECT COUNT(*) FROM articles WHERE user_id = :uid");
|
||||
$stmtCount->execute(['uid'=>current_user_id()]);
|
||||
$countArticles = (int)$stmtCount->fetchColumn();
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
<h2>Mon espace</h2>
|
||||
<div class="notice">Bienvenue, <?= e(current_user_login()) ?> </div>
|
||||
<p>Vos articles : <strong><?= $countArticles ?></strong></p>
|
||||
|
||||
<p>
|
||||
<!-- Lien pour créer un nouvel article -->
|
||||
<a class="btn primary" href="/admin/articles_create.php">Écrire un nouvel article</a>
|
||||
<!-- Lien pour revenir à la partie publique du site -->
|
||||
<a class="btn" href="/public/index.php">Retourner a l'accueil</a>
|
||||
</p>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th><th>Titre</th><th>Date</th><th>Auteur</th><th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
// Récupère la liste d'articles à afficher : tous si admin, sinon filtrés par user_id
|
||||
if (is_admin()) {
|
||||
$stmt = $pdo->query("
|
||||
SELECT a.id, a.titre, a.date_creation, u.login AS auteur
|
||||
FROM articles a
|
||||
LEFT JOIN utilisateur u ON u.id = a.user_id
|
||||
ORDER BY a.date_creation DESC
|
||||
");
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT a.id, a.titre, a.date_creation, u.login AS auteur
|
||||
FROM articles a
|
||||
LEFT JOIN utilisateur u ON u.id = a.user_id
|
||||
WHERE a.user_id = :uid
|
||||
ORDER BY a.date_creation DESC
|
||||
");
|
||||
$stmt->execute(['uid'=>current_user_id()]);
|
||||
}
|
||||
|
||||
// Affiche chaque article dans une ligne du tableau (échappé pour éviter les attaques par xss)
|
||||
foreach ($stmt as $row): ?>
|
||||
<tr>
|
||||
<td><?= (int)$row['id'] ?></td>
|
||||
<td><?= e($row['titre']) ?></td>
|
||||
<td><?= e(date('d/m/Y H:i', strtotime($row['date_creation']))) ?></td>
|
||||
<td><?= e($row['auteur'] ?? 'Anonyme') ?></td>
|
||||
<td>
|
||||
<!-- Actions d'édition/suppression sur l'article courant -->
|
||||
<a class="btn" href="/admin/articles_edit.php?id=<?= (int)$row['id'] ?>">Modifier</a>
|
||||
<a class="btn danger" href="/admin/articles_delete.php?id=<?= (int)$row['id'] ?>">Supprimer</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php
|
||||
// Footer
|
||||
require_once __DIR__ . '/../inc/footer.php';
|
||||
?>
|
||||
77
cms_simplifie/admin/login.php
Normal file
77
cms_simplifie/admin/login.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
// Charge la config, la connexion BDD et les fonctions
|
||||
require_once __DIR__ . '/../inc/config.php';
|
||||
require_once __DIR__ . '/../inc/db.php';
|
||||
require_once __DIR__ . '/../inc/functions.php';
|
||||
|
||||
$errors = []; // Contiendra les messages d'erreur à afficher
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf(); // Sécurise le formulaire contre les attaques CSRF
|
||||
|
||||
// Récupère et nettoie les identifiants saisis
|
||||
$loginOrEmail = trim($_POST['login'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
// Validation des champs requis
|
||||
if ($loginOrEmail === '' || $password === '') {
|
||||
$errors[] = "Veuillez renseigner vos identifiants.";
|
||||
} else {
|
||||
// Recherche de l'utilisateur par login ou email (requête préparée sécurisée)
|
||||
$stmt = $pdo->prepare("SELECT id, login, password, role FROM utilisateur WHERE login = :v OR email = :v LIMIT 1");
|
||||
$stmt->execute(['v'=>$loginOrEmail]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
// Vérifie le mot de passe
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
|
||||
|
||||
// Sauvegarde l'état de connexion en session
|
||||
$_SESSION['user_id'] = (int)$user['id'];
|
||||
$_SESSION['login'] = $user['login'];
|
||||
$_SESSION['role'] = $user['role'] ?: 'user';
|
||||
|
||||
// Redirige vers le tableau de bord une fois connecté
|
||||
header('Location: /admin/dashboard.php'); exit;
|
||||
} else {
|
||||
// Message générique pour ne pas révéler si login/email existe
|
||||
$errors[] = "Identifiants incorrects.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// header
|
||||
require_once __DIR__ . '/../inc/header.php';
|
||||
?>
|
||||
<h2>Connexion</h2>
|
||||
|
||||
<?php if ($errors): ?>
|
||||
<div class="error">
|
||||
<?php foreach ($errors as $e): ?>
|
||||
<p><?= e($e) ?></p> <!-- Affiche les erreurs de manière échappée -->
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Formulaire de connexion protégé par CSRF -->
|
||||
<form method="post">
|
||||
<?= csrf_input() ?> <!-- Champ caché contenant le jeton CSRF -->
|
||||
|
||||
<div class="form-group">
|
||||
<label>Login ou Email</label>
|
||||
<!-- Conserve la saisie utilisateur en cas d'erreur -->
|
||||
<input type="text" name="login" value="<?= e($_POST['login'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Mot de passe</label>
|
||||
<input type="password" name="password">
|
||||
</div>
|
||||
|
||||
<button class="btn primary" type="submit">Se connecter</button>
|
||||
<a class="btn" href="/admin/register.php">Créer un compte</a>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
// footer
|
||||
require_once __DIR__ . '/../inc/footer.php';
|
||||
?>
|
||||
5
cms_simplifie/admin/logout.php
Normal file
5
cms_simplifie/admin/logout.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../inc/functions.php';
|
||||
session_destroy();
|
||||
header('Location: /public/index.php');
|
||||
exit;
|
||||
95
cms_simplifie/admin/register.php
Normal file
95
cms_simplifie/admin/register.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
// Charge la config, la connexion BDD et les fonction
|
||||
require_once __DIR__ . '/../inc/config.php';
|
||||
require_once __DIR__ . '/../inc/db.php';
|
||||
require_once __DIR__ . '/../inc/functions.php';
|
||||
|
||||
$errors = []; // Collecte des messages d'erreur
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf(); // Protection CSRF du formulaire
|
||||
|
||||
// Récupération et on vide les champs
|
||||
$login = trim($_POST['login'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$password2 = $_POST['password2'] ?? '';
|
||||
|
||||
// Validations de base
|
||||
if ($login === '' || $email === '' || $password === '' || $password2 === '') { $errors[] = "Tous les champs sont obligatoires."; }
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = "Email invalide."; }
|
||||
if ($password !== $password2) { $errors[] = "Les mots de passe ne correspondent pas."; }
|
||||
|
||||
// Si valide, vérifie login/email puis crée l'utilisateur
|
||||
if (!$errors) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM utilisateur WHERE login = :login OR email = :email LIMIT 1");
|
||||
$stmt->execute(['login'=>$login,'email'=>$email]);
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
$errors[] = "Login ou email déjà pris."; // Conflit
|
||||
} else {
|
||||
// Hash sécurisé du mot de passe
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
// Insertion de l'utilisateur en base (rôle par défaut: user)
|
||||
$ins = $pdo->prepare("INSERT INTO utilisateur (login, email, password, role, created_at) VALUES (:login, :email, :password, 'user', NOW())");
|
||||
$ins->execute(['login'=>$login,'email'=>$email,'password'=>$hash]);
|
||||
|
||||
// Connexion automatique après inscription
|
||||
$_SESSION['user_id'] = (int)$pdo->lastInsertId();
|
||||
$_SESSION['login'] = $login;
|
||||
$_SESSION['role'] = 'user';
|
||||
|
||||
|
||||
// Redirection vers le tableau de bord
|
||||
header('Location: /admin/dashboard.php'); exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header
|
||||
require_once __DIR__ . '/../inc/header.php';
|
||||
?>
|
||||
<h2>Inscription</h2>
|
||||
|
||||
<!-- Affichage des erreurs éventuelles -->
|
||||
<?php if ($errors): ?>
|
||||
<div class="error">
|
||||
<?php foreach ($errors as $e): ?>
|
||||
<p><?= e($e) ?></p>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Formulaire d'inscription protégé par CSRF -->
|
||||
<form method="post">
|
||||
<?= csrf_input() ?> <!-- Jeton CSRF -->
|
||||
|
||||
<div class="form-group">
|
||||
<label>Login</label>
|
||||
<input type="text" name="login" value="<?= e($_POST['login'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" name="email" value="<?= e($_POST['email'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Mot de passe</label>
|
||||
<input type="password" name="password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Confirmer le mot de passe</label>
|
||||
<input type="password" name="password2">
|
||||
</div>
|
||||
|
||||
<button class="btn primary" type="submit">Créer mon compte</button>
|
||||
<a class="btn" href="/admin/login.php">J'ai déjà un compte</a>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
// Footer
|
||||
require_once __DIR__ . '/../inc/footer.php';
|
||||
?>
|
||||
22
cms_simplifie/assets/css/style.css
Normal file
22
cms_simplifie/assets/css/style.css
Normal file
@@ -0,0 +1,22 @@
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,'Helvetica Neue',Arial,'Noto Sans',sans-serif;line-height:1.6;background:#f7f7fb;color:#222}
|
||||
a{color:#0a66c2;text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
.container{max-width:980px;margin:0 auto;padding:1rem}
|
||||
.site-header{background:#fff;border-bottom:1px solid #e5e7eb;position:sticky;top:0;z-index:10}
|
||||
.logo a{color:#111;text-decoration:none}
|
||||
.site-header nav a{margin-right:1rem}
|
||||
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem;margin:1rem 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}
|
||||
.card h2{margin-top:.2rem}
|
||||
.btn{display:inline-block;padding:.6rem 1rem;border-radius:10px;border:1px solid #e5e7eb;background:#fff;cursor:pointer}
|
||||
.btn.primary{background:#0a66c2;color:#fff;border-color:#0a66c2}
|
||||
.btn.danger{background:#e11d48;color:#fff;border-color:#e11d48}
|
||||
.form-group{margin-bottom:1rem}
|
||||
input[type=text],input[type=password],input[type=email],textarea{width:100%;padding:.6rem;border:1px solid #d1d5db;border-radius:10px;background:#fff}
|
||||
textarea{min-height:180px}
|
||||
.table{width:100%;border-collapse:collapse}
|
||||
.table th,.table td{border:1px solid #e5e7eb;padding:.6rem;text-align:left}
|
||||
.table th{background:#fafafa}
|
||||
.error,.notice{padding:.8rem 1rem;border-radius:10px;margin:1rem 0}
|
||||
.error{background:#fff1f2;border:1px solid #f43f5e}
|
||||
.notice{background:#ecfeff;border:1px solid #06b6d4}
|
||||
44
cms_simplifie/docker-compose.yml
Normal file
44
cms_simplifie/docker-compose.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: cms_app
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_NAME: cms_simplifie
|
||||
DB_USER: cms
|
||||
DB_PASS: cmspass
|
||||
TZ: Europe/Paris
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
|
||||
|
||||
db:
|
||||
image: mysql:8.4
|
||||
container_name: cms_db
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_DATABASE: cms_simplifie
|
||||
MYSQL_USER: cms
|
||||
MYSQL_PASSWORD: cmspass
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
TZ: Europe/Paris
|
||||
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./sql/dump.sql:/docker-entrypoint-initdb.d/00_dump.sql:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -prootpass || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 7s
|
||||
retries: 12
|
||||
|
||||
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
11
cms_simplifie/inc/config.php
Normal file
11
cms_simplifie/inc/config.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
$env = fn($k, $d=null) => getenv($k) !== false ? getenv($k) : $d;
|
||||
define('DB_HOST', $env('DB_HOST', 'localhost'));
|
||||
define('DB_NAME', $env('DB_NAME', 'cms_simplifie'));
|
||||
define('DB_USER', $env('DB_USER', 'root'));
|
||||
define('DB_PASS', $env('DB_PASS', ''));
|
||||
|
||||
define('BASE_URL', '');
|
||||
define('APP_NAME', 'CMS Simplifié');
|
||||
date_default_timezone_set('Europe/Paris');
|
||||
|
||||
12
cms_simplifie/inc/db.php
Normal file
12
cms_simplifie/inc/db.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
try {
|
||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC
|
||||
]);
|
||||
} catch(PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo "<h1>Erreur DB</h1><p>".htmlspecialchars($e->getMessage())."</p>";
|
||||
exit;
|
||||
}
|
||||
9
cms_simplifie/inc/footer.php
Normal file
9
cms_simplifie/inc/footer.php
Normal file
@@ -0,0 +1,9 @@
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p>© <?= date('Y') ?> <?= e(APP_NAME) ?> — <a href="https://www.basilis-digitale.com" target="_blank" rel="noopener noreferrer">
|
||||
Développé par Elyes Atoui</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
45
cms_simplifie/inc/functions.php
Normal file
45
cms_simplifie/inc/functions.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// Démarre la session si elle n'est pas déjà active
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
|
||||
// Vrai si un utilisateur est connecté
|
||||
function is_logged_in(): bool { return !empty($_SESSION['user_id']); }
|
||||
|
||||
// Renvoie l'ID de l'utilisateur connecté (ou null)
|
||||
function current_user_id(): ?int { return $_SESSION['user_id'] ?? null; }
|
||||
|
||||
// Renvoie le login/pseudo de l'utilisateur (ou null)
|
||||
function current_user_login(): ?string { return $_SESSION['login'] ?? null; }
|
||||
|
||||
// Renvoie le rôle courant (par défaut 'user')
|
||||
function current_user_role(): string { return $_SESSION['role'] ?? 'user'; }
|
||||
|
||||
// Vrai si l'utilisateur courant est admin
|
||||
function is_admin(): bool { return current_user_role() === 'admin'; }
|
||||
|
||||
// Protège une page : redirige vers /admin/login.php si non connecté
|
||||
function require_login(): void { if (!is_logged_in()) { header('Location: /admin/login.php'); exit; } }
|
||||
|
||||
// Échappe le HTML pour éviter les attaque xss
|
||||
function e(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
// Retourne un extrait limité à 150 caractères
|
||||
function excerpt(string $c, int $l=150): string { $c=strip_tags($c); return mb_strlen($c)<= $l? $c: mb_substr($c,0,$l).'…'; }
|
||||
|
||||
// Génère/récupère le jeton CSRF stocké en session
|
||||
function csrf_token(): string { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token']=bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; }
|
||||
|
||||
// Champ caché à insérer dans les formulaires avec le jeton CSRF
|
||||
function csrf_input(): string { return '<input type="hidden" name="csrf" value="'.e(csrf_token()).'">'; }
|
||||
|
||||
// Vérifie le jeton CSRF sur les requêtes POST, sinon bloque avec 400
|
||||
function verify_csrf(): void {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$t = $_POST['csrf'] ?? '';
|
||||
if (!$t || !hash_equals($_SESSION['csrf_token'] ?? '', $t)) {
|
||||
http_response_code(400);
|
||||
echo "<h1>Requête invalide</h1><p>Jeton CSRF manquant ou invalide.</p>";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
cms_simplifie/inc/header.php
Normal file
28
cms_simplifie/inc/header.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/functions.php';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><?= e(APP_NAME) ?></title>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<h1 class="logo"><a href="/public/index.php"><?= e(APP_NAME) ?></a></h1>
|
||||
<nav>
|
||||
<a href="/public/index.php">Accueil</a>
|
||||
<?php if (is_logged_in()): ?>
|
||||
<a href="/admin/dashboard.php">Mon espace</a>
|
||||
<a href="/admin/logout.php">Déconnexion (<?= e(current_user_login()) ?>)</a>
|
||||
<?php else: ?>
|
||||
<a href="/admin/login.php">Connexion</a>
|
||||
<a href="/admin/register.php">Inscription</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container">
|
||||
4
cms_simplifie/index.php
Normal file
4
cms_simplifie/index.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
// Redirige la racine vers la zone publique de l'application
|
||||
header('Location: /public/index.php', true, 302);
|
||||
exit;
|
||||
54
cms_simplifie/public/article.php
Normal file
54
cms_simplifie/public/article.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
// Charge la config, la connexion BDD et le header
|
||||
require_once __DIR__ . '/../inc/config.php';
|
||||
require_once __DIR__ . '/../inc/db.php';
|
||||
require_once __DIR__ . '/../inc/header.php';
|
||||
|
||||
// Récupère et valide l'ID d'article depuis l'URL
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if ($id <= 0) {
|
||||
http_response_code(404);
|
||||
include __DIR__.'/page404.php';
|
||||
require_once __DIR__.'/../inc/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Requête sécurisée : récupère l'article + auteur avec jointure
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT a.id, a.titre, a.contenu, a.date_creation, u.login AS auteur
|
||||
FROM articles a
|
||||
LEFT JOIN utilisateur u ON u.id = a.user_id
|
||||
WHERE a.id = :id
|
||||
");
|
||||
$stmt->execute(['id'=>$id]);
|
||||
$article = $stmt->fetch();
|
||||
|
||||
// Si aucun article trouvé → 404 propre
|
||||
if (!$article) {
|
||||
http_response_code(404);
|
||||
include __DIR__.'/page404.php';
|
||||
require_once __DIR__.'/../inc/footer.php';
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<article class="card">
|
||||
<!-- Titre échappé pour prévenir la XSS -->
|
||||
<h2><?= e($article['titre']) ?></h2>
|
||||
|
||||
<!-- Métadonnées : auteur (fallback Anonyme) + date formatée -->
|
||||
<p class="muted">
|
||||
Par <a href="/public/author.php?login=<?= urlencode($article['auteur'] ?? 'inconnu') ?>"><?= e($article['auteur'] ?? 'Anonyme') ?></a>
|
||||
— <?= e(date('d/m/Y H:i', strtotime($article['date_creation']))) ?>
|
||||
</p>
|
||||
|
||||
<!-- Contenu : échappé puis nl2br pour conserver les retours à la ligne -->
|
||||
<div><?= nl2br(e($article['contenu'])) ?></div>
|
||||
</article>
|
||||
|
||||
<!-- Lien de retour à l'accueil -->
|
||||
<p><a href="/public/index.php">← Retour à l'accueil</a></p>
|
||||
|
||||
<?php
|
||||
// Footer commun (scripts, fermeture des balises)
|
||||
require_once __DIR__ . '/../inc/footer.php';
|
||||
?>
|
||||
50
cms_simplifie/public/author.php
Normal file
50
cms_simplifie/public/author.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
// Charge la config, la connexion BDD et le header
|
||||
require_once __DIR__ . '/../inc/config.php';
|
||||
require_once __DIR__ . '/../inc/db.php';
|
||||
require_once __DIR__ . '/../inc/header.php';
|
||||
|
||||
// Récupère le login depuis la requête et le valide
|
||||
$login = trim($_GET['login'] ?? '');
|
||||
if ($login === '') {
|
||||
include __DIR__.'/page404.php';
|
||||
require_once __DIR__.'/../inc/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Recherche de l'utilisateur par son login (requête préparée)
|
||||
$stmt = $pdo->prepare("SELECT id, login FROM utilisateur WHERE login = :login");
|
||||
$stmt->execute(['login'=>$login]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
// Si l'utilisateur n'existe pas, message simple puis fin
|
||||
if (!$user) {
|
||||
echo "<div class='card'><p>Utilisateur introuvable.</p></div>";
|
||||
require_once __DIR__ . '/../inc/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Titre de page avec le login
|
||||
echo '<h2>Post de ' . e($user['login']) . '</h2>';
|
||||
|
||||
// Récupère les articles de cet auteur (du plus récent au plus ancien)
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, titre, contenu, date_creation
|
||||
FROM articles
|
||||
WHERE user_id = :uid
|
||||
ORDER BY date_creation DESC
|
||||
");
|
||||
$stmt->execute(['uid'=>$user['id']]);
|
||||
|
||||
// affichage des articles (extrait + lien "Lire la suite")
|
||||
foreach ($stmt as $a) {
|
||||
echo '<article class="card">';
|
||||
echo '<h2><a href="/public/article.php?id='.(int)$a['id'].'">'.e($a['titre']).'</a></h2>';
|
||||
echo '<p class="muted">'.e(date('d/m/Y H:i', strtotime($a['date_creation']))).'</p>';
|
||||
echo '<p>'.e(excerpt($a['contenu'])).'</p>';
|
||||
echo '<p><a class="btn" href="/public/article.php?id='.(int)$a['id'].'">Lire la suite</a></p>';
|
||||
echo '</article>';
|
||||
}
|
||||
|
||||
// Footer
|
||||
require_once __DIR__ . '/../inc/footer.php';
|
||||
43
cms_simplifie/public/index.php
Normal file
43
cms_simplifie/public/index.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
// Charge la config, la connexion BDD et le header
|
||||
require_once __DIR__ . '/../inc/config.php';
|
||||
require_once __DIR__ . '/../inc/db.php';
|
||||
require_once __DIR__ . '/../inc/header.php';
|
||||
|
||||
// Récupère les 10 derniers articles avec leur auteur (avec jointure)
|
||||
$stmt = $pdo->query("
|
||||
SELECT a.id, a.titre, a.contenu, a.date_creation, u.login AS auteur
|
||||
FROM articles a
|
||||
LEFT JOIN utilisateur u ON u.id = a.user_id
|
||||
ORDER BY a.date_creation DESC
|
||||
LIMIT 10
|
||||
");
|
||||
$articles = $stmt->fetchAll(); // Tableau d'articles pour l'affichage
|
||||
?>
|
||||
<h2>Derniers posts</h2>
|
||||
|
||||
<?php if (!$articles): ?>
|
||||
<!-- Message si aucun article n'est disponible -->
|
||||
<p>Aucun article pour l'instant.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($articles as $a): ?>
|
||||
<article class="card">
|
||||
<!-- Titre + lien vers la page de l'article -->
|
||||
<h2><a href="/public/article.php?id=<?= (int)$a['id'] ?>"><?= e($a['titre']) ?></a></h2>
|
||||
|
||||
<!-- Métadonnées : auteur + date formatée -->
|
||||
<p class="muted">
|
||||
Par <a href="/public/author.php?login=<?= urlencode($a['auteur'] ?? 'inconnu') ?>"><?= e($a['auteur'] ?? 'Anonyme') ?></a>
|
||||
— <?= e(date('d/m/Y H:i', strtotime($a['date_creation']))) ?>
|
||||
</p>
|
||||
|
||||
<!-- Extrait -->
|
||||
<p><?= e(excerpt($a['contenu'], 150)) ?></p>
|
||||
|
||||
<!-- Lien pour lire l'article complet -->
|
||||
<p><a class="btn" href="/public/article.php?id=<?= (int)$a['id'] ?>">Lire la suite</a></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php require_once __DIR__ . '/../inc/footer.php'; ?>
|
||||
5
cms_simplifie/public/page404.php
Normal file
5
cms_simplifie/public/page404.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="card error">
|
||||
<h2>Erreur 404</h2>
|
||||
<p>La page demandée est introuvable.</p>
|
||||
<p><a href="/public/index.php">Revenir à l'accueil</a></p>
|
||||
</div>
|
||||
27
cms_simplifie/sql/dump.sql
Normal file
27
cms_simplifie/sql/dump.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- MySQL 8.x schema
|
||||
DROP TABLE IF EXISTS articles;
|
||||
DROP TABLE IF EXISTS utilisateur;
|
||||
|
||||
CREATE TABLE utilisateur (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
login VARCHAR(50) NOT NULL UNIQUE,
|
||||
email VARCHAR(190) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','user') NOT NULL DEFAULT 'user',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE articles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NULL,
|
||||
titre VARCHAR(255) NOT NULL,
|
||||
contenu TEXT NOT NULL,
|
||||
date_creation DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_articles_user FOREIGN KEY (user_id) REFERENCES utilisateur(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO articles (titre, contenu) VALUES
|
||||
('Bienvenue', 'Bienvenue sur notre CMS !'),
|
||||
('Ecrire', 'Inscrivez-vous, connectez-vous et redigez vos propres posts.');
|
||||
|
||||
INSERT INTO utilisateur (login, email, password, role) VALUES ('admin','admin@example.com','$2y$10$rZeIxDeW.MM/BWDySZxAmuhd6X/6YHfbpawNMqQjdgAJbHEXmS6Ay', 'admin');
|
||||
Reference in New Issue
Block a user