first commit

This commit is contained in:
mrtoine 2025-09-12 10:57:48 +02:00
commit b216a187bd
34 changed files with 4829 additions and 0 deletions

BIN
admin/.DS_Store vendored Normal file

Binary file not shown.

83
admin/SECURITY_README.md Normal file
View file

@ -0,0 +1,83 @@
# Améliorations de Sécurité - Administration
## 🔒 Corrections Apportées
### 1. Authentification Sécurisée
- ✅ **Hash des mots de passe** : Utilisation de `password_hash()` et `password_verify()`
- ✅ **Protection CSRF** : Tokens CSRF sur tous les formulaires
- ✅ **Limitation des tentatives** : Max 5 tentatives, blocage de 15 minutes
- ✅ **Sessions sécurisées** : Configuration stricte des cookies de session
- ✅ **Timeout de session** : Déconnexion automatique après 1 heure d'inactivité
### 2. Validation et Sanitization
- ✅ **Échappement HTML** : Protection contre XSS
- ✅ **Validation des données** : Vérification des formats et longueurs
- ✅ **Sanitization** : Nettoyage des entrées utilisateur
### 3. Protection des Fichiers
- ✅ **Fichiers sensibles cachés** : `.htaccess` pour protéger config.php
- ✅ **Headers de sécurité** : X-Frame-Options, CSP, etc.
- ✅ **Logs de tentatives** : Enregistrement des tentatives de connexion
## 🚀 Installation
### Étape 1 : Générer le hash du mot de passe
1. Accédez à `admin/generate_password_hash.php`
2. Entrez votre mot de passe actuel
3. Copiez le hash généré
4. **SUPPRIMEZ le fichier** `generate_password_hash.php`
### Étape 2 : Configuration
1. Ouvrez `admin/config.php`
2. Remplacez `ADMIN_PASSWORD_HASH` par le hash généré
3. Ajustez les autres paramètres si nécessaire
### Étape 3 : Test
1. Testez la connexion avec vos identifiants
2. Vérifiez que les tentatives incorrectes sont bloquées
3. Testez la déconnexion sécurisée
## ⚙️ Configuration
### Paramètres dans config.php
```php
define('ADMIN_PASSWORD_HASH', 'votre_hash_ici');
define('SESSION_TIMEOUT', 3600); // 1 heure
define('MAX_LOGIN_ATTEMPTS', 5); // 5 tentatives
define('LOGIN_LOCKOUT_TIME', 900); // 15 minutes
```
## 🔐 Fonctionnalités de Sécurité
### Protection contre les attaques par force brute
- Limitation du nombre de tentatives par IP
- Blocage temporaire après échec
- Délai artificiel sur les tentatives échouées
### Protection CSRF
- Token unique par session
- Vérification sur tous les formulaires sensibles
- Régénération automatique des tokens
### Gestion des sessions
- Configuration sécurisée des cookies
- Régénération périodique des IDs de session
- Nettoyage complet lors de la déconnexion
## 📁 Fichiers Modifiés
- `admin/config.php` - Configuration de sécurité
- `admin/login.php` - Authentification sécurisée
- `admin/logout.php` - Déconnexion sécurisée
- `admin/index.php` - Gestion des sessions
- `admin/projects.php` - Protection CSRF et validation
- `admin/includes/nav.php` - Déconnexion sécurisée
- `admin/.htaccess` - Protection des fichiers
- `admin/generate_password_hash.php` - Générateur de hash (à supprimer)
## ⚠️ Important
1. **Supprimez** `generate_password_hash.php` après utilisation
2. **Sauvegardez** vos données avant mise en production
3. **Testez** toutes les fonctionnalités après déploiement
4. **Surveillez** les logs de tentatives de connexion

119
admin/config.php Normal file
View file

@ -0,0 +1,119 @@
<?php
// Configuration de sécurité
// IMPORTANT: Remplacez le hash ci-dessous par le hash de votre mot de passe
// Utilisez generate_password_hash.php pour générer le hash
define('ADMIN_PASSWORD_HASH', '$2y$10$5L/rCnFdy8GEXBH85Rce.ujXeb9JC1LH0Uvyltx3p2EtbhxXKJpna'); // Hash de "Toine1990@"
define('ADMIN_USERNAME', 'toine');
define('SESSION_TIMEOUT', 3600); // 1 heure
define('MAX_LOGIN_ATTEMPTS', 5);
define('LOGIN_LOCKOUT_TIME', 900); // 15 minutes
// Configuration de session sécurisée - DOIT être appelé AVANT session_start()
function configureSecureSession() {
// Paramètres de sécurité pour les sessions
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', isset($_SERVER['HTTPS']));
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_samesite', 'Strict');
}
// Fonction à appeler APRÈS session_start()
function initSecureSession() {
// Régénérer l'ID de session périodiquement
if (!isset($_SESSION['last_regeneration'])) {
$_SESSION['last_regeneration'] = time();
} elseif (time() - $_SESSION['last_regeneration'] > 300) { // 5 minutes
session_regenerate_id(true);
$_SESSION['last_regeneration'] = time();
}
}
// Fonction pour vérifier les tentatives de connexion
function checkLoginAttempts($ip) {
$attempts_file = __DIR__ . '/login_attempts.json';
if (!file_exists($attempts_file)) {
return true;
}
$attempts_data = file_get_contents($attempts_file);
$attempts = $attempts_data ? json_decode($attempts_data, true) : [];
if (isset($attempts[$ip])) {
$last_attempt = $attempts[$ip]['last_attempt'];
$attempt_count = $attempts[$ip]['count'];
// Si le délai de blocage est écoulé, réinitialiser
if (time() - $last_attempt > LOGIN_LOCKOUT_TIME) {
unset($attempts[$ip]);
file_put_contents($attempts_file, json_encode($attempts));
return true;
}
// Si trop de tentatives
if ($attempt_count >= MAX_LOGIN_ATTEMPTS) {
return false;
}
}
return true;
}
// Fonction pour enregistrer une tentative de connexion
function recordLoginAttempt($ip, $success = false) {
$attempts_file = __DIR__ . '/login_attempts.json';
$attempts_data = file_exists($attempts_file) ? file_get_contents($attempts_file) : '{}';
$attempts = json_decode($attempts_data, true) ?: [];
if ($success) {
// Supprimer les tentatives en cas de succès
if (isset($attempts[$ip])) {
unset($attempts[$ip]);
}
} else {
// Enregistrer la tentative échouée
$attempts[$ip] = [
'count' => ($attempts[$ip]['count'] ?? 0) + 1,
'last_attempt' => time()
];
}
file_put_contents($attempts_file, json_encode($attempts));
}
// Générer un token CSRF
function generateCSRFToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Vérifier le token CSRF
function verifyCSRFToken($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
// Vérifier si l'utilisateur est connecté et la session est valide
function isAuthenticated() {
if (!isset($_SESSION['connected']) || !$_SESSION['connected']) {
return false;
}
// Vérifier le timeout de session
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > SESSION_TIMEOUT)) {
session_destroy();
return false;
}
// Mettre à jour l'activité
$_SESSION['last_activity'] = time();
return true;
}
// Nettoyer et valider les données d'entrée
function sanitizeInput($data) {
return htmlspecialchars(trim($data), ENT_QUOTES, 'UTF-8');
}
?>

111
admin/contact.php Normal file
View file

@ -0,0 +1,111 @@
<?php
require_once './includes/nav.php';
require_once 'config.php';
// Vérifier l'authentification
if (!isAuthenticated()) {
header("Location: ?page=login");
exit;
}
$message = "";
$firstname = "";
$lastname = "";
$email = "";
$gsm = "";
$linkedin = "";
$twitter = "";
$github = "";
if($_POST){
if (!isset($_POST['csrf_token']) || !verifyCSRFToken($_POST['csrf_token'])) {
echo "<div class='alert alert-error'>Token de sécurité invalide.</div>";
} else {
$firstname = sanitizeInput($_POST['firstname']);
$lastname = sanitizeInput($_POST['lastname']);
$email = sanitizeInput($_POST['email']);
$gsm = sanitizeInput($_POST['gsm']);
$linkedin = sanitizeInput($_POST['linkedin']);
$twitter = sanitizeInput($_POST['twitter']);
$github = sanitizeInput($_POST['github']);
$message = '<div class="alert alert-success">Formulaire soumis</div>';
$jsonFile = '../data/contacts.json';
if(file_exists($jsonFile)) {
$content = file_get_contents($jsonFile);
$contact = $content;
if($contact) {
$contact = json_decode($content, true);
}
$updatedContact = [
'firstname' => $firstname,
'lastname' => $lastname,
'email' => $email,
'gsm' => $gsm,
'linkedin' => $linkedin,
'twitter' => $twitter,
'github' => $github
];
$contact = array_merge($contact, $updatedContact);
if (file_put_contents($jsonFile, json_encode($contact, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
$message = '<div class="alert alert-success">Données de contact mises à jour avec succès !</div>';
} else {
$message = '<div class="alert alert-error">Erreur lors de la mise à jour des données de contact.</div>';
}
}
}
} else {
// Charger les données de contact existantes
$jsonFile = '../data/contacts.json';
if(file_exists($jsonFile)) {
$content = file_get_contents($jsonFile);
if($content) {
$contact = json_decode($content, true);
$firstname = $contact['firstname'] ?? '';
$lastname = $contact['lastname'] ?? '';
$email = $contact['email'] ?? '';
$gsm = $contact['gsm'] ?? '';
$linkedin = $contact['linkedin'] ?? '';
$twitter = $contact['twitter'] ?? '';
$github = $contact['github'] ?? '';
}
}
}
?>
<section>
<div class="dashboard">
<h1>Données de contacts</h1>
<p>Les données de contacts affichées ici sont reprise sur le site dans la rubrique contact</p>
<div class="form-project">
<form action="" method="post">
<?= $message; ?>
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCSRFToken()); ?>">
<div class="form-group">
<input type="text" name="firstname" placeholder="Prénom" value="<?= $firstname; ?>" />
</div>
<div class="form-group">
<input type="text" name="lastname" placeholder="Nom de famille" value="<?= $lastname; ?>" />
</div>
<div class="form-group">
<input type="email" name="email" placeholder="Adresse email" value="<?= $email; ?>" />
</div>
<div class="form-group">
<input type="tel" name="gsm" placeholder="Numéro de téléphone" value="<?= $gsm; ?>" />
</div>
<div class="form-group">
<input type="text" name="linkedin" placeholder="LinkedIn" value="<?= $linkedin; ?>" />
</div>
<div class="form-group">
<input type="text" name="twitter" placeholder="X (Twitter)" value="<?= $twitter; ?>" />
</div>
<div class="form-group">
<input type="text" name="github" placeholder="Github" value="<?= $github; ?>" />
</div>
<button type="submit" class="btn-success">Mettre à jour</button>
</form>
</div>
</div>
</section>

34
admin/delete-project.php Normal file
View file

@ -0,0 +1,34 @@
<?php
require_once './includes/nav.php';
$id = isset($_GET['id']) ? sanitizeInput($_GET['id']) : null;
if (!$id) {
echo "<div class='alert alert-error'>ID de projet manquant.</div>";
exit;
}
$jsonFile = '../data/projects.json';
if(file_exists($jsonFile)) {
$content = file_get_contents($jsonFile);
$projects = $content ? json_decode($content, true) : [];
} else {
$projects = [];
}
// Check if project exists
$projectKey = array_search($id, array_column($projects, 'id'));
if ($projectKey === false) {
echo "<div class='alert alert-error'>Projet non trouvé.</div>";
exit;
}
// On supprime le projet
unset($projects[$projectKey]);
// Reindex the array
$projects = array_values($projects);
// Save updated projects back to the JSON file
if (file_put_contents($jsonFile, json_encode($projects, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
echo "<div class='alert alert-success'>Projet supprimé avec succès !</div>";
} else {
echo "<div class='alert alert-error'>Erreur lors de la suppression du projet.</div>";
}
?>

View file

@ -0,0 +1,60 @@
<?php
// Script à exécuter UNE FOIS pour générer le hash de votre mot de passe
// Ensuite, supprimez ce fichier ou déplacez-le hors du répertoire web
echo "<h2>Générateur de hash pour mot de passe</h2>";
if ($_POST && isset($_POST['password'])) {
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
echo "<div style='background: #f0f8ff; padding: 15px; border: 1px solid #0066cc; margin: 10px 0;'>";
echo "<strong>Hash généré :</strong><br>";
echo "<code style='background: #e8e8e8; padding: 5px; word-break: break-all;'>" . htmlspecialchars($hash) . "</code><br><br>";
echo "<strong>Copiez ce hash dans config.php à la place de ADMIN_PASSWORD_HASH</strong>";
echo "</div>";
// Vérification
if (password_verify($password, $hash)) {
echo "<p style='color: green;'>✓ Vérification réussie - Le hash fonctionne correctement</p>";
} else {
echo "<p style='color: red;'>✗ Erreur de vérification</p>";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Générateur de Hash</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
input, button { padding: 10px; margin: 5px 0; }
input[type="password"] { width: 300px; }
button { background: #0066cc; color: white; border: none; cursor: pointer; }
button:hover { background: #0052a3; }
.warning { background: #fff3cd; padding: 15px; border: 1px solid #ffeaa7; color: #856404; margin: 20px 0; }
</style>
</head>
<body>
<div class="warning">
<strong>⚠️ ATTENTION :</strong> Supprimez ce fichier après utilisation pour des raisons de sécurité !
</div>
<form method="post">
<label for="password">Entrez votre mot de passe :</label><br>
<input type="password" name="password" id="password" required><br>
<button type="submit">Générer le hash</button>
</form>
<div style="margin-top: 30px; font-size: 14px; color: #666;">
<h3>Instructions :</h3>
<ol>
<li>Entrez votre mot de passe ci-dessus</li>
<li>Copiez le hash généré</li>
<li>Remplacez la valeur de ADMIN_PASSWORD_HASH dans config.php</li>
<li><strong>Supprimez ce fichier (generate_password_hash.php)</strong></li>
</ol>
</div>
</body>
</html>

61
admin/home.php Normal file
View file

@ -0,0 +1,61 @@
<?php require_once './includes/nav.php'; ?>
<section>
<div class="dashboard">
<h1>Tableau de bord</h1>
<p class="welcome-message">Bienvenue dans l'espace d'administration de votre site personnel.</p>
<div class="dashboard-grid">
<div class="dashboard-card">
<h3>🎨 Projets</h3>
<p>Gérez vos projets et réalisations</p>
<a href="./?page=projects" class="btn-primary">Accéder</a>
</div>
<div class="dashboard-card">
<h3>📧 Contacts</h3>
<p>Consultez les données de contact</p>
<a href="./?page=contact" class="btn-primary">Accéder</a>
</div>
<div class="dashboard-card">
<h3>📊 Statistiques</h3>
<div class="stats">
<div class="stat-item">
<span class="stat-number">5</span>
<span class="stat-label">Projets actifs</span>
</div>
<div class="stat-item">
<span class="stat-number">12</span>
<span class="stat-label">Messages reçus</span>
</div>
</div>
</div>
<div class="dashboard-card">
<h3>🚀 Actions rapides</h3>
<div class="quick-actions">
<a href="../index.html" class="btn-secondary">Voir le site</a>
<a href="./logout.php" class="btn-danger">Se déconnecter</a>
</div>
</div>
</div>
<!--<div class="recent-activity">
<h3>Activité récente</h3>
<div class="activity-list">
<div class="activity-item">
<span class="activity-date">Aujourd'hui</span>
<span class="activity-desc">Connexion à l'administration</span>
</div>
<div class="activity-item">
<span class="activity-date">Hier</span>
<span class="activity-desc">Nouveau message de contact reçu</span>
</div>
<div class="activity-item">
<span class="activity-date">3 jours</span>
<span class="activity-desc">Projet "Portfolio React" mis à jour</span>
</div>
</div>
</div>-->
</div>
</section>

37
admin/htaccess Normal file
View file

@ -0,0 +1,37 @@
# Sécurisation du dossier d'administration
# Cacher les fichiers sensibles
<Files "config.php">
Require all denied
</Files>
<Files "login_attempts.json">
Require all denied
</Files>
<Files "generate_password_hash.php">
Require all denied
</Files>
# Protection contre les attaques par force brute
<RequireAll>
Require all granted
# Limiter les requêtes POST (optionnel, à configurer selon vos besoins)
</RequireAll>
# Headers de sécurité
<IfModule mod_headers.c>
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
</IfModule>
# Désactiver l'affichage des erreurs PHP en production
php_flag display_errors off
php_flag log_errors on
# Limiter la taille des uploads
php_value upload_max_filesize 10M
php_value post_max_size 10M

15
admin/includes/nav.php Normal file
View file

@ -0,0 +1,15 @@
<nav>
<ul>
<li class="title">Administration</li>
<li class="nav-item"><a href="./?page=home">Accueil</a></li>
<li class="nav-item"><a href="./?page=projects">Les projets</a></li>
<li class="nav-item"><a href="./?page=contact">Données de contacts</a></li>
<li class="nav-item"><a href="../index.html">Retour au site</a></li>
<li class="nav-item">
<form method="post" action="./logout.php" style="display: inline;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCSRFToken()); ?>">
<button type="submit" style="background: none; border: none; color: inherit; text-decoration: underline; cursor: pointer;">Déconnexion</button>
</form>
</li>
</ul>
</nav>

36
admin/index.php Normal file
View file

@ -0,0 +1,36 @@
<?php
require_once 'init.php';
$connected = "false";
if (!isset($_GET["page"])) {
header("Location: ?page=home");
exit;
}
$page = sanitizeInput($_GET["page"]);
// Vérifier l'authentification
if (isAuthenticated()) {
$admin = array(
"username" => $_SESSION['username']
);
$connected = "true";
} else {
$page = "login";
}
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../Static/css/admin.css">
<script type="module" src="../Static/js/admin.js" defer></script>
<title>Administration</title>
</head>
<body<?= ($page == "login") ? " class='login-page'" : "" ?>>
<?php require_once $page.".php" ?>
</body>
</html>

22
admin/init.php Normal file
View file

@ -0,0 +1,22 @@
<?php
// Fichier d'initialisation global pour l'administration
// Ce fichier doit être inclus au début de chaque page admin
require_once __DIR__ . '/config.php';
// Fonction pour démarrer une session sécurisée
function startSecureSession() {
// Ne démarrer la session que si elle n'est pas déjà active
if (session_status() === PHP_SESSION_NONE) {
// Configurer la session AVANT de la démarrer
configureSecureSession();
session_start();
// Initialiser après le démarrage
initSecureSession();
}
}
// Démarrer la session sécurisée
startSecureSession();
?>

61
admin/login.php Normal file
View file

@ -0,0 +1,61 @@
<?php
require_once 'config.php';
$msg = "";
$client_ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// Vérifier si l'IP est bloquée
if (!checkLoginAttempts($client_ip)) {
$remaining_time = LOGIN_LOCKOUT_TIME - (time() - json_decode(file_get_contents(__DIR__ . '/login_attempts.json'), true)[$client_ip]['last_attempt']);
$msg = '<span style="color:red">Trop de tentatives de connexion. Réessayez dans ' . ceil($remaining_time / 60) . ' minutes.</span>';
} else {
// Traitement du formulaire de connexion
if ($_POST && isset($_POST['username']) && isset($_POST['userPassword']) && isset($_POST['csrf_token'])) {
// Vérifier le token CSRF
if (!verifyCSRFToken($_POST['csrf_token'])) {
$msg = '<span style="color:red">Token de sécurité invalide. Veuillez réessayer.</span>';
} else {
$username = sanitizeInput($_POST['username']);
$password = $_POST['userPassword'];
// Vérifier les identifiants
if ($username === ADMIN_USERNAME && password_verify($password, ADMIN_PASSWORD_HASH)) {
// Connexion réussie
session_regenerate_id(true);
$_SESSION['connected'] = true;
$_SESSION['username'] = $username;
$_SESSION['last_activity'] = time();
// Supprimer les tentatives de connexion en cas de succès
recordLoginAttempt($client_ip, true);
$msg = '<span style="color:green">Connexion réussie !</span><br><a href="?page=home">Accéder à l\'administration</a>';
} else {
// Connexion échouée
recordLoginAttempt($client_ip, false);
$msg = '<span style="color:red">Identifiants incorrects.</span>';
// Ajouter un délai pour ralentir les attaques par force brute
sleep(2);
}
}
}
}
?>
<section class="login" id="admin-sanctuary">
<h2>Connexion</h2>
<?= $msg; ?>
<form method="post" action="./?page=login" style="margin-top: 1rem;" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCSRFToken()); ?>">
<div style="margin-bottom: 1.5rem;">
<label for="username" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Utilisateur</label>
<input type="text" name="username" id="username" placeholder="Utilisateur" required autocomplete="username" />
</div>
<div style="margin-bottom: 1.5rem;">
<label for="userPassword" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Mot de passe</label>
<input type="password" name="userPassword" id="userPassword" placeholder="Mot de passe" required autocomplete="current-password" />
</div>
<button type="submit" class="btn" id="admin-login">Se connecter</button>
</form>
</section>

View file

@ -0,0 +1 @@
[]

27
admin/logout.php Normal file
View file

@ -0,0 +1,27 @@
<?php
require_once 'init.php';
// Vérifier le token CSRF pour la déconnexion
if ($_POST && isset($_POST['csrf_token'])) {
if (verifyCSRFToken($_POST['csrf_token'])) {
// Nettoyer complètement la session
$_SESSION = array();
// Détruire le cookie de session
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-3600, '/');
}
// Détruire la session
session_destroy();
// Redirection sécurisée
header("Location: ../index.html");
exit;
}
}
// Si pas de POST ou token invalide, rediriger vers l'admin
header("Location: ?page=home");
exit;
?>

126
admin/projects.php Normal file
View file

@ -0,0 +1,126 @@
<?php
require_once './includes/nav.php';
if($_POST) {
// Vérifier le token CSRF
if (!isset($_POST['csrf_token']) || !verifyCSRFToken($_POST['csrf_token'])) {
echo "<div class='alert alert-error'>Token de sécurité invalide.</div>";
} else {
// Handle form submission for project creation
if(isset($_POST['name']) && isset($_POST['description']) && isset($_POST['start_date'])) {
$type = sanitizeInput($_POST['type']);
$name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']);
$start_date = sanitizeInput($_POST['start_date']);
$end_date = isset($_POST['end_date']) && !empty($_POST['end_date']) ? sanitizeInput($_POST['end_date']) : null;
$link = isset($_POST['link']) && !empty($_POST['link']) ? sanitizeInput($_POST['link']) : null;
$technologies = isset($_POST['technologies']) && !empty($_POST['technologies']) ?
array_map('trim', explode(',', sanitizeInput($_POST['technologies']))) : [];
$tags = isset($_POST['tags']) && !empty($_POST['tags']) ?
array_map('trim', explode(',', sanitizeInput($_POST['tags']))) : [];
// Validation des données
if (empty($type) || empty($name) || empty($description) || empty($start_date)) {
echo "<div class='alert alert-error'>Tous les champs obligatoires doivent être remplis.</div>";
} elseif (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date)) {
echo "<div class='alert alert-error'>Format de date de début invalide.</div>";
} elseif ($end_date && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $end_date)) {
echo "<div class='alert alert-error'>Format de date de fin invalide.</div>";
} else {
$jsonFile = '../data/projects.json';
// Read existing projects
if(file_exists($jsonFile)) {
$content = file_get_contents($jsonFile);
$projects = $content ? json_decode($content, true) : [];
} else {
$projects = [];
}
// Create new project entry
$newProject = [
'id' => uniqid(),
'type' => $type,
'name' => $name,
'description' => $description,
'link' => $link,
'technologies' => $technologies,
'start_date' => $start_date,
'end_date' => $end_date,
'created_at' => date('Y-m-d H:i:s'),
'active' => true
];
// Add new project to the list
$projects[] = $newProject;
// Save updated projects back to the JSON file
if (file_put_contents($jsonFile, json_encode($projects, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
echo "<div class='alert alert-success'>Projet '$name' créé avec succès !</div>";
// Reset form fields
unset($_POST);
} else {
echo "<div class='alert alert-error'>Erreur lors de la sauvegarde du projet.</div>";
}
}
}
}
}
?>
<section>
<div class="dashboard" data-type="projects">
<div class="projects-actions">
<h1>Projets</h1>
<a href="#" class="btn-success" data-id="creation-project-btn">Ajouter projet</a>
</div>
<div class="form-project hidden">
<form action="" method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCSRFToken()); ?>">
<div class="form-group">
<label for="project-type">Type du projet *</label>
<input type="text" id="project-type" name="type" placeholder="Ex: Site vitrine e-commerce" required maxlength="100">
</div>
<div class="form-group">
<label for="project-name">Nom du projet *</label>
<input type="text" id="project-name" name="name" placeholder="Ex: Sandwicherie" required maxlength="150">
</div>
<div class="form-group">
<label for="project-description">Description *</label>
<textarea id="project-description" name="description" placeholder="Décrivez votre projet, les technologies utilisées, les défis relevés..." required maxlength="1000"></textarea>
</div>
<div class="form-group">
<label for="project-image">Lien du projet</label>
<input type="url" id="project-link" name="link" placeholder="https://exemple.com/projet" maxlength="255">
</div>
<div class="form-group">
<label for="project-technologies">Technologies utilisées *</label>
<div class="technologies-grid">
<!-- Le contenu sera généré par JavaScript -->
<div class="loading-technologies">
<p>Chargement des technologies...</p>
</div>
</div>
<input type="hidden" id="selected-technologies" name="technologies">
</div>
<div class="form-group">
<label for="project-tags">Tags (séparés par des virgules)</label>
<input type="text" id="project-tags" name="tags" placeholder="Ex: responsive, moderne, e-commerce" maxlength="200">
<small class="form-help">Ajoutez des mots-clés pour décrire votre projet (séparés par des virgules)</small>
</div>
<div class="form-group">
<label for="project-start-date">Date de début *</label>
<input type="date" id="project-start-date" name="start_date" required>
</div>
<div class="form-group">
<label for="project-end-date">Date de fin</label>
<input type="date" id="project-end-date" name="end_date">
</div>
<button type="submit" class="btn-success">Enregistrer</button>
</form>
</div>
<div data-id="projects">
<div class="projects-grid"></div>
</div>
</div>
</section>
<script src="../Static/js/technologies.js"></script>