diff --git a/MetroDF.ttf b/MetroDF.ttf new file mode 100644 index 0000000..9d31af4 Binary files /dev/null and b/MetroDF.ttf differ diff --git a/MetroDF.woff2 b/MetroDF.woff2 new file mode 100644 index 0000000..005307f Binary files /dev/null and b/MetroDF.woff2 differ diff --git a/aspic.php b/aspic.php new file mode 100644 index 0000000..8b5e354 --- /dev/null +++ b/aspic.php @@ -0,0 +1,93 @@ + 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'], + ], +]); diff --git a/contacts.php b/contacts.php new file mode 100644 index 0000000..1b50508 --- /dev/null +++ b/contacts.php @@ -0,0 +1,301 @@ + '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()]); +} diff --git a/db.php b/db.php new file mode 100644 index 0000000..31aa4c2 --- /dev/null +++ b/db.php @@ -0,0 +1,16 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]; + + return new PDO($dsn, $user, $pass, $options); +} diff --git a/identification.html b/identification.html new file mode 100644 index 0000000..5ffa4fe --- /dev/null +++ b/identification.html @@ -0,0 +1,570 @@ + + +
+ + +' . htmlspecialchars($texte) . '
' + . ''; +} + +$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.'); diff --git a/realisations.php b/realisations.php new file mode 100644 index 0000000..2e09f60 --- /dev/null +++ b/realisations.php @@ -0,0 +1,100 @@ + : 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()]); +} diff --git a/statuts.php b/statuts.php new file mode 100644 index 0000000..14398e2 --- /dev/null +++ b/statuts.php @@ -0,0 +1,48 @@ + '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()]); +}