This commit is contained in:
KANE LAZENI 2026-02-22 10:01:06 +00:00
parent 1308517894
commit 11b16d3c45
6 changed files with 2065 additions and 0 deletions

View File

@ -0,0 +1,361 @@
<?php
session_start();
session_unset();
function afficherMessage($message) {
echo "<!DOCTYPE html>
<html lang='fr'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Erreur</title>
<link href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css' rel='stylesheet'>
<link href='https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css' rel='stylesheet'>
<style>
.error-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.error-card {
background: white;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
padding: 2rem;
max-width: 500px;
width: 100%;
border-left: 4px solid #dc3545;
}
</style>
</head>
<body class='bg-light'>
<div class='error-container'>
<div class='error-card'>
<div class='text-center mb-3'>
<i class='bi bi-exclamation-triangle-fill text-danger' style='font-size: 3rem;'></i>
</div>
<h4 class='text-center text-danger mb-3'>Erreur</h4>
<p class='text-center text-muted'>{$message}</p>
</div>
</div>
</body>
</html>";
exit();
}
if (!isset($_GET['lg'])) {
afficherMessage("Paramètre langue absent de la requête!");
}
$lg = $_GET['lg'];
$codeLangue = base64_decode($lg);
$tab_code_langue = ["fr_FR", "en_US"];
if (!in_array($codeLangue, $tab_code_langue)) {
afficherMessage("Langue inconnue!");
}
if (!isset($_GET['codeEntite'])) {
$msg = $codeLangue == 'en_US' ? "Entity parameter missing from query!" : "Paramètre entité absent de la requête!";
afficherMessage($msg);
}
if (!isset($_GET['idAdherent'])) {
$msg = $codeLangue == 'en_US' ? "Family parameter missing from query!" : "Paramètre famille absent de la requête!";
afficherMessage($msg);
}
if (!isset($_GET['dossier'])) {
$msg = $codeLangue == 'en_US' ? "Entity dossier missing from query!" : "Paramètre dossier absent de la requête!";
afficherMessage($msg);
}
$_SESSION['codeLangue'] = $lg;
$_SESSION['codeEntite'] = $_GET['codeEntite'];
$_SESSION['idAdherent'] = $_GET['idAdherent'];
$_SESSION['dossier'] = $_GET['dossier'];
$codeEntite = $_GET['codeEntite'];
$idAdherent = $_GET['idAdherent'];
$dossier = $_GET['dossier'];
$title = $codeLangue == 'en_US' ? "Contest a medical record" : "Contester un dossier médical";
$label = $codeLangue == 'en_US' ? "Submit" : "Soumettre";
$labelMotifContestation = $codeLangue == 'en_US' ? "Reason for the contestation:" : "Motif de la contestation:";
$placeholder = $codeLangue == 'en_US' ? "Please describe in detail the reason for your contestation..." : "Veuillez décrire en détail le motif de votre contestation...";
$action = "/Contestation/Validercontestation.php?" .
"codeEntite=" . urlencode($codeEntite) .
"&idAdherent=" . urlencode($idAdherent) .
"&dossier=" . urlencode($dossier) .
"&lg=" . urlencode($lg);
?>
<!doctype html>
<html lang="<?= $codeLangue == 'en_US' ? 'en' : 'fr' ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($title) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<link rel="icon" href="/Contestation/favicon.ico"/>
<style>
.contestation-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.contestation-card {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
overflow: hidden;
max-width: 700px;
margin: 40px auto;
}
.contestation-header {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
padding: 2rem;
text-align: center;
position: relative;
}
.contestation-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none"><path d="M0,0 L100,0 L100,100 Z" fill="rgba(255,255,255,0.1)"/></svg>');
background-size: cover;
}
.contestation-header h1 {
font-weight: 600;
margin: 0;
font-size: 1.8rem;
position: relative;
z-index: 1;
}
.contestation-body {
padding: 2.5rem;
}
.form-label {
font-weight: 600;
color: #333;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.required::after {
content: " *";
color: #dc3545;
}
.form-control {
border: 2px solid #e9ecef;
border-radius: 10px;
padding: 1rem;
font-size: 1rem;
transition: all 0.3s ease;
}
.form-control:focus {
border-color: #4caf50;
box-shadow: 0 0 0 0.2rem rgba(76, 175, 80, 0.25);
}
.btn-submit {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
border: none;
border-radius: 10px;
padding: 1rem 2rem;
font-size: 1.1rem;
font-weight: 600;
transition: all 0.3s ease;
width: 100%;
margin-top: 1rem;
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(76, 175, 80, 0.3);
}
.character-count {
text-align: right;
font-size: 0.875rem;
color: #6c757d;
margin-top: 0.5rem;
}
.info-section {
background: #f8f9fa;
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 2rem;
border-left: 4px solid #4caf50;
display: none;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-item i {
color: #4caf50;
margin-right: 0.75rem;
font-size: 1.1rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.contestation-container {
padding: 15px;
}
.contestation-card {
margin: 20px auto;
}
.contestation-header {
padding: 1.5rem;
}
.contestation-header h1 {
font-size: 1.5rem;
}
.contestation-body {
padding: 1.5rem;
}
}
@media (max-width: 576px) {
.contestation-container {
padding: 10px;
}
.contestation-header {
padding: 1.25rem;
}
.contestation-header h1 {
font-size: 1.3rem;
}
.contestation-body {
padding: 1.25rem;
}
.info-section {
padding: 1rem;
}
}
</style>
</head>
<body>
<div class="contestation-container">
<div class="contestation-card">
<div class="contestation-header">
<h1>
<i class="bi bi-clipboard-x me-2"></i>
<?= htmlspecialchars($title) ?>
</h1>
</div>
<div class="contestation-body">
<!-- Information Section -->
<div class="info-section">
<div class="info-item">
<i class="bi bi-info-circle"></i>
<strong><?= $codeLangue == 'en_US' ? 'Information:' : 'Information :' ?></strong>
</div>
<div class="info-item">
<i class="bi bi-building"></i>
<span><?= $codeLangue == 'en_US' ? 'Entity:' : 'Entité :' ?> <?= htmlspecialchars($codeEntite) ?></span>
</div>
<div class="info-item">
<i class="bi bi-person"></i>
<span><?= $codeLangue == 'en_US' ? 'Family ID:' : 'ID Famille :' ?> <?= htmlspecialchars($idAdherent) ?></span>
</div>
<div class="info-item">
<i class="bi bi-folder"></i>
<span><?= $codeLangue == 'en_US' ? 'Record:' : 'Dossier :' ?> <?= htmlspecialchars($dossier) ?></span>
</div>
</div>
<!-- Contestation Form -->
<form action="<?= $action ?>" method="post">
<div class="mb-4">
<label for="motifContestation" class="form-label required">
<?= htmlspecialchars($labelMotifContestation) ?>
</label>
<textarea
id="motifContestation"
name="motifContestation"
class="form-control border border-success"
rows="8"
required
placeholder="<?= htmlspecialchars($placeholder) ?>"
maxlength="1000"
oninput="updateCharacterCount(this)"></textarea>
<div class="character-count">
<span id="charCount">0</span>/1000 <?= $codeLangue == 'en_US' ? 'characters' : 'caractères' ?>
</div>
</div>
<button type="submit" class="btn-submit">
<i class="bi bi-send-check me-2"></i>
<?= htmlspecialchars($label) ?>
</button>
</form>
</div>
</div>
</div>
<script>
function updateCharacterCount(textarea) {
const charCount = textarea.value.length;
document.getElementById('charCount').textContent = charCount;
// Change color when approaching limit
const charCountElement = document.getElementById('charCount');
if (charCount > 900) {
charCountElement.style.color = '#dc3545';
charCountElement.style.fontWeight = 'bold';
} else if (charCount > 750) {
charCountElement.style.color = '#ffc107';
charCountElement.style.fontWeight = 'bold';
} else {
charCountElement.style.color = '#6c757d';
charCountElement.style.fontWeight = 'normal';
}
}
// Initialize character count on page load
document.addEventListener('DOMContentLoaded', function() {
const textarea = document.getElementById('motifContestation');
updateCharacterCount(textarea);
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,361 @@
<?php
session_start();
session_unset();
function afficherMessage($message) {
echo "<!DOCTYPE html>
<html lang='fr'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Erreur</title>
<link href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css' rel='stylesheet'>
<link href='https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css' rel='stylesheet'>
<style>
.error-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.error-card {
background: white;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
padding: 2rem;
max-width: 500px;
width: 100%;
border-left: 4px solid #dc3545;
}
</style>
</head>
<body class='bg-light'>
<div class='error-container'>
<div class='error-card'>
<div class='text-center mb-3'>
<i class='bi bi-exclamation-triangle-fill text-danger' style='font-size: 3rem;'></i>
</div>
<h4 class='text-center text-danger mb-3'>Erreur</h4>
<p class='text-center text-muted'>{$message}</p>
</div>
</div>
</body>
</html>";
exit();
}
if (!isset($_GET['lg'])) {
afficherMessage("Paramètre langue absent de la requête!");
}
$lg = $_GET['lg'];
$codeLangue = base64_decode($lg);
$tab_code_langue = ["fr_FR", "en_US"];
if (!in_array($codeLangue, $tab_code_langue)) {
afficherMessage("Langue inconnue!");
}
if (!isset($_GET['codeEntite'])) {
$msg = $codeLangue == 'en_US' ? "Entity parameter missing from query!" : "Paramètre entité absent de la requête!";
afficherMessage($msg);
}
if (!isset($_GET['idAdherent'])) {
$msg = $codeLangue == 'en_US' ? "Family parameter missing from query!" : "Paramètre famille absent de la requête!";
afficherMessage($msg);
}
if (!isset($_GET['dossier'])) {
$msg = $codeLangue == 'en_US' ? "Entity dossier missing from query!" : "Paramètre dossier absent de la requête!";
afficherMessage($msg);
}
$_SESSION['codeLangue'] = $lg;
$_SESSION['codeEntite'] = $_GET['codeEntite'];
$_SESSION['idAdherent'] = $_GET['idAdherent'];
$_SESSION['dossier'] = $_GET['dossier'];
$codeEntite = $_GET['codeEntite'];
$idAdherent = $_GET['idAdherent'];
$dossier = $_GET['dossier'];
$title = $codeLangue == 'en_US' ? "Contest a medical record" : "Contester un dossier médical";
$label = $codeLangue == 'en_US' ? "Submit" : "Soumettre";
$labelMotifContestation = $codeLangue == 'en_US' ? "Reason for the contestation:" : "Motif de la contestation:";
$placeholder = $codeLangue == 'en_US' ? "Please describe in detail the reason for your contestation..." : "Veuillez décrire en détail le motif de votre contestation...";
$action = "/Contestation/Validercontestation.php?" .
"codeEntite=" . urlencode($codeEntite) .
"&idAdherent=" . urlencode($idAdherent) .
"&dossier=" . urlencode($dossier) .
"&lg=" . urlencode($lg);
?>
<!doctype html>
<html lang="<?= $codeLangue == 'en_US' ? 'en' : 'fr' ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($title) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<link rel="icon" href="/Contestation/favicon.ico"/>
<style>
.contestation-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.contestation-card {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
overflow: hidden;
max-width: 700px;
margin: 40px auto;
}
.contestation-header {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
padding: 2rem;
text-align: center;
position: relative;
}
.contestation-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none"><path d="M0,0 L100,0 L100,100 Z" fill="rgba(255,255,255,0.1)"/></svg>');
background-size: cover;
}
.contestation-header h1 {
font-weight: 600;
margin: 0;
font-size: 1.8rem;
position: relative;
z-index: 1;
}
.contestation-body {
padding: 2.5rem;
}
.form-label {
font-weight: 600;
color: #333;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.required::after {
content: " *";
color: #dc3545;
}
.form-control {
border: 2px solid #e9ecef;
border-radius: 10px;
padding: 1rem;
font-size: 1rem;
transition: all 0.3s ease;
}
.form-control:focus {
border-color: #4caf50;
box-shadow: 0 0 0 0.2rem rgba(76, 175, 80, 0.25);
}
.btn-submit {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
border: none;
border-radius: 10px;
padding: 1rem 2rem;
font-size: 1.1rem;
font-weight: 600;
transition: all 0.3s ease;
width: 100%;
margin-top: 1rem;
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(76, 175, 80, 0.3);
}
.character-count {
text-align: right;
font-size: 0.875rem;
color: #6c757d;
margin-top: 0.5rem;
}
.info-section {
background: #f8f9fa;
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 2rem;
border-left: 4px solid #4caf50;
display: none;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-item i {
color: #4caf50;
margin-right: 0.75rem;
font-size: 1.1rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.contestation-container {
padding: 15px;
}
.contestation-card {
margin: 20px auto;
}
.contestation-header {
padding: 1.5rem;
}
.contestation-header h1 {
font-size: 1.5rem;
}
.contestation-body {
padding: 1.5rem;
}
}
@media (max-width: 576px) {
.contestation-container {
padding: 10px;
}
.contestation-header {
padding: 1.25rem;
}
.contestation-header h1 {
font-size: 1.3rem;
}
.contestation-body {
padding: 1.25rem;
}
.info-section {
padding: 1rem;
}
}
</style>
</head>
<body>
<div class="contestation-container">
<div class="contestation-card">
<div class="contestation-header">
<h1>
<i class="bi bi-clipboard-x me-2"></i>
<?= htmlspecialchars($title) ?>
</h1>
</div>
<div class="contestation-body">
<!-- Information Section -->
<div class="info-section">
<div class="info-item">
<i class="bi bi-info-circle"></i>
<strong><?= $codeLangue == 'en_US' ? 'Information:' : 'Information :' ?></strong>
</div>
<div class="info-item">
<i class="bi bi-building"></i>
<span><?= $codeLangue == 'en_US' ? 'Entity:' : 'Entité :' ?> <?= htmlspecialchars($codeEntite) ?></span>
</div>
<div class="info-item">
<i class="bi bi-person"></i>
<span><?= $codeLangue == 'en_US' ? 'Family ID:' : 'ID Famille :' ?> <?= htmlspecialchars($idAdherent) ?></span>
</div>
<div class="info-item">
<i class="bi bi-folder"></i>
<span><?= $codeLangue == 'en_US' ? 'Record:' : 'Dossier :' ?> <?= htmlspecialchars($dossier) ?></span>
</div>
</div>
<!-- Contestation Form -->
<form action="<?= $action ?>" method="post">
<div class="mb-4">
<label for="motifContestation" class="form-label required">
<?= htmlspecialchars($labelMotifContestation) ?>
</label>
<textarea
id="motifContestation"
name="motifContestation"
class="form-control border border-success"
rows="8"
required
placeholder="<?= htmlspecialchars($placeholder) ?>"
maxlength="1000"
oninput="updateCharacterCount(this)"></textarea>
<div class="character-count">
<span id="charCount">0</span>/1000 <?= $codeLangue == 'en_US' ? 'characters' : 'caractères' ?>
</div>
</div>
<button type="submit" class="btn-submit">
<i class="bi bi-send-check me-2"></i>
<?= htmlspecialchars($label) ?>
</button>
</form>
</div>
</div>
</div>
<script>
function updateCharacterCount(textarea) {
const charCount = textarea.value.length;
document.getElementById('charCount').textContent = charCount;
// Change color when approaching limit
const charCountElement = document.getElementById('charCount');
if (charCount > 900) {
charCountElement.style.color = '#dc3545';
charCountElement.style.fontWeight = 'bold';
} else if (charCount > 750) {
charCountElement.style.color = '#ffc107';
charCountElement.style.fontWeight = 'bold';
} else {
charCountElement.style.color = '#6c757d';
charCountElement.style.fontWeight = 'normal';
}
}
// Initialize character count on page load
document.addEventListener('DOMContentLoaded', function() {
const textarea = document.getElementById('motifContestation');
updateCharacterCount(textarea);
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,58 @@
<?php
/**
* API pour vérifier le statut de la vérification faciale en temps réel
* Utilisé par le prestataire pour savoir quand l'assuré a validé
*/
header('Content-Type: application/json');
require_once 'config.php';
require_once 'database.php';
$requestId = $_GET['request_id'] ?? null;
if (!$requestId) {
echo json_encode([
'success' => false,
'message' => 'ID de requête manquant'
]);
exit;
}
$db = new Database();
$conn = $db->getConnection();
// Récupérer le statut de la vérification
$sql = "SELECT vr.*, pas.session_token
FROM facial_verification_requests vr
LEFT JOIN prestation_authorization_sessions pas
ON vr.id = pas.verification_request_id
AND pas.status = 'active'
WHERE vr.id = ?";
$stmt = $conn->prepare($sql);
$stmt->execute([$requestId]);
$request = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$request) {
echo json_encode([
'success' => false,
'message' => 'Requête non trouvée'
]);
exit;
}
// Calculer le temps restant
$now = new DateTime();
$expiresAt = new DateTime($request['expires_at']);
$interval = $now->diff($expiresAt);
$secondsRemaining = ($expiresAt->getTimestamp() - $now->getTimestamp());
echo json_encode([
'success' => true,
'status' => $request['status'],
'attempts' => $request['attempts'],
'match_confidence' => $request['match_confidence'],
'session_token' => $request['session_token'],
'seconds_remaining' => max(0, $secondsRemaining),
'verified_at' => $request['verified_at']
]);

View File

@ -0,0 +1,513 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>INTER-SANTÉ - Vérification d'identité</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 100%;
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
}
.content {
padding: 40px 30px;
}
.step {
display: none;
}
.step.active {
display: block;
}
.video-container {
position: relative;
background: #000;
border-radius: 15px;
overflow: hidden;
margin: 20px 0;
}
#video, #canvas {
width: 100%;
display: block;
border-radius: 15px;
}
#canvas {
display: none;
}
.face-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 250px;
height: 300px;
border: 3px dashed rgba(255, 255, 255, 0.7);
border-radius: 50%;
pointer-events: none;
}
.instructions {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin: 20px 0;
}
.instructions h3 {
color: #2c3e50;
margin-bottom: 15px;
display: flex;
align-items: center;
}
.instructions h3::before {
content: "";
margin-right: 10px;
}
.instructions ul {
list-style: none;
padding-left: 0;
}
.instructions li {
padding: 8px 0;
padding-left: 25px;
position: relative;
}
.instructions li::before {
content: "";
position: absolute;
left: 0;
color: #27ae60;
font-weight: bold;
}
.button {
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
color: white;
border: none;
padding: 15px 30px;
font-size: 16px;
border-radius: 10px;
cursor: pointer;
width: 100%;
margin: 10px 0;
transition: transform 0.2s, box-shadow 0.2s;
font-weight: 600;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.button.capture {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
}
.button.retry {
background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%);
}
.status {
padding: 15px;
border-radius: 10px;
margin: 20px 0;
text-align: center;
font-weight: 500;
}
.status.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.preview-image {
max-width: 100%;
border-radius: 15px;
margin: 20px 0;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.match-result {
text-align: center;
padding: 30px;
}
.match-result .icon {
font-size: 80px;
margin-bottom: 20px;
}
.match-result h2 {
color: #2c3e50;
margin-bottom: 15px;
}
.countdown {
font-size: 14px;
color: #7f8c8d;
text-align: center;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🏥 INTER-SANTÉ</h1>
<p>Vérification d'identité sécurisée</p>
</div>
<div class="content">
<!-- Étape 1: Chargement -->
<div id="step-loading" class="step active">
<div class="loader"></div>
<p style="text-align: center; color: #7f8c8d;">Vérification du lien...</p>
</div>
<!-- Étape 2: Instructions -->
<div id="step-instructions" class="step">
<div class="instructions">
<h3>Instructions pour la vérification</h3>
<ul>
<li>Positionnez votre visage dans l'ovale</li>
<li>Assurez-vous d'être dans un endroit bien éclairé</li>
<li>Regardez directement la caméra</li>
<li>Restez immobile lors de la capture</li>
<li>Retirez lunettes de soleil, casquette ou masque</li>
</ul>
</div>
<button class="button" onclick="startCamera()">📸 Démarrer la caméra</button>
</div>
<!-- Étape 3: Capture -->
<div id="step-capture" class="step">
<div class="video-container">
<video id="video" autoplay playsinline></video>
<canvas id="canvas"></canvas>
<div class="face-overlay"></div>
</div>
<div id="camera-status" class="status info">
Positionnez votre visage dans l'ovale
</div>
<button class="button capture" onclick="capturePhoto()">📷 Prendre la photo</button>
</div>
<!-- Étape 4: Confirmation -->
<div id="step-confirm" class="step">
<h3 style="color: #2c3e50; margin-bottom: 15px;">Confirmez votre photo</h3>
<img id="preview" class="preview-image" alt="Votre photo">
<button class="button" onclick="verifyPhoto()"> Confirmer et vérifier</button>
<button class="button retry" onclick="retakePhoto()"> Reprendre la photo</button>
</div>
<!-- Étape 5: Vérification en cours -->
<div id="step-verifying" class="step">
<div class="loader"></div>
<p style="text-align: center; color: #7f8c8d; margin-top: 20px;">
Vérification de votre identité en cours...<br>
<small>Merci de patienter</small>
</p>
</div>
<!-- Étape 6: Résultat -->
<div id="step-result" class="step">
<div class="match-result">
<div class="icon" id="result-icon"></div>
<h2 id="result-title"></h2>
<p id="result-message"></p>
</div>
<div id="countdown" class="countdown"></div>
</div>
<!-- Étape 7: Erreur -->
<div id="step-error" class="step">
<div class="status error">
<h3> Erreur</h3>
<p id="error-message"></p>
</div>
</div>
</div>
</div>
<script>
let video = document.getElementById('video');
let canvas = document.getElementById('canvas');
let context = canvas.getContext('2d');
let stream = null;
let verificationToken = null;
let capturedImage = null;
// Récupérer le token depuis l'URL
const urlParams = new URLSearchParams(window.location.search);
verificationToken = urlParams.get('token');
// Initialisation
window.onload = function() {
if (!verificationToken) {
showError('Lien de vérification invalide');
return;
}
// Vérifier la validité du token
validateToken();
};
function validateToken() {
fetch('verify_facial_api.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'validate_token',
token: verificationToken
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showStep('step-instructions');
} else {
showError(data.message || 'Lien expiré ou invalide');
}
})
.catch(error => {
showError('Erreur de connexion au serveur');
});
}
function showStep(stepId) {
document.querySelectorAll('.step').forEach(step => {
step.classList.remove('active');
});
document.getElementById(stepId).classList.add('active');
}
function showError(message) {
document.getElementById('error-message').textContent = message;
showStep('step-error');
}
async function startCamera() {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'user',
width: { ideal: 1280 },
height: { ideal: 720 }
}
});
video.srcObject = stream;
showStep('step-capture');
} catch (error) {
console.error('Erreur caméra:', error);
showError('Impossible d\'accéder à la caméra. Veuillez autoriser l\'accès.');
}
}
function capturePhoto() {
// Configurer le canvas
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// Capturer l'image
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// Convertir en base64
capturedImage = canvas.toDataURL('image/jpeg', 0.9);
// Afficher l'aperçu
document.getElementById('preview').src = capturedImage;
// Arrêter la caméra
stopCamera();
// Passer à l'étape de confirmation
showStep('step-confirm');
}
function retakePhoto() {
capturedImage = null;
startCamera();
}
function stopCamera() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
}
function verifyPhoto() {
showStep('step-verifying');
fetch('verify_facial_api.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'verify_face',
token: verificationToken,
image: capturedImage
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.match) {
showSuccess(data);
} else {
showFailure(data);
}
})
.catch(error => {
showError('Erreur lors de la vérification: ' + error.message);
});
}
function showSuccess(data) {
document.getElementById('result-icon').textContent = '✅';
document.getElementById('result-title').textContent = 'Identité vérifiée !';
document.getElementById('result-title').style.color = '#27ae60';
document.getElementById('result-message').innerHTML =
`Votre identité a été confirmée avec succès.<br>
Confiance: ${data.confidence}%<br><br>
<strong>Vous pouvez maintenant accéder à vos prestations.</strong>`;
showStep('step-result');
// Redirection automatique après 5 secondes
startCountdown(5, () => {
window.location.href = data.redirect_url || 'dashboard.php';
});
}
function showFailure(data) {
document.getElementById('result-icon').textContent = '❌';
document.getElementById('result-title').textContent = 'Vérification échouée';
document.getElementById('result-title').style.color = '#e74c3c';
document.getElementById('result-message').innerHTML =
`${data.message || 'Votre visage ne correspond pas à notre base de données.'}<br><br>
Si vous pensez qu'il s'agit d'une erreur, veuillez contacter notre service client.<br>
<strong>Tentatives restantes: ${data.attempts_remaining || 0}</strong>`;
showStep('step-result');
// Permettre une nouvelle tentative si disponible
if (data.attempts_remaining > 0) {
setTimeout(() => {
location.reload();
}, 5000);
}
}
function startCountdown(seconds, callback) {
let remaining = seconds;
const countdownEl = document.getElementById('countdown');
const interval = setInterval(() => {
countdownEl.textContent = `Redirection dans ${remaining} secondes...`;
remaining--;
if (remaining < 0) {
clearInterval(interval);
callback();
}
}, 1000);
}
// Nettoyer la caméra quand on quitte la page
window.onbeforeunload = function() {
stopCamera();
};
</script>
</body>
</html>

View File

@ -0,0 +1,341 @@
<?php
/**
* Exemple d'intégration de la vérification faciale dans le flux existant
* À utiliser après la lecture du tag NFC ou QR code
*/
require_once 'config.php';
require_once 'database.php';
require_once 'send_verification_link.php';
// ====================================================================
// SCÉNARIO 1: Lecture du tag NFC/QR Code
// ====================================================================
/**
* Fonction appelée après la lecture réussie du tag NFC ou QR code
*/
function handleNFCOrQRCodeScan($tagData) {
// 1. Décoder les données du tag
$assureData = json_decode($tagData, true);
$assureId = $assureData['assure_id'];
// 2. Vérifier que l'assuré existe
$db = new Database();
$conn = $db->getConnection();
$sql = "SELECT id, nom, prenoms, numero_carte, email, telephone, photo_reference_path
FROM assures
WHERE id = ? AND statut = 'actif'";
$stmt = $conn->prepare($sql);
$stmt->execute([$assureId]);
$assure = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$assure) {
return [
'success' => false,
'message' => 'Assuré non trouvé ou inactif'
];
}
// 3. Vérifier si une photo de référence existe
if (empty($assure['photo_reference_path'])) {
// Pas de photo de référence = accès sans vérification faciale
// Loguer cette tentative pour sécurité
logAction("Accès sans vérification faciale - Assuré #{$assureId} - Aucune photo de référence", 'WARNING');
return [
'success' => true,
'require_facial_verification' => false,
'message' => 'Accès autorisé (pas de photo de référence)',
'assure' => $assure
];
}
// 4. Envoyer le lien de vérification faciale
$verifier = new FacialVerificationLink($conn);
// Choisir le canal d'envoi selon les préférences
$method = 'both'; // 'email', 'whatsapp', ou 'both'
$result = $verifier->sendVerificationLink($assureId, $method);
if ($result['success']) {
return [
'success' => true,
'require_facial_verification' => true,
'message' => 'Lien de vérification envoyé',
'email_sent' => $result['email_sent'],
'whatsapp_sent' => $result['whatsapp_sent'],
'assure' => $assure
];
} else {
return [
'success' => false,
'message' => $result['message']
];
}
}
// ====================================================================
// SCÉNARIO 2: Vérification de session avant saisie des prestations
// ====================================================================
/**
* Vérifie si l'utilisateur a une session d'autorisation valide
* À appeler au début de saisie_prestations.php
*/
function checkAuthorizationSession() {
$sessionToken = $_GET['token'] ?? $_SESSION['authorization_token'] ?? null;
if (!$sessionToken) {
return [
'authorized' => false,
'message' => 'Aucune session d\'autorisation'
];
}
$db = new Database();
$conn = $db->getConnection();
$sql = "SELECT pas.*, a.id as assure_id, a.nom, a.prenoms, a.numero_carte
FROM prestation_authorization_sessions pas
JOIN assures a ON pas.assure_id = a.id
WHERE pas.session_token = ?
AND pas.status = 'active'
AND pas.expires_at > NOW()";
$stmt = $conn->prepare($sql);
$stmt->execute([$sessionToken]);
$session = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$session) {
return [
'authorized' => false,
'message' => 'Session invalide ou expirée'
];
}
// Stocker en session PHP pour les requêtes suivantes
$_SESSION['authorization_token'] = $sessionToken;
$_SESSION['authorized_assure_id'] = $session['assure_id'];
return [
'authorized' => true,
'assure' => [
'id' => $session['assure_id'],
'nom' => $session['nom'],
'prenoms' => $session['prenoms'],
'numero_carte' => $session['numero_carte']
],
'session' => $session
];
}
/**
* Marque une session comme utilisée après saisie d'une prestation
*/
function markSessionAsUsed($sessionToken) {
$db = new Database();
$conn = $db->getConnection();
$sql = "UPDATE prestation_authorization_sessions
SET prestations_saisies = prestations_saisies + 1,
used_at = NOW()
WHERE session_token = ?";
$stmt = $conn->prepare($sql);
return $stmt->execute([$sessionToken]);
}
// ====================================================================
// SCÉNARIO 3: Interface utilisateur pour le prestataire
// ====================================================================
/**
* Génère l'interface HTML pour le prestataire après scan NFC/QR
*/
function renderVerificationWaitingScreen($assure, $verificationSent) {
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vérification en attente - INTER-SANTÉ</title>
<style>
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: 50px auto;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 40px;
text-align: center;
}
.spinner {
width: 60px;
height: 60px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 30px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.assure-info {
background: #ecf0f1;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.status {
color: #7f8c8d;
font-size: 14px;
margin: 10px 0;
}
.channels {
display: flex;
justify-content: center;
gap: 20px;
margin: 20px 0;
}
.channel {
padding: 10px 20px;
border-radius: 5px;
font-size: 14px;
}
.channel.sent {
background: #d4edda;
color: #155724;
}
.channel.not-sent {
background: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<div class="container">
<h1>🏥 INTER-SANTÉ</h1>
<h2>Vérification d'identité en cours</h2>
<div class="assure-info">
<h3><?php echo htmlspecialchars($assure['prenoms'] . ' ' . $assure['nom']); ?></h3>
<p>Carte <?php echo htmlspecialchars($assure['numero_carte']); ?></p>
</div>
<div class="spinner"></div>
<p class="status">
Un lien de vérification a été envoyé à l'assuré.<br>
En attente de la vérification faciale...
</p>
<div class="channels">
<?php if ($verificationSent['email_sent']): ?>
<div class="channel sent"> Email envoyé</div>
<?php else: ?>
<div class="channel not-sent"> Email non envoyé</div>
<?php endif; ?>
<?php if ($verificationSent['whatsapp_sent']): ?>
<div class="channel sent"> WhatsApp envoyé</div>
<?php else: ?>
<div class="channel not-sent"> WhatsApp non envoyé</div>
<?php endif; ?>
</div>
<p style="font-size: 12px; color: #95a5a6;">
Le lien de vérification est valable pendant 15 minutes
</p>
</div>
<script>
// Vérifier le statut toutes les 3 secondes
const requestId = <?php echo $verificationSent['request_id']; ?>;
function checkVerificationStatus() {
fetch('check_verification_status.php?request_id=' + requestId)
.then(response => response.json())
.then(data => {
if (data.status === 'verified') {
// Rediriger vers la saisie des prestations
window.location.href = 'saisie_prestations.php?token=' + data.session_token;
} else if (data.status === 'failed' || data.status === 'expired') {
// Afficher un message d'erreur
alert('La vérification a échoué ou a expiré');
window.location.href = 'index.php';
}
});
}
// Vérifier toutes les 3 secondes
setInterval(checkVerificationStatus, 3000);
</script>
</body>
</html>
<?php
}
// ====================================================================
// EXEMPLE D'UTILISATION COMPLÈTE
// ====================================================================
// Dans votre fichier de scan NFC/QR (ex: scan_handler.php)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$tagData = $_POST['tag_data'] ?? null;
if (!$tagData) {
echo json_encode(['success' => false, 'message' => 'Données manquantes']);
exit;
}
// Traiter le scan
$result = handleNFCOrQRCodeScan($tagData);
if ($result['success'] && $result['require_facial_verification']) {
// Afficher l'écran d'attente
renderVerificationWaitingScreen($result['assure'], $result);
} elseif ($result['success'] && !$result['require_facial_verification']) {
// Accès direct (pas de photo de référence)
header('Location: saisie_prestations.php?assure_id=' . $result['assure']['id']);
} else {
// Erreur
echo json_encode($result);
}
}
// Dans votre fichier saisie_prestations.php
session_start();
// Vérifier l'autorisation
$auth = checkAuthorizationSession();
if (!$auth['authorized']) {
die('Accès non autorisé. Veuillez scanner la carte de l\'assuré.');
}
// L'utilisateur est autorisé, afficher le formulaire de saisie
$assure = $auth['assure'];
// ... Votre code de saisie de prestations ...
// Après enregistrement d'une prestation
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_prestation'])) {
// Enregistrer la prestation
// ...
// Marquer la session comme utilisée
markSessionAsUsed($_SESSION['authorization_token']);
}

View File

@ -0,0 +1,431 @@
<?php
/**
* API Backend pour la vérification faciale
* Gère la validation des tokens et la comparaison des visages
*/
header('Content-Type: application/json');
require_once 'config.php';
require_once 'database.php';
class FacialVerificationAPI {
private $db;
private $maxAttempts = 3;
public function __construct($db) {
$this->db = $db;
}
/**
* Valide un token de vérification
*/
public function validateToken($token) {
try {
$sql = "SELECT vr.*, a.nom, a.prenoms, a.photo_reference_path
FROM facial_verification_requests vr
JOIN assures a ON vr.assure_id = a.id
WHERE vr.verification_token = ?
AND vr.status = 'pending'
AND vr.expires_at > NOW()";
$stmt = $this->db->prepare($sql);
$stmt->execute([$token]);
$request = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$request) {
return [
'success' => false,
'message' => 'Lien expiré ou invalide'
];
}
return [
'success' => true,
'message' => 'Token valide',
'assure' => [
'nom' => $request['nom'],
'prenoms' => $request['prenoms']
]
];
} catch (Exception $e) {
error_log("Erreur validateToken: " . $e->getMessage());
return [
'success' => false,
'message' => 'Erreur serveur'
];
}
}
/**
* Compare deux visages avec l'API de reconnaissance faciale
* Utilisez Azure Face API, AWS Rekognition, ou une solution locale
*/
private function compareFaces($referenceImagePath, $capturedImageBase64) {
// Option 1: Azure Face API (Recommandé)
return $this->compareWithAzureFaceAPI($referenceImagePath, $capturedImageBase64);
// Option 2: AWS Rekognition
// return $this->compareWithAWSRekognition($referenceImagePath, $capturedImageBase64);
// Option 3: Solution locale avec OpenCV/dlib (avancé)
// return $this->compareWithLocalFaceRecognition($referenceImagePath, $capturedImageBase64);
}
/**
* Comparaison avec Azure Face API
*/
private function compareWithAzureFaceAPI($referenceImagePath, $capturedImageBase64) {
$endpoint = AZURE_FACE_ENDPOINT; // Ex: https://your-resource.cognitiveservices.azure.com
$apiKey = AZURE_FACE_API_KEY;
try {
// 1. Détecter le visage dans l'image de référence
$referenceImageData = base64_encode(file_get_contents($referenceImagePath));
$referenceFaceId = $this->detectFaceAzure($referenceImageData, $endpoint, $apiKey);
if (!$referenceFaceId) {
return [
'match' => false,
'confidence' => 0,
'error' => 'Aucun visage détecté dans la photo de référence'
];
}
// 2. Détecter le visage dans l'image capturée
$capturedImageData = explode(',', $capturedImageBase64)[1]; // Enlever le préfixe data:image
$capturedFaceId = $this->detectFaceAzure($capturedImageData, $endpoint, $apiKey);
if (!$capturedFaceId) {
return [
'match' => false,
'confidence' => 0,
'error' => 'Aucun visage détecté dans votre photo'
];
}
// 3. Comparer les deux visages
$verifyUrl = $endpoint . '/face/v1.0/verify';
$data = [
'faceId1' => $referenceFaceId,
'faceId2' => $capturedFaceId
];
$ch = curl_init($verifyUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Ocp-Apim-Subscription-Key: ' . $apiKey
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("Azure API error: " . $response);
}
$result = json_decode($response, true);
return [
'match' => $result['isIdentical'],
'confidence' => round($result['confidence'] * 100, 2),
'error' => null
];
} catch (Exception $e) {
error_log("Erreur Azure Face API: " . $e->getMessage());
return [
'match' => false,
'confidence' => 0,
'error' => 'Erreur lors de la vérification faciale'
];
}
}
/**
* Détecte un visage avec Azure Face API et retourne le faceId
*/
private function detectFaceAzure($imageBase64, $endpoint, $apiKey) {
$detectUrl = $endpoint . '/face/v1.0/detect?returnFaceId=true';
$ch = curl_init($detectUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, base64_decode($imageBase64));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/octet-stream',
'Ocp-Apim-Subscription-Key: ' . $apiKey
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return null;
}
$faces = json_decode($response, true);
if (empty($faces)) {
return null;
}
return $faces[0]['faceId'];
}
/**
* Comparaison avec AWS Rekognition (Alternative)
*/
private function compareWithAWSRekognition($referenceImagePath, $capturedImageBase64) {
require_once 'vendor/autoload.php'; // AWS SDK
try {
$rekognitionClient = new Aws\Rekognition\RekognitionClient([
'version' => 'latest',
'region' => AWS_REGION,
'credentials' => [
'key' => AWS_ACCESS_KEY_ID,
'secret' => AWS_SECRET_ACCESS_KEY
]
]);
$referenceImageData = file_get_contents($referenceImagePath);
$capturedImageData = base64_decode(explode(',', $capturedImageBase64)[1]);
$result = $rekognitionClient->compareFaces([
'SourceImage' => ['Bytes' => $referenceImageData],
'TargetImage' => ['Bytes' => $capturedImageData],
'SimilarityThreshold' => 80
]);
if (empty($result['FaceMatches'])) {
return [
'match' => false,
'confidence' => 0,
'error' => 'Les visages ne correspondent pas'
];
}
$similarity = $result['FaceMatches'][0]['Similarity'];
return [
'match' => $similarity >= 80,
'confidence' => round($similarity, 2),
'error' => null
];
} catch (Exception $e) {
error_log("Erreur AWS Rekognition: " . $e->getMessage());
return [
'match' => false,
'confidence' => 0,
'error' => 'Erreur lors de la vérification faciale'
];
}
}
/**
* Enregistre la photo capturée
*/
private function saveCapturedImage($assureId, $imageBase64) {
$uploadDir = 'uploads/facial_verification/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$imageData = explode(',', $imageBase64)[1];
$imageData = base64_decode($imageData);
$filename = $uploadDir . $assureId . '_' . time() . '.jpg';
file_put_contents($filename, $imageData);
return $filename;
}
/**
* Met à jour le statut de la vérification
*/
private function updateVerificationStatus($token, $status, $matchResult = null, $capturedPhotoPath = null) {
$sql = "UPDATE facial_verification_requests
SET status = ?,
verified_at = NOW(),
match_confidence = ?,
captured_photo_path = ?,
attempts = attempts + 1
WHERE verification_token = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([
$status,
$matchResult ? $matchResult['confidence'] : null,
$capturedPhotoPath,
$token
]);
}
/**
* Crée une session d'autorisation pour l'accès aux prestations
*/
private function createAuthorizationSession($assureId, $verificationRequestId) {
$sessionToken = bin2hex(random_bytes(32));
$expiresAt = date('Y-m-d H:i:s', time() + 3600); // 1 heure
$sql = "INSERT INTO prestation_authorization_sessions
(assure_id, verification_request_id, session_token, expires_at, status)
VALUES (?, ?, ?, ?, 'active')";
$stmt = $this->db->prepare($sql);
$stmt->execute([$assureId, $verificationRequestId, $sessionToken, $expiresAt]);
return $sessionToken;
}
/**
* Vérifie le visage capturé
*/
public function verifyFace($token, $capturedImageBase64) {
try {
// 1. Récupérer les infos de la demande
$sql = "SELECT vr.*, a.photo_reference_path, a.id as assure_id
FROM facial_verification_requests vr
JOIN assures a ON vr.assure_id = a.id
WHERE vr.verification_token = ?
AND vr.status = 'pending'
AND vr.expires_at > NOW()";
$stmt = $this->db->prepare($sql);
$stmt->execute([$token]);
$request = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$request) {
return [
'success' => false,
'match' => false,
'message' => 'Demande expirée ou invalide'
];
}
// 2. Vérifier le nombre de tentatives
if ($request['attempts'] >= $this->maxAttempts) {
$this->updateVerificationStatus($token, 'failed');
return [
'success' => false,
'match' => false,
'message' => 'Nombre maximum de tentatives atteint'
];
}
// 3. Enregistrer la photo capturée
$capturedPhotoPath = $this->saveCapturedImage($request['assure_id'], $capturedImageBase64);
// 4. Comparer les visages
$comparisonResult = $this->compareFaces(
$request['photo_reference_path'],
$capturedImageBase64
);
if ($comparisonResult['error']) {
$this->updateVerificationStatus($token, 'error', $comparisonResult, $capturedPhotoPath);
return [
'success' => false,
'match' => false,
'message' => $comparisonResult['error'],
'attempts_remaining' => $this->maxAttempts - ($request['attempts'] + 1)
];
}
// 5. Seuil de confiance minimum (ex: 80%)
$confidenceThreshold = 80;
$isMatch = $comparisonResult['match'] && $comparisonResult['confidence'] >= $confidenceThreshold;
if ($isMatch) {
// Succès: créer une session d'autorisation
$this->updateVerificationStatus($token, 'verified', $comparisonResult, $capturedPhotoPath);
$sessionToken = $this->createAuthorizationSession($request['assure_id'], $request['id']);
return [
'success' => true,
'match' => true,
'confidence' => $comparisonResult['confidence'],
'message' => 'Identité vérifiée avec succès',
'session_token' => $sessionToken,
'redirect_url' => 'saisie_prestations.php?token=' . $sessionToken
];
} else {
// Échec de correspondance
$attemptsRemaining = $this->maxAttempts - ($request['attempts'] + 1);
if ($attemptsRemaining > 0) {
$this->updateVerificationStatus($token, 'pending', $comparisonResult, $capturedPhotoPath);
return [
'success' => false,
'match' => false,
'confidence' => $comparisonResult['confidence'],
'message' => 'Votre visage ne correspond pas',
'attempts_remaining' => $attemptsRemaining
];
} else {
$this->updateVerificationStatus($token, 'failed', $comparisonResult, $capturedPhotoPath);
return [
'success' => false,
'match' => false,
'confidence' => $comparisonResult['confidence'],
'message' => 'Vérification échouée. Nombre maximum de tentatives atteint.',
'attempts_remaining' => 0
];
}
}
} catch (Exception $e) {
error_log("Erreur verifyFace: " . $e->getMessage());
return [
'success' => false,
'match' => false,
'message' => 'Erreur lors de la vérification: ' . $e->getMessage()
];
}
}
}
// Traiter la requête
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? null;
$db = new Database();
$api = new FacialVerificationAPI($db->getConnection());
switch ($action) {
case 'validate_token':
$token = $input['token'] ?? null;
if (!$token) {
echo json_encode(['success' => false, 'message' => 'Token requis']);
exit;
}
echo json_encode($api->validateToken($token));
break;
case 'verify_face':
$token = $input['token'] ?? null;
$image = $input['image'] ?? null;
if (!$token || !$image) {
echo json_encode(['success' => false, 'message' => 'Token et image requis']);
exit;
}
echo json_encode($api->verifyFace($token, $image));
break;
default:
echo json_encode(['success' => false, 'message' => 'Action invalide']);
}
} else {
echo json_encode(['success' => false, 'message' => 'Méthode non autorisée']);
}