first commit
This commit is contained in:
commit
b216a187bd
34 changed files with 4829 additions and 0 deletions
BIN
admin/.DS_Store
vendored
Normal file
BIN
admin/.DS_Store
vendored
Normal file
Binary file not shown.
83
admin/SECURITY_README.md
Normal file
83
admin/SECURITY_README.md
Normal 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
119
admin/config.php
Normal 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
111
admin/contact.php
Normal 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
34
admin/delete-project.php
Normal 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>";
|
||||
}
|
||||
?>
|
||||
60
admin/generate_password_hash.php
Normal file
60
admin/generate_password_hash.php
Normal 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
61
admin/home.php
Normal 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
37
admin/htaccess
Normal 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
15
admin/includes/nav.php
Normal 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
36
admin/index.php
Normal 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
22
admin/init.php
Normal 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
61
admin/login.php
Normal 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>
|
||||
1
admin/login_attempts.json
Normal file
1
admin/login_attempts.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
27
admin/logout.php
Normal file
27
admin/logout.php
Normal 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
126
admin/projects.php
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue