mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-08-28 23:33:13 +00:00
Add files via upload
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ASPIC — API d'authentification (aspic.php, anciennement login.php —
|
||||||
|
* renommé le 21 août 2026 à la demande de Jean-Marc, purement cosmétique,
|
||||||
|
* le contenu et le comportement ne changent pas)
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Vérifie un couple identifiant/mot de passe contre ASP_Users et répond en
|
||||||
|
* JSON. Ne fait aucune gestion de compteur d'essais ni de verrouillage de
|
||||||
|
* compte : ce comportement (2 essais puis sortie, voir journal ASPIC section
|
||||||
|
* 8) est géré côté front (identification.php), application strictement
|
||||||
|
* interne, pas de mécanisme de bannissement d'IP prévu.
|
||||||
|
*
|
||||||
|
* Requête attendue : POST, corps JSON { "identifiant": "...", "motdepasse": "..." }
|
||||||
|
* (identifiant = USR_Code, 3 caractères — pas USR_Nom, voir journal section 2).
|
||||||
|
*
|
||||||
|
* Réponses :
|
||||||
|
* 200 { "success": true, "user": { "USR_Code": "...", "USR_Nom": "..." } }
|
||||||
|
* 200 { "success": false } (identifiants incorrects)
|
||||||
|
* 400 { "success": false, "error": "..." } (requête mal formée)
|
||||||
|
* 405 { "success": false, "error": "Méthode non autorisée" } (autre que POST)
|
||||||
|
* 500 { "success": false, "error": "..." } (connexion base impossible)
|
||||||
|
*
|
||||||
|
* ⚠️ Session utilisateur intégrée le 24 août 2026 (voir journal, sections 14
|
||||||
|
* et 15) : en cas d'identification réussie, une session PHP native est
|
||||||
|
* ouverte (`session_start()` + `$_SESSION['USR_Code']`), lue ensuite par
|
||||||
|
* contacts.php/professions.php/statuts.php/realisations.php pour autoriser
|
||||||
|
* leurs appels. `session_start()` doit rester la toute première instruction
|
||||||
|
* du fichier, avant tout `header()`/`echo` — sinon PHP refuse d'envoyer les
|
||||||
|
* cookies de session.
|
||||||
|
*/
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$identifiant = isset($data['identifiant']) ? trim((string) $data['identifiant']) : '';
|
||||||
|
$motdepasse = isset($data['motdepasse']) ? (string) $data['motdepasse'] : '';
|
||||||
|
|
||||||
|
if ($identifiant === '' || $motdepasse === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Identifiant et mot de passe sont obligatoires.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Connexion base de données impossible']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// USR_Code est un code court (3 caractères) conventionnellement saisi en
|
||||||
|
// majuscules ; comparaison insensible à la casse sur l'identifiant, comme
|
||||||
|
// dans le prototype front (voir ASPIC_ecran_identification.html,
|
||||||
|
// trouverUtilisateurDemo). Le mot de passe, lui, reste comparé strictement
|
||||||
|
// (sensible à la casse).
|
||||||
|
$stmt = $pdo->prepare('SELECT USR_Code, USR_Nom, USR_MDP FROM ASP_Users WHERE UPPER(USR_Code) = UPPER(?)');
|
||||||
|
$stmt->execute([$identifiant]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
// hash_equals() plutôt que === : comparaison en temps constant, pour ne pas
|
||||||
|
// laisser une attaque par mesure de timing déduire des informations sur le
|
||||||
|
// mot de passe stocké. Mot de passe en clair (USR_MDP) assumé consciemment
|
||||||
|
// pour cette application interne (voir journal section 2) — cette précaution
|
||||||
|
// est un geste peu coûteux, pas une garantie de sécurité forte sur ce point.
|
||||||
|
if (!$user || !hash_equals($user['USR_MDP'], $motdepasse)) {
|
||||||
|
// Message volontairement générique : ne pas révéler si c'est l'identifiant
|
||||||
|
// ou le mot de passe qui est en cause. Le compteur d'essais / la sortie au
|
||||||
|
// 3e échec sont gérés côté front, pas ici (voir en-tête de ce fichier).
|
||||||
|
echo json_encode(['success' => false]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session ouverte : les endpoints suivants (contacts.php, professions.php,
|
||||||
|
// statuts.php, realisations.php) exigent désormais $_SESSION['USR_Code']
|
||||||
|
// (voir journal, section 15).
|
||||||
|
$_SESSION['USR_Code'] = $user['USR_Code'];
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'user' => [
|
||||||
|
'USR_Code' => $user['USR_Code'],
|
||||||
|
'USR_Nom' => $user['USR_Nom'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ASPIC — contacts.php
|
||||||
|
* API CRUD sur ASP_Contacts (GET liste + fiche / POST création / PUT
|
||||||
|
* modification / DELETE suppression logique).
|
||||||
|
*
|
||||||
|
* ⚠️ Reconstruction du 24 août 2026, à comparer avec le fichier actuellement
|
||||||
|
* en production avant remplacement. Cette session n'avait pas accès au
|
||||||
|
* contenu exact de la version en ligne (alignée le 21 août 2026, avec les
|
||||||
|
* correctifs du 23 août sur le joker « * » et sur PUT/DELETE — voir journal
|
||||||
|
* de développement) ; ce fichier reprend fidèlement tout ce qui est
|
||||||
|
* documenté dans le journal, mais mérite une comparaison avant déploiement
|
||||||
|
* si la version en ligne contient des ajustements non journalisés.
|
||||||
|
*
|
||||||
|
* Reprend :
|
||||||
|
* - GET liste : tri restreint à une liste blanche de colonnes, jusqu'à 7
|
||||||
|
* filtres optionnels avec joker "*", WHERE CNT_Actif = 1 systématique.
|
||||||
|
* - GET fiche (?id=) : un contact actif.
|
||||||
|
* - POST : création. Génère CNT_Code (25 août 2026, voir journal section 16) :
|
||||||
|
* 1. Recherche ANN_Code dans ASP_Annees pour l'année courante — AVANT
|
||||||
|
* l'INSERT, pour ne jamais créer un contact sans code si l'année n'est
|
||||||
|
* pas encore configurée (échec net, 500, plutôt qu'un contact orphelin
|
||||||
|
* de code à corriger après coup).
|
||||||
|
* 2. INSERT du contact, lastInsertId() = CNT_Incr.
|
||||||
|
* 3. Nombre aléatoire 0-999, zéro-padé sur 3 caractères.
|
||||||
|
* 4. UPDATE CNT_Code = ANN_Code . "-" . CNT_Incr (sans zéro de tête) .
|
||||||
|
* nombre aléatoire (3 caractères).
|
||||||
|
* La journalisation ASP_Realisations (CNT_CRE) reste à la charge du
|
||||||
|
* front-end, sur le même principe que CNT_MAJ pour la modification (POST
|
||||||
|
* séparé vers realisations.php, best-effort, ne bloque pas la création).
|
||||||
|
* - PUT (?id=) : modification, sur l'ensemble des champs éditables du
|
||||||
|
* panneau de détail (étendu le 24 août 2026 — l'ancien prototype ne
|
||||||
|
* couvrait que Nom/Prénom/Mail).
|
||||||
|
* - DELETE (?id=) : suppression logique (CNT_Actif = 0), jamais de DELETE
|
||||||
|
* SQL réel.
|
||||||
|
* - PUT vérifie l'EXISTENCE du contact par un SELECT séparé de l'UPDATE lui
|
||||||
|
* même (corrigé le 24 août 2026 — voir note dans le bloc PUT ci-dessous).
|
||||||
|
* DELETE continue de s'appuyer sur rowCount(), valide dans son cas.
|
||||||
|
* - Toute exception PDO (contenu qui ne passe pas une contrainte de colonne,
|
||||||
|
* erreur de connexion perdue en cours de route, etc.) est interceptée et
|
||||||
|
* renvoyée en JSON avec un code 500, plutôt que de laisser remonter une
|
||||||
|
* erreur PHP brute (non-JSON) que le front-end ne peut pas interpréter
|
||||||
|
* proprement — correctif du 24 août 2026, voir journal.
|
||||||
|
*
|
||||||
|
* Note sur le joker "*" : contrairement à la première version (bug corrigé
|
||||||
|
* le 23 août 2026), l'échappement du LIKE n'est JAMAIS construit par
|
||||||
|
* concaténation dans le texte SQL. Le caractère d'échappement est passé en
|
||||||
|
* paramètre lié (ESCAPE ?), comme le motif lui-même — ce qui évite par
|
||||||
|
* construction le piège du antislash interprété différemment dans un
|
||||||
|
* littéral SQL selon le moteur (MySQL/MariaDB vs SQLite, voir journal).
|
||||||
|
*
|
||||||
|
* ⚠️ Session utilisateur intégrée le 24 août 2026 (voir journal, sections 14
|
||||||
|
* et 15) : tout appel exige désormais une session PHP valide, ouverte par
|
||||||
|
* `aspic.php` à l'identification (`$_SESSION['USR_Code']`). Sans elle,
|
||||||
|
* l'endpoint répond 401 — y compris en lecture (GET). C'est `aspic.php` qui
|
||||||
|
* doit appeler `session_start()` et renseigner `$_SESSION['USR_Code']` au
|
||||||
|
* moment où l'identifiant/mot de passe sont validés ; sans cette mise à jour
|
||||||
|
* côté `aspic.php`, plus aucun appel à ce fichier ne pourra aboutir.
|
||||||
|
*/
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
// Accès réservé aux utilisateurs identifiés (session ouverte par aspic.php).
|
||||||
|
if (empty($_SESSION['USR_Code'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Non identifié — veuillez vous connecter.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Connexion base de données impossible']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colonnes éditables depuis le panneau de détail du contact (hors CNT_Code,
|
||||||
|
// toujours généré/lecture seule, et hors CNT_Incr/CNT_Actif).
|
||||||
|
const CHAMPS_EDITABLES = [
|
||||||
|
'CNT_Nom', 'CNT_Prenom', 'CNT_Statut', 'CNT_Société', 'CNT_Service',
|
||||||
|
'CNT_Fonction', 'CNT_AdrLig1', 'CNT_AdrLig2', 'CNT_AdrLig3', 'CNT_AdrCP',
|
||||||
|
'CNT_AdrVille', 'CNT_SIRET', 'CNT_Tel', 'CNT_Portable', 'CNT_Mail',
|
||||||
|
'CNT_Notes', 'CNT_CodeProfession',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Liste blanche des colonnes de tri autorisées (celles réellement triables
|
||||||
|
// depuis l'écran liste des contacts).
|
||||||
|
const COLONNES_TRI_AUTORISEES = ['CNT_Nom', 'CNT_AdrVille'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit un motif LIKE à partir d'une valeur de filtre utilisateur,
|
||||||
|
* en échappant d'abord les caractères spéciaux LIKE déjà présents
|
||||||
|
* (antislash, %, _) avant de traduire le joker "*" de l'utilisateur en "%".
|
||||||
|
* Le caractère d'échappement utilisé (un seul antislash) est renvoyé à part,
|
||||||
|
* pour être passé en paramètre lié à la clause ESCAPE — jamais concaténé
|
||||||
|
* dans le texte SQL (voir note d'en-tête).
|
||||||
|
*/
|
||||||
|
function construireMotifLike(string $valeur): array
|
||||||
|
{
|
||||||
|
$echappe = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $valeur);
|
||||||
|
$motif = str_replace('*', '%', $echappe);
|
||||||
|
return [$motif, '\\'];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($method) {
|
||||||
|
case 'GET':
|
||||||
|
if (isset($_GET['id'])) {
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM ASP_Contacts WHERE CNT_Incr = ? AND CNT_Actif = 1');
|
||||||
|
$stmt->execute([$_GET['id']]);
|
||||||
|
$contact = $stmt->fetch();
|
||||||
|
if (!$contact) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Contact introuvable']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
echo json_encode($contact);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tri = $_GET['tri'] ?? 'CNT_Nom';
|
||||||
|
if (!in_array($tri, COLONNES_TRI_AUTORISEES, true)) {
|
||||||
|
$tri = 'CNT_Nom';
|
||||||
|
}
|
||||||
|
$sens = (isset($_GET['sens']) && strtolower($_GET['sens']) === 'desc') ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
|
$conditions = ['CNT_Actif = 1'];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
$filtresColonnes = [
|
||||||
|
'f_nom' => 'CNT_Nom',
|
||||||
|
'f_prenom' => 'CNT_Prenom',
|
||||||
|
'f_ville' => 'CNT_AdrVille',
|
||||||
|
'f_tel' => 'CNT_Tel',
|
||||||
|
'f_portable' => 'CNT_Portable',
|
||||||
|
'f_mail' => 'CNT_Mail',
|
||||||
|
];
|
||||||
|
foreach ($filtresColonnes as $param => $colonne) {
|
||||||
|
if (!empty($_GET[$param])) {
|
||||||
|
[$motif, $echap] = construireMotifLike($_GET[$param]);
|
||||||
|
$conditions[] = "$colonne LIKE ? ESCAPE ?";
|
||||||
|
$params[] = $motif;
|
||||||
|
$params[] = $echap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Département = 2 premiers caractères de CNT_AdrCP.
|
||||||
|
if (!empty($_GET['f_dept'])) {
|
||||||
|
[$motif, $echap] = construireMotifLike($_GET['f_dept']);
|
||||||
|
$conditions[] = 'LEFT(CNT_AdrCP, 2) LIKE ? ESCAPE ?';
|
||||||
|
$params[] = $motif;
|
||||||
|
$params[] = $echap;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = 'SELECT *, LEFT(CNT_AdrCP, 2) AS CNT_Dept FROM ASP_Contacts WHERE '
|
||||||
|
. implode(' AND ', $conditions)
|
||||||
|
. " ORDER BY $tri $sens, CNT_Prenom ASC";
|
||||||
|
|
||||||
|
$stmtTotal = $pdo->prepare('SELECT COUNT(*) FROM ASP_Contacts WHERE ' . implode(' AND ', $conditions));
|
||||||
|
$stmtTotal->execute($params);
|
||||||
|
$total = (int)$stmtTotal->fetchColumn();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
echo json_encode(['contacts' => $stmt->fetchAll(), 'total' => $total]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
$nom = trim($data['CNT_Nom'] ?? '');
|
||||||
|
if ($nom === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Le nom est obligatoire']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étape 1 (voir en-tête du fichier) : ANN_Code de l'année courante,
|
||||||
|
// recherché AVANT l'INSERT pour ne jamais créer un contact sans
|
||||||
|
// code faute d'année configurée dans ASP_Annees.
|
||||||
|
$anneeCourante = (int)date('Y');
|
||||||
|
$stmtAnnee = $pdo->prepare('SELECT ANN_Code FROM ASP_Annees WHERE ANN_Année = ?');
|
||||||
|
$stmtAnnee->execute([$anneeCourante]);
|
||||||
|
$annCode = $stmtAnnee->fetchColumn();
|
||||||
|
if ($annCode === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => "Aucun code configuré pour l'année $anneeCourante dans ASP_Annees — impossible de générer le Code du contact."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$colonnes = [];
|
||||||
|
$placeholders = [];
|
||||||
|
$params = [];
|
||||||
|
foreach (CHAMPS_EDITABLES as $champ) {
|
||||||
|
$colonnes[] = $champ;
|
||||||
|
$placeholders[] = '?';
|
||||||
|
$valeur = $data[$champ] ?? null;
|
||||||
|
$params[] = ($valeur === '' ? null : $valeur);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étape 2 : création du contact (CNT_Code encore NULL à ce stade,
|
||||||
|
// il dépend de CNT_Incr qui n'existe qu'après l'INSERT).
|
||||||
|
$sql = 'INSERT INTO ASP_Contacts (' . implode(', ', $colonnes) . ') VALUES (' . implode(', ', $placeholders) . ')';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$nouvelIncr = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Étapes 3-4 : nombre aléatoire 0-999 zéro-padé sur 3 caractères,
|
||||||
|
// concaténation ANN_Code-CNT_Incr+aléatoire (voir journal section 2
|
||||||
|
// pour la logique détaillée, et section 16 pour cette implémentation).
|
||||||
|
$aleatoire = str_pad((string)random_int(0, 999), 3, '0', STR_PAD_LEFT);
|
||||||
|
$code = $annCode . '-' . $nouvelIncr . $aleatoire;
|
||||||
|
|
||||||
|
$stmtCode = $pdo->prepare('UPDATE ASP_Contacts SET CNT_Code = ? WHERE CNT_Incr = ?');
|
||||||
|
$stmtCode->execute([$code, $nouvelIncr]);
|
||||||
|
|
||||||
|
echo json_encode(['id' => $nouvelIncr, 'code' => $code]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PUT':
|
||||||
|
if (!isset($_GET['id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'id manquant']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
$nom = trim($data['CNT_Nom'] ?? '');
|
||||||
|
if ($nom === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Le nom est obligatoire']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification d'EXISTENCE séparée de l'UPDATE lui-même : par
|
||||||
|
// défaut, rowCount() après un UPDATE reflète le nombre de lignes
|
||||||
|
// RÉELLEMENT MODIFIÉES par MySQL/MariaDB, pas le nombre de lignes
|
||||||
|
// trouvées par le WHERE. Une re-soumission du formulaire sans
|
||||||
|
// changement réel (ou un enregistrement où, par coïncidence,
|
||||||
|
// aucune des valeurs soumises ne diffère de ce qui est déjà en
|
||||||
|
// base) donnerait alors rowCount() = 0, et l'ancien code
|
||||||
|
// l'interprétait à tort comme "contact introuvable" (404) — bug
|
||||||
|
// corrigé le 24 août 2026, repéré par Jean-Marc ("erreur si je
|
||||||
|
// remplis Notes ou Informations", en réalité lié à des essais
|
||||||
|
// répétés sans changement effectif des champs du contact).
|
||||||
|
$verif = $pdo->prepare('SELECT CNT_Incr FROM ASP_Contacts WHERE CNT_Incr = ? AND CNT_Actif = 1');
|
||||||
|
$verif->execute([$_GET['id']]);
|
||||||
|
if (!$verif->fetch()) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Contact introuvable ou déjà désactivé']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignations = [];
|
||||||
|
$params = [];
|
||||||
|
foreach (CHAMPS_EDITABLES as $champ) {
|
||||||
|
$assignations[] = "$champ = ?";
|
||||||
|
$valeur = $data[$champ] ?? null;
|
||||||
|
$params[] = ($valeur === '' ? null : $valeur);
|
||||||
|
}
|
||||||
|
$params[] = $_GET['id'];
|
||||||
|
|
||||||
|
$sql = 'UPDATE ASP_Contacts SET ' . implode(', ', $assignations) . ' WHERE CNT_Incr = ? AND CNT_Actif = 1';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'DELETE':
|
||||||
|
if (!isset($_GET['id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'id manquant']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// Suppression logique uniquement (voir journal, section 2) —
|
||||||
|
// jamais de DELETE SQL réel. Ici rowCount() reste un test valide
|
||||||
|
// (contrairement au PUT ci-dessus) : CNT_Actif passe toujours
|
||||||
|
// réellement de 1 à 0, donc rowCount() = 0 signifie bien "déjà
|
||||||
|
// désactivé ou inexistant".
|
||||||
|
$stmt = $pdo->prepare('UPDATE ASP_Contacts SET CNT_Actif = 0 WHERE CNT_Incr = ? AND CNT_Actif = 1');
|
||||||
|
$stmt->execute([$_GET['id']]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Contact introuvable ou déjà désactivé']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Méthode non autorisée']);
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
function getDbConnection(): PDO
|
||||||
|
{
|
||||||
|
$host = getenv('DB_HOST') ?: 'mysql';
|
||||||
|
$db = getenv('DB_NAME') ?: 'crm';
|
||||||
|
$user = getenv('DB_USER') ?: 'crm_user';
|
||||||
|
$pass = getenv('DB_PASS') ?: '';
|
||||||
|
|
||||||
|
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
];
|
||||||
|
|
||||||
|
return new PDO($dsn, $user, $pass, $options);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ASPIC — point d'entrée du domaine (index.php)
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Ne contient aucune logique : redirige simplement vers l'écran
|
||||||
|
* d'identification, qui garde son nom explicite (identification.html) plutôt
|
||||||
|
* que d'être lui-même renommé en index.html (voir journal ASPIC, section 3,
|
||||||
|
* convention de nommage des fichiers). Permet de charger
|
||||||
|
* https://aspic.mvl-tls.fr/ directement sur l'écran de connexion.
|
||||||
|
*
|
||||||
|
* À faire évoluer le jour où l'application aura une vraie notion de session :
|
||||||
|
* rediriger vers liste_contacts.html si une session valide existe déjà,
|
||||||
|
* vers identification.html sinon.
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Location: identification.html');
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ASPIC — preferences.php
|
||||||
|
* Préférences d'affichage liées à l'utilisateur connecté (ASP_Users), et non
|
||||||
|
* à un contact ou à une donnée métier — pour l'instant, uniquement le ratio
|
||||||
|
* de répartition du panneau de détail (grille des champs / sous-liste des
|
||||||
|
* réalisations), voir journal de développement, section 17.
|
||||||
|
*
|
||||||
|
* Toujours restreint à l'utilisateur de la session en cours
|
||||||
|
* ($_SESSION['USR_Code'], ouverte par aspic.php) : aucun USR_Code n'est
|
||||||
|
* jamais accepté depuis le corps de la requête, sur le même principe que
|
||||||
|
* REA_CodeUser dans realisations.php (voir journal, section 15) — impossible
|
||||||
|
* pour un utilisateur de lire ou modifier les préférences d'un autre.
|
||||||
|
*
|
||||||
|
* GET : renvoie les deux ratios (0-100) de l'utilisateur connecté.
|
||||||
|
* PUT : met à jour un ou plusieurs ratios (0-100 uniquement, whitelist des
|
||||||
|
* colonnes acceptées — voir CHAMPS_RATIO).
|
||||||
|
*/
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
if (empty($_SESSION['USR_Code'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Non identifié — veuillez vous connecter.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Connexion base de données impossible']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitelist des colonnes de préférence modifiables — n'accepter que celles-ci
|
||||||
|
// dans le corps d'un PUT, jamais un nom de colonne arbitraire fourni par le
|
||||||
|
// client (même logique que CHAMPS_EDITABLES dans contacts.php).
|
||||||
|
const CHAMPS_RATIO = ['USR_RatioPanneauContact', 'USR_RatioPanneauProspect'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($method) {
|
||||||
|
case 'GET':
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT USR_RatioPanneauContact, USR_RatioPanneauProspect FROM ASP_Users WHERE USR_Code = ?'
|
||||||
|
);
|
||||||
|
$stmt->execute([$_SESSION['USR_Code']]);
|
||||||
|
$ligne = $stmt->fetch();
|
||||||
|
if (!$ligne) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Utilisateur introuvable']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
echo json_encode([
|
||||||
|
'USR_RatioPanneauContact' => (int)$ligne['USR_RatioPanneauContact'],
|
||||||
|
'USR_RatioPanneauProspect' => (int)$ligne['USR_RatioPanneauProspect'],
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PUT':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
$assignations = [];
|
||||||
|
$params = [];
|
||||||
|
foreach (CHAMPS_RATIO as $champ) {
|
||||||
|
if (!array_key_exists($champ, $data)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$valeur = $data[$champ];
|
||||||
|
if (!is_numeric($valeur) || (int)$valeur < 0 || (int)$valeur > 100) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => "$champ doit être un entier compris entre 0 et 100"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$assignations[] = "$champ = ?";
|
||||||
|
$params[] = (int)$valeur;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$assignations) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Aucun champ de préférence reconnu à mettre à jour']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[] = $_SESSION['USR_Code'];
|
||||||
|
$sql = 'UPDATE ASP_Users SET ' . implode(', ', $assignations) . ' WHERE USR_Code = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Méthode non autorisée']);
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ASPIC — professions.php
|
||||||
|
* Table de référence ASP_Professions (liée à ASP_Contacts via
|
||||||
|
* CNT_CodeProfession = PRF_Incr, lien applicatif, sans contrainte FOREIGN KEY
|
||||||
|
* SQL — voir journal de développement, section 2).
|
||||||
|
*
|
||||||
|
* GET : liste des professions, triée par libellé.
|
||||||
|
* POST : création d'une nouvelle profession (utilisée par le formulaire de
|
||||||
|
* détail du contact, option "+ Nouvelle profession…" — exception à la
|
||||||
|
* règle "table gérée à la main" décidée le 23 août 2026, car
|
||||||
|
* ASP_Professions est une liste ouverte destinée à grandir avec
|
||||||
|
* l'usage, contrairement à ASP_Statuts/ASP_Actions).
|
||||||
|
*
|
||||||
|
* ⚠️ Session utilisateur intégrée le 24 août 2026 (voir journal, section 14) :
|
||||||
|
* accès réservé aux utilisateurs identifiés (`$_SESSION['USR_Code']`, ouverte
|
||||||
|
* par `aspic.php`).
|
||||||
|
*/
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
if (empty($_SESSION['USR_Code'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Non identifié — veuillez vous connecter.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Connexion base de données impossible']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($method) {
|
||||||
|
case 'GET':
|
||||||
|
$stmt = $pdo->query('SELECT PRF_Incr, PRF_Libelle FROM ASP_Professions ORDER BY PRF_Libelle ASC');
|
||||||
|
echo json_encode(['professions' => $stmt->fetchAll()]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$libelle = trim($data['PRF_Libelle'] ?? '');
|
||||||
|
if ($libelle === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Le libellé de la profession est obligatoire']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas de contrainte UNIQUE en base sur PRF_Libelle à ce stade (voir
|
||||||
|
// journal) : on évite ici un doublon évident et strictement identique,
|
||||||
|
// sans garantie absolue en cas de création concurrente.
|
||||||
|
$verif = $pdo->prepare('SELECT PRF_Incr FROM ASP_Professions WHERE PRF_Libelle = ?');
|
||||||
|
$verif->execute([$libelle]);
|
||||||
|
$existant = $verif->fetch();
|
||||||
|
if ($existant) {
|
||||||
|
echo json_encode(['id' => (int)$existant['PRF_Incr'], 'existant' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO ASP_Professions (PRF_Libelle) VALUES (?)');
|
||||||
|
$stmt->execute([$libelle]);
|
||||||
|
echo json_encode(['id' => (int)$pdo->lastInsertId(), 'existant' => false]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Méthode non autorisée']);
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
// prospect_doc.php
|
||||||
|
// Point d'entrée public (clic sur le lien de documentation dans un mail) :
|
||||||
|
// vérifie que les paramètres n'ont pas été modifiés (signature), journalise
|
||||||
|
// la lecture du prospect, puis redirige vers l'URL réelle du document.
|
||||||
|
// URL attendue : prospect_doc.php?a=<PSP_Incr>&b=<DOC_Incr>&c=<CMP_Incr>&d=<signature>
|
||||||
|
|
||||||
|
require __DIR__ . '/prospects_core.php';
|
||||||
|
|
||||||
|
// Destination neutre si les paramètres ont été trafiqués : ta page d'accueil
|
||||||
|
// plutôt qu'une réponse d'erreur qui confirmerait la tentative au visiteur.
|
||||||
|
const URL_REDIRECTION_SI_INVALIDE = 'https://mevelia-solutions.bzh';
|
||||||
|
|
||||||
|
$idProspect = filter_input(INPUT_GET, 'a', FILTER_VALIDATE_INT);
|
||||||
|
$idDoc = filter_input(INPUT_GET, 'b', FILTER_VALIDATE_INT);
|
||||||
|
$idCampagne = filter_input(INPUT_GET, 'c', FILTER_VALIDATE_INT);
|
||||||
|
$signature = $_GET['d'] ?? '';
|
||||||
|
|
||||||
|
if (!$idProspect || !$idDoc || !$idCampagne || $signature === '') {
|
||||||
|
header('Location: ' . URL_REDIRECTION_SI_INVALIDE, true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$signatureAttendue = signerParametresDoc($idProspect, $idDoc, $idCampagne);
|
||||||
|
if (!hash_equals($signatureAttendue, $signature)) {
|
||||||
|
header('Location: ' . URL_REDIRECTION_SI_INVALIDE, true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT PSP_CodeCampagne FROM ASP_Prospects WHERE PSP_Incr = ?');
|
||||||
|
$stmt->execute([$idProspect]);
|
||||||
|
$prospect = $stmt->fetch();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT DOC_URL, DOC_Libelle FROM ASP_Docs WHERE DOC_Incr = ?');
|
||||||
|
$stmt->execute([$idDoc]);
|
||||||
|
$doc = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$prospect || !$doc) {
|
||||||
|
header('Location: ' . URL_REDIRECTION_SI_INVALIDE, true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
journaliserOperation($idProspect, $prospect['PSP_CodeCampagne'] . '-LEC', $doc['DOC_Libelle']);
|
||||||
|
|
||||||
|
header('Location: ' . $doc['DOC_URL'], true, 302);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
// prospect_web-form-001_ajoute.php
|
||||||
|
// Point d'entrée public pour la campagne WEB-FORM-001 (formulaire du site web).
|
||||||
|
// Ne contient aucune logique propre : délègue entièrement à prospects_core.php.
|
||||||
|
|
||||||
|
require __DIR__ . '/prospects_core.php';
|
||||||
|
|
||||||
|
traiterAjoutProspect('WEB-FORM-001');
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
<?php
|
||||||
|
// prospects_core.php
|
||||||
|
// Logique commune à tous les scripts publics de prospection ASPIC.
|
||||||
|
// Réutilise db.php (déjà en production) pour la connexion PDO.
|
||||||
|
|
||||||
|
require __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
// --- Configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
const BREVO_FROM_EMAIL = 'viktor@ms.bzh';
|
||||||
|
const BREVO_FROM_NAME = 'Viktor - Mevelia Solutions';
|
||||||
|
const BREVO_REPLY_TO = 'contact@mevelia-solutions.bzh';
|
||||||
|
const ASPIC_BASE_URL = 'https://aspic.mvl-tls.fr';
|
||||||
|
|
||||||
|
// URL publique du logo, injectée automatiquement dans toutes les variables
|
||||||
|
// disponibles pour les modèles de mail (utilisable via {(LogoUrl)}).
|
||||||
|
const LOGO_URL = 'https://aspic.mvl-tls.fr/logo.png';
|
||||||
|
|
||||||
|
const ORIGINES_CORS_AUTORISEES = [
|
||||||
|
'https://mevelia-solutions.bzh',
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Utilitaires génériques ------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie les en-têtes CORS nécessaires si l'origine de la requête est autorisée,
|
||||||
|
* et répond immédiatement aux requêtes de pré-vérification (OPTIONS).
|
||||||
|
*/
|
||||||
|
function autoriserCorsSiNecessaire(): void
|
||||||
|
{
|
||||||
|
$origine = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
|
if (in_array($origine, ORIGINES_CORS_AUTORISEES, true)) {
|
||||||
|
header('Access-Control-Allow-Origin: ' . $origine);
|
||||||
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
}
|
||||||
|
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
|
||||||
|
http_response_code(204);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un token de vérification imprévisible (64 caractères hexadécimaux).
|
||||||
|
*/
|
||||||
|
function genererTokenVerification(): string
|
||||||
|
{
|
||||||
|
return bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un mail depuis un modèle de ASP_Mails, en substituant les variables
|
||||||
|
* de la forme {(NomVariable)} dans l'objet et le corps (texte et HTML).
|
||||||
|
* {(LogoUrl)} est toujours disponible, sans avoir à la passer explicitement.
|
||||||
|
* Retourne true si l'envoi (via l'API Brevo) a réussi.
|
||||||
|
*/
|
||||||
|
function envoyerMail(string $codeMail, string $destinataire, array $variables): bool
|
||||||
|
{
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
$stmt = $pdo->prepare('SELECT MAI_Objet, MAI_Corps, MAI_CorpsHtml FROM ASP_Mails WHERE MAI_Code = ?');
|
||||||
|
$stmt->execute([$codeMail]);
|
||||||
|
$modele = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$modele) {
|
||||||
|
error_log("ASPIC prospects: modèle de mail introuvable ($codeMail)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$variables = $variables + ['LogoUrl' => LOGO_URL];
|
||||||
|
|
||||||
|
$objet = $modele['MAI_Objet'];
|
||||||
|
$corpsTexte = $modele['MAI_Corps'];
|
||||||
|
$corpsHtml = $modele['MAI_CorpsHtml'];
|
||||||
|
foreach ($variables as $cle => $valeur) {
|
||||||
|
$motif = '{(' . $cle . ')}';
|
||||||
|
$objet = str_replace($motif, (string) $valeur, $objet);
|
||||||
|
$corpsTexte = str_replace($motif, (string) $valeur, $corpsTexte);
|
||||||
|
if ($corpsHtml !== null) {
|
||||||
|
$corpsHtml = str_replace($motif, (string) $valeur, $corpsHtml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = getenv('BREVO_API_KEY');
|
||||||
|
if (!$apiKey) {
|
||||||
|
error_log('ASPIC prospects: BREVO_API_KEY non configurée');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$donneesEnvoi = [
|
||||||
|
'sender' => ['name' => BREVO_FROM_NAME, 'email' => BREVO_FROM_EMAIL],
|
||||||
|
'to' => [['email' => $destinataire]],
|
||||||
|
'replyTo' => ['email' => BREVO_REPLY_TO],
|
||||||
|
'subject' => $objet,
|
||||||
|
'textContent' => $corpsTexte,
|
||||||
|
];
|
||||||
|
if ($corpsHtml !== null && trim($corpsHtml) !== '') {
|
||||||
|
$donneesEnvoi['htmlContent'] = $corpsHtml;
|
||||||
|
}
|
||||||
|
$payload = json_encode($donneesEnvoi);
|
||||||
|
|
||||||
|
$ch = curl_init('https://api.brevo.com/v3/smtp/email');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $payload,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'accept: application/json',
|
||||||
|
'api-key: ' . $apiKey,
|
||||||
|
'content-type: application/json',
|
||||||
|
],
|
||||||
|
CURLOPT_TIMEOUT => 10,
|
||||||
|
]);
|
||||||
|
$reponse = curl_exec($ch);
|
||||||
|
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$erreurCurl = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($codeHttp < 200 || $codeHttp >= 300) {
|
||||||
|
error_log("ASPIC prospects: échec envoi Brevo (HTTP $codeHttp) : $reponse $erreurCurl");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Journalise une opération sur un prospect dans ASP_Operations.
|
||||||
|
* OPE_CodeUser est toujours 'Vik' (Viktor) : ces événements sont automatiques,
|
||||||
|
* jamais déclenchés par un utilisateur ASPIC connecté.
|
||||||
|
*/
|
||||||
|
function journaliserOperation(int $idProspect, string $codeAction, ?string $commentaire = null): void
|
||||||
|
{
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO ASP_Operations (OPE_IncrProspect, OPE_CodeUser, OPE_Date, OPE_Action, OPE_Comment)
|
||||||
|
VALUES (?, ?, NOW(), ?, ?)'
|
||||||
|
);
|
||||||
|
$stmt->execute([$idProspect, 'Vik', $codeAction, $commentaire]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule la signature HMAC des paramètres d'un lien de documentation (a/b/c),
|
||||||
|
* pour empêcher qu'un visiteur ne les modifie sans que ça se voie.
|
||||||
|
* La clé ne doit JAMAIS être exposée côté client.
|
||||||
|
*/
|
||||||
|
function signerParametresDoc(int $idProspect, int $idDoc, int $idCampagne): string
|
||||||
|
{
|
||||||
|
$cle = getenv('LIEN_DOC_CLE_SECRETE');
|
||||||
|
if (!$cle) {
|
||||||
|
error_log('ASPIC prospects: LIEN_DOC_CLE_SECRETE non configurée');
|
||||||
|
$cle = '';
|
||||||
|
}
|
||||||
|
return substr(hash_hmac('sha256', "$idProspect-$idDoc-$idCampagne", $cle), 0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit un lien de documentation signé, à insérer dans un mail.
|
||||||
|
*/
|
||||||
|
function genererLienDoc(int $idProspect, int $idDoc, int $idCampagne): string
|
||||||
|
{
|
||||||
|
$signature = signerParametresDoc($idProspect, $idDoc, $idCampagne);
|
||||||
|
return ASPIC_BASE_URL . '/prospect_doc.php?a=' . $idProspect
|
||||||
|
. '&b=' . $idDoc . '&c=' . $idCampagne . '&d=' . $signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine si un numéro de téléphone saisi va dans PSP_Tel ou PSP_Portable :
|
||||||
|
* préfixe 06/07 (après suppression des espaces) => portable, sinon => fixe.
|
||||||
|
*/
|
||||||
|
function routerTelephone(string $telephoneBrut): array
|
||||||
|
{
|
||||||
|
$nettoye = preg_replace('/\s+/', '', $telephoneBrut);
|
||||||
|
if ($nettoye === '') {
|
||||||
|
return ['tel' => null, 'portable' => null];
|
||||||
|
}
|
||||||
|
$prefixe = substr($nettoye, 0, 2);
|
||||||
|
if ($prefixe === '06' || $prefixe === '07') {
|
||||||
|
return ['tel' => null, 'portable' => $nettoye];
|
||||||
|
}
|
||||||
|
return ['tel' => $nettoye, 'portable' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite la soumission du formulaire public pour une campagne donnée.
|
||||||
|
* Dérive automatiquement les codes ASP_Actions et le code du mail de vérification
|
||||||
|
* à partir du code de campagne, suivant la convention "<campagne>-CRE/-VER/-VAL/-DOC/-LEC"
|
||||||
|
* (codes ASP_Actions) et "<campagne>-DEM" (mail de vérification).
|
||||||
|
*/
|
||||||
|
function traiterAjoutProspect(string $codeCampagne): void
|
||||||
|
{
|
||||||
|
autoriserCorsSiNecessaire();
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
// Honeypot anti-spam : champ invisible pour un humain, rempli seulement par un robot.
|
||||||
|
if (!empty($data['site_web'] ?? '')) {
|
||||||
|
http_response_code(200);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nom = trim((string) ($data['nom'] ?? ''));
|
||||||
|
$prenom = trim((string) ($data['prenom'] ?? ''));
|
||||||
|
$telephone = trim((string) ($data['telephone'] ?? ''));
|
||||||
|
$mail = trim((string) ($data['mail'] ?? ''));
|
||||||
|
|
||||||
|
if ($mail === '' && $telephone === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Merci de renseigner au moins une adresse mail ou un numéro de téléphone.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (strlen($nom) > 40 || strlen($prenom) > 40 || strlen($mail) > 40) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Un des champs saisis est trop long.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$telNettoye = preg_replace('/\s+/', '', $telephone);
|
||||||
|
if (strlen($telNettoye) > 14) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Le numéro de téléphone saisi est trop long.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($mail !== '' && !filter_var($mail, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => "L'adresse mail saisie n'est pas valide."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tel = routerTelephone($telephone);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
|
||||||
|
$token = genererTokenVerification();
|
||||||
|
$expiration = date('Y-m-d H:i:s', strtotime('+48 hours'));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO ASP_Prospects
|
||||||
|
(PSP_Nom, PSP_Prenom, PSP_Tel, PSP_Portable, PSP_Mail, PSP_CodeCampagne,
|
||||||
|
PSP_MailVerifie, PSP_TokenVerification, PSP_TokenExpiration, PSP_Actif)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, 1)'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
$nom !== '' ? $nom : null,
|
||||||
|
$prenom !== '' ? $prenom : null,
|
||||||
|
$tel['tel'],
|
||||||
|
$tel['portable'],
|
||||||
|
$mail !== '' ? $mail : null,
|
||||||
|
$codeCampagne,
|
||||||
|
$token,
|
||||||
|
$expiration,
|
||||||
|
]);
|
||||||
|
$idProspect = (int) $pdo->lastInsertId();
|
||||||
|
|
||||||
|
journaliserOperation($idProspect, $codeCampagne . '-CRE');
|
||||||
|
|
||||||
|
if ($mail !== '') {
|
||||||
|
$lien = ASPIC_BASE_URL . '/prospects_verifier.php?token=' . urlencode($token);
|
||||||
|
$envoiOk = envoyerMail($codeCampagne . '-DEM', $mail, [
|
||||||
|
'Prenom' => $prenom,
|
||||||
|
'Nom' => $nom,
|
||||||
|
'Lien' => $lien,
|
||||||
|
]);
|
||||||
|
if ($envoiOk) {
|
||||||
|
journaliserOperation($idProspect, $codeCampagne . '-VER');
|
||||||
|
} else {
|
||||||
|
error_log("ASPIC prospects: échec envoi mail de vérification pour le prospect $idProspect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log('ASPIC prospects: erreur PDO - ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Erreur serveur, merci de réessayer plus tard.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
// prospects_verifier.php
|
||||||
|
// Point d'entrée public (clic sur le lien reçu par mail) : valide l'adresse mail
|
||||||
|
// d'un prospect à partir de son token, quelle que soit la campagne d'origine,
|
||||||
|
// puis envoie automatiquement la documentation par défaut de cette campagne.
|
||||||
|
|
||||||
|
require __DIR__ . '/prospects_core.php';
|
||||||
|
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
|
||||||
|
function pageMessage(string $texte): void
|
||||||
|
{
|
||||||
|
echo '<!doctype html><html lang="fr"><head><meta charset="utf-8">'
|
||||||
|
. '<title>ASPIC - Vérification</title></head>'
|
||||||
|
. '<body style="font-family:sans-serif;max-width:480px;margin:80px auto;text-align:center;">'
|
||||||
|
. '<p>' . htmlspecialchars($texte) . '</p>'
|
||||||
|
. '</body></html>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $_GET['token'] ?? '';
|
||||||
|
if ($token === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
pageMessage('Lien de vérification invalide.');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT PSP_Incr, PSP_Prenom, PSP_Nom, PSP_Mail, PSP_CodeCampagne, PSP_TokenExpiration
|
||||||
|
FROM ASP_Prospects
|
||||||
|
WHERE PSP_TokenVerification = ? AND PSP_MailVerifie = 0 AND PSP_Actif = 1'
|
||||||
|
);
|
||||||
|
$stmt->execute([$token]);
|
||||||
|
$prospect = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$prospect) {
|
||||||
|
pageMessage('Ce lien de vérification est invalide ou a déjà été utilisé.');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strtotime($prospect['PSP_TokenExpiration']) < time()) {
|
||||||
|
pageMessage('Ce lien de vérification a expiré.');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$idProspect = (int) $prospect['PSP_Incr'];
|
||||||
|
$codeCampagne = $prospect['PSP_CodeCampagne'];
|
||||||
|
|
||||||
|
$pdo->prepare(
|
||||||
|
'UPDATE ASP_Prospects
|
||||||
|
SET PSP_MailVerifie = 1, PSP_TokenVerification = NULL, PSP_TokenExpiration = NULL
|
||||||
|
WHERE PSP_Incr = ?'
|
||||||
|
)->execute([$idProspect]);
|
||||||
|
|
||||||
|
journaliserOperation($idProspect, $codeCampagne . '-VAL');
|
||||||
|
|
||||||
|
// Envoi automatique de la doc par défaut de la campagne, si elle en a une de définie.
|
||||||
|
$stmt = $pdo->prepare('SELECT CMP_Incr, CMP_DocDefaut FROM ASP_Campagnes WHERE CMP_CodeCampagne = ?');
|
||||||
|
$stmt->execute([$codeCampagne]);
|
||||||
|
$campagne = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($campagne && $campagne['CMP_DocDefaut']) {
|
||||||
|
$idDoc = (int) $campagne['CMP_DocDefaut'];
|
||||||
|
$idCampagneIncr = (int) $campagne['CMP_Incr'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT DOC_Libelle FROM ASP_Docs WHERE DOC_Incr = ?');
|
||||||
|
$stmt->execute([$idDoc]);
|
||||||
|
$doc = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($doc) {
|
||||||
|
$lien = genererLienDoc($idProspect, $idDoc, $idCampagneIncr);
|
||||||
|
$envoiOk = envoyerMail($codeCampagne . '-DOC', $prospect['PSP_Mail'], [
|
||||||
|
'Prenom' => $prospect['PSP_Prenom'],
|
||||||
|
'Nom' => $prospect['PSP_Nom'],
|
||||||
|
'Lien' => $lien,
|
||||||
|
'Titre' => $doc['DOC_Libelle'],
|
||||||
|
]);
|
||||||
|
if ($envoiOk) {
|
||||||
|
journaliserOperation($idProspect, $codeCampagne . '-DOC');
|
||||||
|
} else {
|
||||||
|
error_log("ASPIC prospects: échec envoi mail de doc pour le prospect $idProspect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageMessage('Merci, votre adresse mail est vérifiée ! Vous allez recevoir notre documentation très prochainement.');
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ASPIC — realisations.php
|
||||||
|
* Journal des actions réalisées sur un contact (ASP_Realisations). Voir
|
||||||
|
* journal de développement, section 2, pour la distinction entre ACT_Comment
|
||||||
|
* (commentaire générique du type d'action, ASP_Actions) et REA_Comment
|
||||||
|
* (commentaire propre à une occurrence, ASP_Realisations) — c'est bien
|
||||||
|
* REA_Comment qui est manipulé ici.
|
||||||
|
*
|
||||||
|
* GET ?id=<CNT_Incr> : historique des réalisations d'un contact, avec le
|
||||||
|
* libellé de l'action (jointure sur ASP_Actions), trié du plus ancien
|
||||||
|
* au plus récent.
|
||||||
|
* POST : journalise une nouvelle réalisation (REA_IncrContact, REA_Action
|
||||||
|
* obligatoires ; REA_Comment optionnel). REA_Date est déterminée côté
|
||||||
|
* serveur (NOW()).
|
||||||
|
*
|
||||||
|
* ⚠️ Session utilisateur intégrée le 24 août 2026 (voir journal, sections 14
|
||||||
|
* et 15) : accès réservé aux utilisateurs identifiés, et REA_CodeUser est
|
||||||
|
* désormais lu directement dans `$_SESSION['USR_Code']` (ouverte par
|
||||||
|
* `aspic.php`) — le champ REA_CodeUser du corps de la requête POST n'est
|
||||||
|
* plus utilisé et ne peut plus être falsifié par le client. Le "JMP" en dur
|
||||||
|
* précédent a été supprimé (n'avait de sens que faute de session).
|
||||||
|
*/
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
if (empty($_SESSION['USR_Code'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Non identifié — veuillez vous connecter.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Connexion base de données impossible']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($method) {
|
||||||
|
case 'GET':
|
||||||
|
if (!isset($_GET['id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'id (REA_IncrContact) manquant']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT r.REA_Incr, r.REA_Date, r.REA_CodeUser, r.REA_Action, r.REA_Comment,
|
||||||
|
a.ACT_Libelle
|
||||||
|
FROM ASP_Realisations r
|
||||||
|
LEFT JOIN ASP_Actions a ON a.ACT_Code = r.REA_Action
|
||||||
|
WHERE r.REA_IncrContact = ?
|
||||||
|
ORDER BY r.REA_Date ASC'
|
||||||
|
);
|
||||||
|
$stmt->execute([$_GET['id']]);
|
||||||
|
echo json_encode(['realisations' => $stmt->fetchAll()]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$idContact = $data['REA_IncrContact'] ?? null;
|
||||||
|
$action = $data['REA_Action'] ?? null;
|
||||||
|
if (!$idContact || !$action) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'REA_IncrContact et REA_Action sont obligatoires']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// REA_CodeUser vient de la session (voir en-tête de ce fichier),
|
||||||
|
// plus du corps de la requête : impossible pour le client de se
|
||||||
|
// faire passer pour un autre utilisateur.
|
||||||
|
$codeUser = $_SESSION['USR_Code'];
|
||||||
|
$commentaire = $data['REA_Comment'] ?? '';
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO ASP_Realisations (REA_IncrContact, REA_CodeUser, REA_Date, REA_Action, REA_Comment)
|
||||||
|
VALUES (?, ?, NOW(), ?, ?)'
|
||||||
|
);
|
||||||
|
$stmt->execute([$idContact, $codeUser, $action, $commentaire]);
|
||||||
|
echo json_encode(['id' => (int)$pdo->lastInsertId()]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Méthode non autorisée']);
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Toute exception PDO (ex. REA_Action trop long pour la colonne, valeur
|
||||||
|
// rejetée par une contrainte) renvoie du JSON exploitable côté front
|
||||||
|
// plutôt qu'une erreur PHP brute — voir contacts.php pour le même
|
||||||
|
// correctif du 24 août 2026.
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ASPIC — statuts.php
|
||||||
|
* Table de référence ASP_Statuts (liée à ASP_Contacts via CNT_Statut =
|
||||||
|
* STA_Code, lien applicatif, sans contrainte FOREIGN KEY SQL — voir journal
|
||||||
|
* de développement, section 2).
|
||||||
|
*
|
||||||
|
* GET seul : ASP_Statuts est une table gérée à la main via phpMyAdmin, pas de
|
||||||
|
* création depuis l'application (contrairement à ASP_Professions).
|
||||||
|
*
|
||||||
|
* ⚠️ Session utilisateur intégrée le 24 août 2026 (voir journal, section 14) :
|
||||||
|
* accès réservé aux utilisateurs identifiés (`$_SESSION['USR_Code']`, ouverte
|
||||||
|
* par `aspic.php`).
|
||||||
|
*/
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
if (empty($_SESSION['USR_Code'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Non identifié — veuillez vous connecter.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Méthode non autorisée']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = getDbConnection();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Connexion base de données impossible']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Tri par STA_Incr (ordre de création/incrément), pas par libellé —
|
||||||
|
// demande explicite de Jean-Marc du 24 août 2026.
|
||||||
|
$stmt = $pdo->query('SELECT STA_Code, STA_Libellé FROM ASP_Statuts ORDER BY STA_Incr ASC');
|
||||||
|
echo json_encode(['statuts' => $stmt->fetchAll()]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user