Initialisation du projet Bolsa

This commit is contained in:
2026-07-01 10:56:55 +00:00
commit 849d7f1350
107 changed files with 14835 additions and 0 deletions
Executable
+16
View File
@@ -0,0 +1,16 @@
<?php
// On envoie explicitement le code d'erreur 404 au navigateur
http_response_code(404);
require_once('header.php');
?>
<div style="text-align: center; padding: 50px; color: white;">
<h1>Erreur 404</h1>
<p>Désolé, la page que vous recherchez est introuvable.</p>
<a href="/index.php" style="color: #007bff;">Retour à l'accueil</a>
</div>
<?php
require_once('footer.php');
?>
+366
View File
@@ -0,0 +1,366 @@
<?php
namespace App\Database;
use PDO;
use PDOException;
use RuntimeException;
/**
* Class Database
*
* Gère la connexion PDO à la base de données MySQL.
*/
class Database
{
private PDO $connection;
public const DB_HOST = '127.0.0.1';
public const DB_NAME = 'bolsa';
public const DB_USER = 'root';
public const DB_PASS = 'sysadm-1963';
public const DB_CHARSET = 'utf8mb4';
/**
* Database constructor.
*
* Initialise la connexion PDO.
*
* @throws RuntimeException
*/
public function __construct()
{
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
self::DB_HOST,
self::DB_NAME,
self::DB_CHARSET
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$this->connection = new PDO($dsn, self::DB_USER, self::DB_PASS, $options);
} catch (PDOException $exception) {
throw new RuntimeException('Connexion à la base de données impossible : ' . $exception->getMessage());
}
}
/**
* Retourne l'objet PDO.
*
* @return PDO
*/
public function getConnection(): PDO
{
return $this->connection;
}
/**
* Ferme la connexion PDO.
*
* @return void
*/
public function disconnect(): void
{
$this->connection = null;
}
}
/**
* Class LoginManager
*
* Fournit les opérations CRUD pour la table `login`.
*/
class LoginManager
{
private PDO $connection;
public function __construct(Database $database)
{
$this->connection = $database->getConnection();
}
/**
* Crée un nouvel utilisateur dans la table login.
*
* @param array $data
* @return int L'ID généré de l'utilisateur.
*/
public function create(array $data): int
{
$statement = $this->connection->prepare(
'INSERT INTO login (login, nom, prenom, mail, pass, actif, creer_le, modifie_le)
VALUES (:login, :nom, :prenom, :mail, :pass, :actif, NOW(), NOW())'
);
$hashedPassword = password_hash($data['pass'], PASSWORD_DEFAULT);
$actif = array_key_exists('actif', $data) ? (int) $data['actif'] : 0;
$statement->bindValue(':login', $data['login'], PDO::PARAM_STR);
$statement->bindValue(':nom', $data['nom'], PDO::PARAM_STR);
$statement->bindValue(':prenom', $data['prenom'], PDO::PARAM_STR);
$statement->bindValue(':mail', $data['mail'], PDO::PARAM_STR);
$statement->bindValue(':pass', $hashedPassword, PDO::PARAM_STR);
$statement->bindValue(':actif', $actif, PDO::PARAM_INT);
$statement->execute();
return (int) $this->connection->lastInsertId();
}
/**
* Lit un utilisateur par son ID.
*
* @param int $id
* @return array|null
*/
public function read(int $id): ?array
{
$statement = $this->connection->prepare(
'SELECT id, login, nom, prenom, mail, creer_le, modifie_le
FROM login
WHERE id = :id'
);
$statement->bindValue(':id', $id, PDO::PARAM_INT);
$statement->execute();
$result = $statement->fetch();
return $result === false ? null : $result;
}
/**
* Met à jour un utilisateur.
*
* @param int $id
* @param array $data
* @return bool
*/
public function update(int $id, array $data): bool
{
$fields = [];
$params = [':id' => $id];
if (isset($data['login'])) {
$fields[] = 'login = :login';
$params[':login'] = $data['login'];
}
if (isset($data['nom'])) {
$fields[] = 'nom = :nom';
$params[':nom'] = $data['nom'];
}
if (isset($data['prenom'])) {
$fields[] = 'prenom = :prenom';
$params[':prenom'] = $data['prenom'];
}
if (isset($data['mail'])) {
$fields[] = 'mail = :mail';
$params[':mail'] = $data['mail'];
}
if (isset($data['pass'])) {
$fields[] = 'pass = :pass';
$params[':pass'] = password_hash($data['pass'], PASSWORD_DEFAULT);
}
if (empty($fields)) {
return false;
}
$fields[] = 'modifie_le = NOW()';
$statement = $this->connection->prepare(
sprintf('UPDATE login SET %s WHERE id = :id', implode(', ', $fields))
);
foreach ($params as $param => $value) {
$statement->bindValue($param, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR);
}
return $statement->execute();
}
/**
* Supprime un utilisateur par son ID.
*
* @param int $id
* @return bool
*/
public function delete(int $id): bool
{
$statement = $this->connection->prepare('DELETE FROM login WHERE id = :id');
$statement->bindValue(':id', $id, PDO::PARAM_INT);
return $statement->execute();
}
/**
* Retourne tous les utilisateurs.
*
* @return array
*/
public function list(): array
{
$statement = $this->connection->query(
'SELECT id, login, nom, prenom, mail, creer_le, modifie_le FROM login ORDER BY id ASC'
);
return $statement->fetchAll();
}
/**
* Retourne un utilisateur par son login.
*
* @param string $login
* @return array|null
*/
public function getByLogin(string $login): ?array
{
$statement = $this->connection->prepare(
'SELECT id, login, nom, prenom, mail, pass, actif, creer_le, modifie_le
FROM login
WHERE login = :login'
);
$statement->bindValue(':login', $login, PDO::PARAM_STR);
$statement->execute();
$result = $statement->fetch();
return $result === false ? null : $result;
}
/**
* Vérifie si le mot de passe correspond au login.
*
* @param string $login
* @param string $password
* @return bool
*/
public function verifyPasswordByLogin(string $login, string $password): bool
{
$user = $this->getByLogin($login);
if ($user === null) {
return false;
}
return ((int) ($user['actif'] ?? 0) === 1) && password_verify($password, $user['pass']);
}
/**
* Vérifie si un login ou un email existe déjà.
*
* @param string $login
* @param string $email
* @return bool
*/
public function existsByLoginOrEmail(string $login, string $email): bool
{
$statement = $this->connection->prepare(
'SELECT COUNT(*) AS total
FROM login
WHERE login = :login OR mail = :mail'
);
$statement->bindValue(':login', $login, PDO::PARAM_STR);
$statement->bindValue(':mail', $email, PDO::PARAM_STR);
$statement->execute();
$result = $statement->fetch();
return isset($result['total']) && (int) $result['total'] > 0;
}
/**
* Vérifie si une adresse e-mail existe.
*
* @param string $email
* @return bool
*/
public function existsByEmail(string $email): bool
{
$statement = $this->connection->prepare(
'SELECT COUNT(*) AS total FROM login WHERE mail = :mail'
);
$statement->bindValue(':mail', $email, PDO::PARAM_STR);
$statement->execute();
$result = $statement->fetch();
return isset($result['total']) && (int) $result['total'] > 0;
}
/**
* Définit un token de réinitialisation pour un utilisateur identifié par son e-mail.
*
* @param string $email
* @param string $token
* @return bool
*/
public function setResetTokenByEmail(string $email, string $token): bool
{
$statement = $this->connection->prepare(
'UPDATE login SET reset_token = :token, reset_token_created_at = NOW(), modifie_le = NOW() WHERE mail = :mail'
);
$statement->bindValue(':token', $token, PDO::PARAM_STR);
$statement->bindValue(':mail', $email, PDO::PARAM_STR);
return $statement->execute();
}
/**
* Retourne un utilisateur identifié par son token de réinitialisation.
*
* @param string $token
* @return array|null
*/
public function getByResetToken(string $token): ?array
{
$statement = $this->connection->prepare(
'SELECT id, login, nom, prenom, mail, pass, actif, reset_token, reset_token_created_at, creer_le, modifie_le
FROM login
WHERE reset_token = :token
AND reset_token_created_at >= NOW() - INTERVAL 30 MINUTE'
);
$statement->bindValue(':token', $token, PDO::PARAM_STR);
$statement->execute();
$result = $statement->fetch();
return $result === false ? null : $result;
}
/**
* Met à jour le mot de passe d'un utilisateur et supprime le token de réinitialisation.
*
* @param int $id
* @param string $password
* @return bool
*/
public function updatePasswordById(int $id, string $password): bool
{
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$statement = $this->connection->prepare(
'UPDATE login SET pass = :pass, reset_token = NULL, reset_token_created_at = NULL, modifie_le = NOW() WHERE id = :id'
);
$statement->bindValue(':pass', $hashedPassword, PDO::PARAM_STR);
$statement->bindValue(':id', $id, PDO::PARAM_INT);
return $statement->execute();
}
}
Executable
+9
View File
@@ -0,0 +1,9 @@
</main>
<footer class="footer">
<?php if (isset($_SESSION['user_id'])): ?>
<a class="logout-button footer-logout" href="/php/menu.php?action=logout">Se déconnecter</a>
<?php endif; ?>
<p>&copy; 2026 - Jean-François GIRAUD - Positions en bourse - Tous droits réservés</p>
</footer>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
session_start();
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/db.php';
use App\Database\Database;
use App\Database\LoginManager;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;
$errors = [];
$successMessage = '';
$email = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim((string) filter_input(INPUT_POST, 'mail', FILTER_SANITIZE_EMAIL));
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Veuillez saisir une adresse e-mail valide.';
} else {
try {
$database = new Database();
$loginManager = new LoginManager($database);
if (!$loginManager->existsByEmail($email)) {
$errors[] = 'Aucun compte nest associé à cette adresse e-mail.';
} else {
$resetToken = bin2hex(random_bytes(32));
if (!$loginManager->setResetTokenByEmail($email, $resetToken)) {
$errors[] = 'Erreur interne : impossible de générer le token de réinitialisation.';
} else {
$resetLink = sprintf(
'https://www.giraud-finance.com/php/reset_password.php?token=%s',
urlencode($resetToken)
);
try {
$mailer = new PHPMailer(true);
$mailer->isSMTP();
$mailer->Host = 'mx.giraud-finance.com';
$mailer->SMTPAuth = true;
$mailer->Username = 'jfgiraud@giraud-finance.com';
$mailer->Password = 'azertyQSDFGH123456+-';
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mailer->Port = 587;
$mailer->CharSet = 'UTF-8';
$mailer->Encoding = 'base64';
$mailer->setFrom('postmaster@giraud-finance.com', 'Giraud Finance');
$mailer->addAddress($email);
$mailer->Subject = 'Réinitialisation de votre mot de passe - Giraud Finance';
$mailer->isHTML(true);
$mailer->Body = sprintf(
'<p>Bonjour,</p><p>Pour réinitialiser votre mot de passe, veuillez cliquer sur le lien suivant :</p><p><a href="%1$s">%1$s</a></p><p>Ce lien est valide pendant 30 minutes.</p><p>Si vous n\'avez pas demandé cette réinitialisation, ignorez ce message.</p>',
htmlspecialchars($resetLink, ENT_QUOTES, 'UTF-8')
);
$mailer->AltBody = "Bonjour,\n\nPour réinitialiser votre mot de passe, veuillez utiliser ce lien : $resetLink\n\nCe lien est valide pendant 30 minutes.\n\nSi vous n\'avez pas demandé cette réinitialisation, ignorez ce message.";
$mailer->send();
$successMessage = 'Nous avons bien pris en compte votre demande. Un lien de réinitialisation a été envoyé si votre adresse existe.';
} catch (Exception $mailException) {
$errors[] = 'Impossible d\'envoyer l\'e-mail de réinitialisation : ' . $mailException->getMessage();
}
}
}
} catch (Throwable $exception) {
$errors[] = 'Erreur interne : impossible de traiter la demande pour le moment.';
}
}
}
?>
<?php require_once __DIR__ . '/header.php'; ?>
<div class="login-container">
<div class="login-box">
<h2 class="login-title">Mot de passe oublié</h2>
<p class="login-subtitle">Renseignez votre adresse e-mail pour réinitialiser votre mot de passe.</p>
<?php if (!empty($errors)): ?>
<div class="alert alert-error">
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($successMessage): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($successMessage, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
<form action="forgot_password.php" method="POST" class="login-form">
<div class="form-group">
<label for="mail" class="form-label">Adresse e-mail</label>
<input type="email" id="mail" name="mail" class="form-input" placeholder="exemple@email.com" required autocomplete="email" value="<?php echo htmlspecialchars($email, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<button type="submit" class="btn-submit">Envoyer la demande</button>
</form>
<div class="login-footer">
<span class="separator">•</span>
<a href="../index.php" class="signup-link">Retour à la connexion</a>
</div>
</div>
</div>
<?php require_once __DIR__ . '/footer.php'; ?>
Executable
+26
View File
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bourse - Page d'Accueil</title>
<link rel="stylesheet" href="/css/style.css">
<link rel="shortcut icon" href="/assets/images/favicon.png" type="image/x-icon">
</head>
<body>
<header class="header">
<div class="container">
<div class="logo">
<img src="/assets/images/GFBlancLogo.png" alt="Logo GF Blanc" class="logo-img">
</div>
<h1 class="site-title">Positions en bourse</h1>
<?php if (isset($_SESSION['user_id'])): ?>
<div class="user-info">
<span>Nom : <?= htmlspecialchars((string) ($_SESSION['user_name'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span>IP : <?= htmlspecialchars((string) ($_SERVER['REMOTE_ADDR'] ?? 'N/A'), ENT_QUOTES, 'UTF-8') ?></span>
<span class="info-date">Date : <?= date('d/m/Y') ?></span>
</div>
<?php endif; ?>
</div>
</header>
<main class="main-content">
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/session.php';
use App\Database\Database;
use App\Database\LoginManager;
if (!isset($_SESSION['login_attempts'])) {
$_SESSION['login_attempts'] = 0;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../index.php');
exit;
}
$login = trim((string) filter_input(INPUT_POST, 'login', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
$password = trim((string) ($_POST['password'] ?? ''));
if ($login === '' || $password === '') {
$_SESSION['login_error'] = 'Veuillez renseigner votre login et votre mot de passe.';
header('Location: ../index.php');
exit;
}
$handleFailedAttempt = function (string $message): void {
$attempts = (int) ($_SESSION['login_attempts'] ?? 0) + 1;
$_SESSION['login_attempts'] = $attempts;
if ($attempts >= 5) {
$_SESSION['blocked'] = true;
header('Location: https://home.giraud-finance.com');
exit;
}
$_SESSION['login_error'] = $message;
header('Location: ../index.php');
exit;
};
try {
$database = new Database();
$loginManager = new LoginManager($database);
$user = $loginManager->getByLogin($login);
} catch (Throwable $exception) {
$_SESSION['login_error'] = 'Erreur interne : impossible de se connecter au service.';
header('Location: ../index.php');
exit;
}
if ($user === null) {
$handleFailedAttempt('Login ou mot de passe incorrect.');
}
if ((int) ($user['actif'] ?? 0) !== 1) {
$_SESSION['login_error'] = 'Votre compte n\'est pas encore activé. Veuillez contacter l\'administrateur.';
header('Location: ../index.php');
exit;
}
if (!password_verify($password, $user['pass'])) {
$handleFailedAttempt('Login ou mot de passe incorrect.');
}
session_regenerate_id(true);
$_SESSION['login_attempts'] = 0;
unset($_SESSION['blocked']);
$_SESSION['user_id'] = (int) $user['id'];
$_SESSION['user_login'] = $user['login'];
$_SESSION['user_name'] = trim($user['prenom'] . ' ' . $user['nom']);
header('Location: menu.php');
exit;
Executable
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/session.php';
// Logout action
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
session_unset();
session_destroy();
setcookie(session_name(), '', time() - 42000, '/');
header('Location: ../index.php');
exit;
}
// Require login
if (empty($_SESSION['user_id'])) {
header('Location: ../index.php');
exit;
}
require_once __DIR__ . '/header.php';
?>
<div class="login-container">
<div class="login-box">
<h2 class="login-title">Espace utilisateur</h2>
<p>Session ID : <?php echo htmlspecialchars(session_id(), ENT_QUOTES, 'UTF-8'); ?></p>
<p>User ID : <?php echo htmlspecialchars((string)($_SESSION['user_id'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></p>
<p>Login : <?php echo htmlspecialchars((string)($_SESSION['user_login'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></p>
<p>Nom : <?php echo htmlspecialchars((string)($_SESSION['user_name'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></p>
<p>Dernière activité (timestamp) : <?php echo htmlspecialchars((string)time(), ENT_QUOTES, 'UTF-8'); ?></p>
<p>Adresse IP : <?php echo htmlspecialchars($_SERVER['REMOTE_ADDR'] ?? '', ENT_QUOTES, 'UTF-8'); ?></p>
<div style="margin-top:16px;">
<a class="btn-submit" href="menu.php?action=logout" style="display:inline-block;text-decoration:none;">Se déconnecter</a>
</div>
</div>
</div>
<?php require_once __DIR__ . '/footer.php'; ?>
+137
View File
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
session_start();
require_once __DIR__ . '/db.php';
use App\Database\Database;
use App\Database\LoginManager;
$errors = [];
$values = [
'login' => '',
'nom' => '',
'prenom' => '',
'mail' => '',
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$values['login'] = trim((string) filter_input(INPUT_POST, 'login', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
$values['nom'] = trim((string) filter_input(INPUT_POST, 'nom', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
$values['prenom'] = trim((string) filter_input(INPUT_POST, 'prenom', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
$values['mail'] = trim((string) filter_input(INPUT_POST, 'mail', FILTER_SANITIZE_EMAIL));
$password = trim((string) ($_POST['password'] ?? ''));
$passwordConfirm = trim((string) ($_POST['password_confirm'] ?? ''));
if ($values['login'] === '') {
$errors[] = 'Le login est requis.';
}
if ($values['nom'] === '') {
$errors[] = 'Le nom est requis.';
}
if ($values['prenom'] === '') {
$errors[] = 'Le prénom est requis.';
}
if ($values['mail'] === '' || !filter_var($values['mail'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Une adresse e-mail valide est requise.';
}
if ($password === '' || $passwordConfirm === '') {
$errors[] = 'Le mot de passe et la confirmation sont requis.';
} elseif ($password !== $passwordConfirm) {
$errors[] = 'Les mots de passe ne correspondent pas.';
} elseif (strlen($password) < 8) {
$errors[] = 'Le mot de passe doit contenir au moins 8 caractères.';
}
if (empty($errors)) {
try {
$database = new Database();
$loginManager = new LoginManager($database);
if ($loginManager->existsByLoginOrEmail($values['login'], $values['mail'])) {
$errors[] = 'Le login ou ladresse e-mail existe déjà.';
} else {
$loginManager->create([
'login' => $values['login'],
'nom' => $values['nom'],
'prenom' => $values['prenom'],
'mail' => $values['mail'],
'pass' => $password,
]);
header('Location: ../index.php?registered=1');
exit;
}
} catch (Throwable $exception) {
$errors[] = 'Erreur interne : impossible de créer le compte.';
}
}
}
?>
<?php require_once __DIR__ . '/header.php'; ?>
<div class="login-container">
<div class="login-box">
<h2 class="login-title">Créer un compte</h2>
<p class="login-subtitle">Inscrivez-vous pour accéder à Giraud Finance.</p>
<?php if (!empty($errors)): ?>
<div class="alert alert-error">
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form action="register.php" method="POST" class="login-form">
<div class="form-group">
<label for="login" class="form-label">Login</label>
<input type="text" id="login" name="login" class="form-input" placeholder="Votre login" required autocomplete="username" value="<?php echo htmlspecialchars($values['login'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="form-group">
<label for="nom" class="form-label">Nom</label>
<input type="text" id="nom" name="nom" class="form-input" placeholder="Votre nom" required value="<?php echo htmlspecialchars($values['nom'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="form-group">
<label for="prenom" class="form-label">Prénom</label>
<input type="text" id="prenom" name="prenom" class="form-input" placeholder="Votre prénom" required value="<?php echo htmlspecialchars($values['prenom'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="form-group">
<label for="mail" class="form-label">Adresse e-mail</label>
<input type="email" id="mail" name="mail" class="form-input" placeholder="exemple@email.com" required autocomplete="email" value="<?php echo htmlspecialchars($values['mail'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="form-group">
<label for="password" class="form-label">Mot de passe</label>
<input type="password" id="password" name="password" class="form-input" placeholder="••••••••" required autocomplete="new-password">
</div>
<div class="form-group">
<label for="password_confirm" class="form-label">Confirmer le mot de passe</label>
<input type="password" id="password_confirm" name="password_confirm" class="form-input" placeholder="••••••••" required autocomplete="new-password">
</div>
<button type="submit" class="btn-submit">Sinscrire</button>
</form>
<div class="login-footer">
<span class="separator">•</span>
<a href="../index.php" class="signup-link">Retour à la connexion</a>
</div>
</div>
</div>
<?php require_once __DIR__ . '/footer.php'; ?>
+109
View File
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
session_start();
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/db.php';
use App\Database\Database;
use App\Database\LoginManager;
$errors = [];
$successMessage = '';
$token = trim((string) ($_GET['token'] ?? ''));
$user = null;
try {
$database = new Database();
$loginManager = new LoginManager($database);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = trim((string) ($_POST['token'] ?? ''));
$password = trim((string) ($_POST['password'] ?? ''));
$passwordConfirm = trim((string) ($_POST['password_confirm'] ?? ''));
if ($token === '') {
$errors[] = 'Lien invalide.';
} else {
$user = $loginManager->getByResetToken($token);
if ($user === null) {
$errors[] = 'Lien invalide.';
} else {
if ($password === '' || $passwordConfirm === '') {
$errors[] = 'Veuillez renseigner les deux champs de mot de passe.';
} elseif ($password !== $passwordConfirm) {
$errors[] = 'Les mots de passe ne correspondent pas.';
} elseif (strlen($password) < 8) {
$errors[] = 'Le mot de passe doit contenir au moins 8 caractères.';
}
if (empty($errors)) {
if ($loginManager->updatePasswordById((int) $user['id'], $password)) {
$successMessage = 'Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter.';
$user = null;
} else {
$errors[] = 'Impossible de mettre à jour le mot de passe. Veuillez réessayer.';
}
}
}
}
} elseif ($token !== '') {
$user = $loginManager->getByResetToken($token);
}
} catch (Throwable $exception) {
$errors[] = 'Erreur interne : impossible de traiter votre demande.';
}
?>
<?php require_once __DIR__ . '/header.php'; ?>
<div class="login-container">
<div class="login-box">
<h2 class="login-title">Réinitialisation du mot de passe</h2>
<p class="login-subtitle">Entrez un nouveau mot de passe pour votre compte.</p>
<?php if (!empty($errors)): ?>
<div class="alert alert-error">
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($successMessage): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($successMessage, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
<?php if ($user !== null): ?>
<form action="reset_password.php" method="POST" class="login-form">
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
<div class="form-group">
<label for="password" class="form-label">Nouveau mot de passe</label>
<input type="password" id="password" name="password" class="form-input" placeholder="••••••••" required autocomplete="new-password">
</div>
<div class="form-group">
<label for="password_confirm" class="form-label">Confirmer le mot de passe</label>
<input type="password" id="password_confirm" name="password_confirm" class="form-input" placeholder="••••••••" required autocomplete="new-password">
</div>
<button type="submit" class="btn-submit">Réinitialiser le mot de passe</button>
</form>
<?php elseif ($successMessage === ''): ?>
<div class="alert alert-error">Lien invalide.</div>
<?php endif; ?>
<div class="login-footer">
<span class="separator">•</span>
<a href="../index.php" class="signup-link">Retour à la connexion</a>
</div>
</div>
</div>
<?php require_once __DIR__ . '/footer.php'; ?>
+185
View File
@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db.php';
use App\Database\Database;
class MySqlSessionHandler implements SessionHandlerInterface
{
private ?PDO $connection = null;
private bool $ipMismatch = false;
public function open(string $savePath, string $sessionName): bool
{
try {
$database = new Database();
$this->connection = $database->getConnection();
return true;
} catch (Throwable $exception) {
return false;
}
}
public function close(): bool
{
$this->connection = null;
return true;
}
public function read(string $id): string
{
if ($this->connection === null) {
if (!$this->open('', '')) {
return '';
}
}
$statement = $this->connection->prepare(
'SELECT data, ip_address
FROM sessions
WHERE id = :id'
);
$statement->bindValue(':id', $id, PDO::PARAM_STR);
$statement->execute();
$row = $statement->fetch(PDO::FETCH_ASSOC);
if ($row === false) {
return '';
}
$currentIp = $_SERVER['REMOTE_ADDR'] ?? '';
if (!empty($row['ip_address']) && $row['ip_address'] !== $currentIp) {
$this->ipMismatch = true;
return '';
}
$this->updateAccessAndIp($id, $currentIp);
return (string) $row['data'];
}
public function write(string $id, string $data): bool
{
if ($this->connection === null) {
if (!$this->open('', '')) {
return false;
}
}
$currentIp = $_SERVER['REMOTE_ADDR'] ?? '';
$access = time();
$userId = isset($_SESSION['user_id']) ? (int) $_SESSION['user_id'] : null;
$createdAt = date('Y-m-d H:i:s');
$statement = $this->connection->prepare(
'INSERT INTO sessions (id, access, data, user_id, ip_address, created_at)
VALUES (:id, :access, :data, :user_id, :ip_address, :created_at)
ON DUPLICATE KEY UPDATE
access = VALUES(access),
data = VALUES(data),
user_id = VALUES(user_id),
ip_address = VALUES(ip_address)'
);
$statement->bindValue(':id', $id, PDO::PARAM_STR);
$statement->bindValue(':access', $access, PDO::PARAM_INT);
$statement->bindValue(':data', $data, PDO::PARAM_STR);
if ($userId === null) {
$statement->bindValue(':user_id', null, PDO::PARAM_NULL);
} else {
$statement->bindValue(':user_id', $userId, PDO::PARAM_INT);
}
$statement->bindValue(':ip_address', $currentIp, PDO::PARAM_STR);
$statement->bindValue(':created_at', $createdAt, PDO::PARAM_STR);
try {
return $statement->execute();
} catch (Throwable $e) {
return false;
}
}
public function destroy(string $id): bool
{
if ($this->connection === null) {
return false;
}
$statement = $this->connection->prepare(
'DELETE FROM sessions WHERE id = :id'
);
$statement->bindValue(':id', $id, PDO::PARAM_STR);
return $statement->execute();
}
public function gc(int $maxLifetime): int|false
{
if ($this->connection === null) {
return false;
}
$statement = $this->connection->prepare(
'DELETE FROM sessions WHERE access < :time'
);
$statement->bindValue(':time', time() - $maxLifetime, PDO::PARAM_INT);
return $statement->execute();
}
public function validateIp(string $id): bool
{
if ($this->connection === null) {
return true;
}
$statement = $this->connection->prepare(
'SELECT ip_address FROM sessions WHERE id = :id'
);
$statement->bindValue(':id', $id, PDO::PARAM_STR);
$statement->execute();
$row = $statement->fetch(PDO::FETCH_ASSOC);
$currentIp = $_SERVER['REMOTE_ADDR'] ?? '';
return empty($row['ip_address']) || $row['ip_address'] === $currentIp;
}
public function hasIpMismatch(): bool
{
return $this->ipMismatch;
}
private function updateAccessAndIp(string $id, string $ip): void
{
$statement = $this->connection->prepare(
'UPDATE sessions
SET access = :access,
ip_address = :ip_address
WHERE id = :id'
);
$statement->bindValue(':access', time(), PDO::PARAM_INT);
$statement->bindValue(':ip_address', $ip, PDO::PARAM_STR);
$statement->bindValue(':id', $id, PDO::PARAM_STR);
$statement->execute();
}
}
$handler = new MySqlSessionHandler();
session_set_save_handler($handler, true);
session_start();
// Only enforce IP mismatch for authenticated sessions (user_id present)
if (!empty($_SESSION['user_id']) && $handler->hasIpMismatch()) {
session_unset();
session_destroy();
setcookie(session_name(), '', time() - 42000, '/');
header('Location: /index.php');
exit;
}