feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API
- Mise à jour VERSION vers 3.3.4 - Optimisations et révisions architecture API (deploy-api.sh, scripts de migration) - Ajout documentation Stripe Tap to Pay complète - Migration vers polices Inter Variable pour Flutter - Optimisations build Android et nettoyage fichiers temporaires - Amélioration système de déploiement avec gestion backups - Ajout scripts CRON et migrations base de données 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -593,6 +593,11 @@ class EntiteController {
|
||||
$updateFields[] = 'chk_user_delete_pass = ?';
|
||||
$params[] = $data['chk_user_delete_pass'] ? 1 : 0;
|
||||
}
|
||||
|
||||
if (isset($data['chk_lot_actif'])) {
|
||||
$updateFields[] = 'chk_lot_actif = ?';
|
||||
$params[] = $data['chk_lot_actif'] ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Si aucun champ à mettre à jour, retourner une erreur
|
||||
|
||||
@@ -50,10 +50,10 @@ class LoginController {
|
||||
$username = trim($data['username']);
|
||||
$encryptedUsername = ApiService::encryptSearchableData($username);
|
||||
|
||||
// Récupérer le type d'utilisateur
|
||||
// admin accessible uniquement aux fk_role>1
|
||||
// user accessible uniquement aux fk_role=1
|
||||
$roleCondition = ($interface === 'user') ? 'AND fk_role=1' : 'AND fk_role>1';
|
||||
// Récupérer le type d'utilisateur
|
||||
// user accessible aux fk_role=1 ET fk_role=2 (membres + admins amicale)
|
||||
// admin accessible uniquement aux fk_role>1 (admins amicale + super-admins)
|
||||
$roleCondition = ($interface === 'user') ? 'AND fk_role IN (1, 2)' : 'AND fk_role>1';
|
||||
|
||||
// Log pour le debug
|
||||
LogService::log('Tentative de connexion GeoSector', [
|
||||
@@ -343,18 +343,26 @@ class LoginController {
|
||||
|
||||
// 3. Récupérer les passages selon l'interface et le rôle
|
||||
if ($interface === 'user' && !empty($sectors)) {
|
||||
// Interface utilisateur : passages liés aux secteurs de l'utilisateur
|
||||
// Interface utilisateur : passages de l'utilisateur + passages à finaliser sur ses secteurs
|
||||
$userId = $user['id'];
|
||||
$sectorIds = array_column($sectors, 'id');
|
||||
$sectorIdsString = implode(',', $sectorIds);
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = ? AND fk_sector IN ($sectorIdsString) AND chk_active = 1"
|
||||
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = ?
|
||||
AND chk_active = 1
|
||||
AND (
|
||||
(fk_user = ?) -- TOUS les passages de l'utilisateur
|
||||
OR
|
||||
(fk_sector IN ($sectorIdsString) AND fk_type = 2 AND fk_user != ?) -- Passages type 2 des autres sur ses secteurs
|
||||
)
|
||||
ORDER BY passed_at DESC"
|
||||
);
|
||||
$passagesStmt->execute([$activeOperationId]);
|
||||
$passagesStmt->execute([$activeOperationId, $userId, $userId]);
|
||||
}
|
||||
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
|
||||
// Interface admin avec rôle 2 : tous les passages de l'opération
|
||||
@@ -888,6 +896,700 @@ class LoginController {
|
||||
}
|
||||
}
|
||||
|
||||
public function refreshSession(): void {
|
||||
try {
|
||||
// 1. Récupérer l'ID utilisateur depuis la session active
|
||||
$userId = Session::getUserId();
|
||||
|
||||
if (!$userId) {
|
||||
Response::json(['error' => 'Session invalide'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Récupérer le mode depuis l'URL
|
||||
$mode = $_GET['mode'] ?? 'user';
|
||||
|
||||
// 3. Validation du mode
|
||||
if (!in_array($mode, ['user', 'admin'])) {
|
||||
Response::json(['error' => 'Mode invalide. Valeurs acceptées: user, admin'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Déterminer le roleCondition selon le mode (même logique que login)
|
||||
$roleCondition = ($mode === 'user') ? 'AND fk_role IN (1, 2)' : 'AND fk_role>1';
|
||||
|
||||
// Log pour le debug
|
||||
LogService::log('Rafraîchissement session GeoSector', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'mode' => $mode,
|
||||
'role_condition' => $roleCondition
|
||||
]);
|
||||
|
||||
// 4. Requête pour récupérer l'utilisateur et son entité (même requête que login)
|
||||
$stmt = $this->db->prepare(
|
||||
'SELECT
|
||||
u.id, u.encrypted_email, u.encrypted_user_name, u.encrypted_name, u.user_pass_hash,
|
||||
u.first_name, u.fk_role, u.fk_entite, u.fk_titre, u.chk_active, u.sect_name,
|
||||
u.date_naissance, u.date_embauche, u.encrypted_phone, u.encrypted_mobile,
|
||||
e.id AS entite_id, e.encrypted_name AS entite_encrypted_name,
|
||||
e.adresse1, e.code_postal, e.ville, e.gps_lat, e.gps_lng, e.chk_active AS entite_chk_active
|
||||
FROM users u
|
||||
LEFT JOIN entites e ON u.fk_entite = e.id
|
||||
WHERE u.id = ? AND u.chk_active != 0 ' . $roleCondition
|
||||
);
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
LogService::log('Rafraîchissement session échoué : utilisateur non trouvé ou accès interdit', [
|
||||
'level' => 'warning',
|
||||
'userId' => $userId,
|
||||
'mode' => $mode
|
||||
]);
|
||||
Response::json(['error' => 'Utilisateur non trouvé ou accès interdit à cette interface'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si l'utilisateur a une entité et si elle est active
|
||||
if (!empty($user['fk_entite']) && (!isset($user['entite_chk_active']) || $user['entite_chk_active'] != 1)) {
|
||||
LogService::log('Rafraîchissement session échoué : entité non active', [
|
||||
'level' => 'warning',
|
||||
'userId' => $userId,
|
||||
'entite_id' => $user['fk_entite']
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Votre amicale n\'est pas activée. Veuillez contacter votre administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Déchiffrement du nom
|
||||
$decryptedName = ApiService::decryptData($user['encrypted_name']);
|
||||
$username = ApiService::decryptSearchableData($user['encrypted_user_name']);
|
||||
|
||||
// Déchiffrement de l'email si disponible
|
||||
$email = '';
|
||||
if (!empty($user['encrypted_email'])) {
|
||||
$email = ApiService::decryptSearchableData($user['encrypted_email']);
|
||||
|
||||
if (empty($email)) {
|
||||
LogService::log('Déchiffrement email échoué', [
|
||||
'level' => 'error',
|
||||
'message' => 'Déchiffrement de l\'email échoué',
|
||||
'encrypted_email' => $user['encrypted_email'],
|
||||
'user_id' => $user['id']
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur de déchiffrement de l\'email. Exécutez le script de migration pour résoudre ce problème.',
|
||||
'debug_info' => [
|
||||
'encrypted_email' => $user['encrypted_email'],
|
||||
'user_id' => $user['id']
|
||||
]
|
||||
], 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Préparation des données utilisateur pour la réponse
|
||||
$userData = [
|
||||
'id' => $user['id'],
|
||||
'fk_entite' => $user['fk_entite'] ?? null,
|
||||
'fk_role' => $user['fk_role'] ?? '0',
|
||||
'fk_titre' => $user['fk_titre'] ?? null,
|
||||
'first_name' => $user['first_name'] ?? '',
|
||||
'sect_name' => $user['sect_name'] ?? '',
|
||||
'date_naissance' => $user['date_naissance'] ?? null,
|
||||
'date_embauche' => $user['date_embauche'] ?? null,
|
||||
'username' => $username,
|
||||
'name' => $decryptedName
|
||||
];
|
||||
|
||||
// Déchiffrement du téléphone
|
||||
if (!empty($user['encrypted_phone'])) {
|
||||
$userData['phone'] = ApiService::decryptData($user['encrypted_phone']);
|
||||
} else {
|
||||
$userData['phone'] = '';
|
||||
}
|
||||
|
||||
// Déchiffrement du mobile
|
||||
if (!empty($user['encrypted_mobile'])) {
|
||||
$userData['mobile'] = ApiService::decryptData($user['encrypted_mobile']);
|
||||
} else {
|
||||
$userData['mobile'] = '';
|
||||
}
|
||||
|
||||
$userData['email'] = $email;
|
||||
|
||||
// 5. Charger toutes les données selon le mode (MÊME LOGIQUE QUE LOGIN)
|
||||
$operationsData = [];
|
||||
$sectorsData = [];
|
||||
$passagesData = [];
|
||||
$usersSectorsData = [];
|
||||
|
||||
// Récupération des opérations selon les critères
|
||||
$operationLimit = 0;
|
||||
$activeOperationOnly = false;
|
||||
|
||||
if ($mode === 'user') {
|
||||
$operationLimit = 1;
|
||||
$activeOperationOnly = true;
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] == 2) {
|
||||
$operationLimit = 3;
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] > 2) {
|
||||
$operationLimit = 10;
|
||||
} else {
|
||||
$operationLimit = 0;
|
||||
}
|
||||
|
||||
if ($operationLimit > 0 && !empty($user['fk_entite'])) {
|
||||
$operationQuery = "SELECT id, fk_entite, libelle, date_deb, date_fin, chk_active
|
||||
FROM operations
|
||||
WHERE fk_entite = ?";
|
||||
|
||||
if ($activeOperationOnly) {
|
||||
$operationQuery .= " AND chk_active = 1";
|
||||
}
|
||||
|
||||
$operationQuery .= " ORDER BY id DESC LIMIT " . $operationLimit;
|
||||
|
||||
$operationStmt = $this->db->prepare($operationQuery);
|
||||
$operationStmt->execute([$user['fk_entite']]);
|
||||
$operations = $operationStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($operations)) {
|
||||
foreach ($operations as $operation) {
|
||||
$operationsData[] = [
|
||||
'id' => $operation['id'],
|
||||
'fk_entite' => $operation['fk_entite'],
|
||||
'libelle' => $operation['libelle'],
|
||||
'date_deb' => $operation['date_deb'],
|
||||
'date_fin' => $operation['date_fin'],
|
||||
'chk_active' => $operation['chk_active']
|
||||
];
|
||||
}
|
||||
|
||||
$activeOperationId = $operations[0]['id'];
|
||||
|
||||
// Récupérer les secteurs selon le mode et le rôle
|
||||
if ($mode === 'user') {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] == 2) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT DISTINCT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
WHERE s.fk_operation = ? AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId]);
|
||||
} else {
|
||||
$sectors = [];
|
||||
$sectorsData = [];
|
||||
}
|
||||
|
||||
if (isset($sectorsStmt)) {
|
||||
$sectors = $sectorsStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} else {
|
||||
$sectors = [];
|
||||
}
|
||||
|
||||
if (!empty($sectors)) {
|
||||
$sectorsData = $sectors;
|
||||
|
||||
// Récupérer les passages selon le mode et le rôle
|
||||
if ($mode === 'user' && !empty($sectors)) {
|
||||
$sectorIds = array_column($sectors, 'id');
|
||||
$sectorIdsString = implode(',', $sectorIds);
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = ?
|
||||
AND chk_active = 1
|
||||
AND (
|
||||
(fk_user = ?)
|
||||
OR
|
||||
(fk_sector IN ($sectorIdsString) AND fk_type = 2 AND fk_user != ?)
|
||||
)
|
||||
ORDER BY passed_at DESC"
|
||||
);
|
||||
$passagesStmt->execute([$activeOperationId, $user['id'], $user['id']]);
|
||||
}
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] == 2) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = ? AND chk_active = 1"
|
||||
);
|
||||
$passagesStmt->execute([$activeOperationId]);
|
||||
} else {
|
||||
$passages = [];
|
||||
$passagesData = [];
|
||||
}
|
||||
|
||||
if (isset($passagesStmt)) {
|
||||
$passages = $passagesStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} else {
|
||||
$passages = [];
|
||||
}
|
||||
|
||||
if (!empty($passages)) {
|
||||
foreach ($passages as &$passage) {
|
||||
$passage['name'] = '';
|
||||
if (!empty($passage['encrypted_name'])) {
|
||||
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
|
||||
}
|
||||
unset($passage['encrypted_name']);
|
||||
|
||||
$passage['email'] = '';
|
||||
if (!empty($passage['encrypted_email'])) {
|
||||
$decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']);
|
||||
if ($decryptedEmail) {
|
||||
$passage['email'] = $decryptedEmail;
|
||||
}
|
||||
}
|
||||
unset($passage['encrypted_email']);
|
||||
|
||||
$passage['phone'] = '';
|
||||
if (!empty($passage['encrypted_phone'])) {
|
||||
$passage['phone'] = ApiService::decryptData($passage['encrypted_phone']);
|
||||
}
|
||||
unset($passage['encrypted_phone']);
|
||||
}
|
||||
$passagesData = $passages;
|
||||
}
|
||||
|
||||
// Récupérer les utilisateurs des secteurs partagés
|
||||
if (($mode === 'user' || ($mode === 'admin' && $user['fk_role'] == 2)) && !empty($sectors)) {
|
||||
$sectorIds = array_column($sectors, 'id');
|
||||
$sectorIdsString = implode(',', $sectorIds);
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
$usersSectorsStmt = $this->db->prepare(
|
||||
"SELECT DISTINCT u.id, u.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users_sectors us ON u.id = us.fk_user
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND u.chk_active = 1
|
||||
AND u.id != ?"
|
||||
);
|
||||
$usersSectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
$usersSectors = $usersSectorsStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($usersSectors)) {
|
||||
foreach ($usersSectors as &$userSector) {
|
||||
if (!empty($userSector['encrypted_name'])) {
|
||||
$userSector['name'] = ApiService::decryptData($userSector['encrypted_name']);
|
||||
unset($userSector['encrypted_name']);
|
||||
}
|
||||
}
|
||||
$usersSectorsData = $usersSectors;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$usersSectorsData = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les membres si nécessaire
|
||||
$membresData = [];
|
||||
if ($mode === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
$membres = $membresStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($membres)) {
|
||||
foreach ($membres as $membre) {
|
||||
$membreItem = [
|
||||
'id' => $membre['id'],
|
||||
'fk_role' => $membre['fk_role'],
|
||||
'fk_entite' => $membre['fk_entite'],
|
||||
'fk_titre' => $membre['fk_titre'],
|
||||
'first_name' => $membre['first_name'] ?? '',
|
||||
'sect_name' => $membre['sect_name'] ?? '',
|
||||
'date_naissance' => $membre['date_naissance'] ?? null,
|
||||
'date_embauche' => $membre['date_embauche'] ?? null,
|
||||
'chk_active' => $membre['chk_active']
|
||||
];
|
||||
|
||||
if (!empty($membre['encrypted_name'])) {
|
||||
$membreItem['name'] = ApiService::decryptData($membre['encrypted_name']);
|
||||
} else {
|
||||
$membreItem['name'] = '';
|
||||
}
|
||||
|
||||
if (!empty($membre['encrypted_user_name'])) {
|
||||
$membreItem['username'] = ApiService::decryptSearchableData($membre['encrypted_user_name']);
|
||||
} else {
|
||||
$membreItem['username'] = '';
|
||||
}
|
||||
|
||||
if (!empty($membre['encrypted_phone'])) {
|
||||
$membreItem['phone'] = ApiService::decryptData($membre['encrypted_phone']);
|
||||
} else {
|
||||
$membreItem['phone'] = '';
|
||||
}
|
||||
|
||||
if (!empty($membre['encrypted_mobile'])) {
|
||||
$membreItem['mobile'] = ApiService::decryptData($membre['encrypted_mobile']);
|
||||
} else {
|
||||
$membreItem['mobile'] = '';
|
||||
}
|
||||
|
||||
if (!empty($membre['encrypted_email'])) {
|
||||
$decryptedEmail = ApiService::decryptSearchableData($membre['encrypted_email']);
|
||||
if ($decryptedEmail) {
|
||||
$membreItem['email'] = $decryptedEmail;
|
||||
}
|
||||
} else {
|
||||
$membreItem['email'] = '';
|
||||
}
|
||||
|
||||
$membresData[] = $membreItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les amicales selon le rôle
|
||||
$amicalesData = [];
|
||||
if (!empty($user['fk_entite'])) {
|
||||
if ($user['fk_role'] <= 2) {
|
||||
$amicaleStmt = $this->db->prepare(
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
WHERE e.id = ? AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute([$user['fk_entite']]);
|
||||
$amicales = $amicaleStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} else {
|
||||
$amicaleStmt = $this->db->prepare(
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
WHERE e.id != 1 AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute();
|
||||
$amicales = $amicaleStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!empty($amicales)) {
|
||||
foreach ($amicales as &$amicale) {
|
||||
if (!empty($amicale['name'])) {
|
||||
$amicale['name'] = ApiService::decryptData($amicale['name']);
|
||||
}
|
||||
if (!empty($amicale['email'])) {
|
||||
$decryptedEmail = ApiService::decryptSearchableData($amicale['email']);
|
||||
if ($decryptedEmail) {
|
||||
$amicale['email'] = $decryptedEmail;
|
||||
}
|
||||
}
|
||||
if (!empty($amicale['phone'])) {
|
||||
$amicale['phone'] = ApiService::decryptData($amicale['phone']);
|
||||
}
|
||||
if (!empty($amicale['mobile'])) {
|
||||
$amicale['mobile'] = ApiService::decryptData($amicale['mobile']);
|
||||
}
|
||||
if (!empty($amicale['stripe_id'])) {
|
||||
$amicale['stripe_id'] = ApiService::decryptData($amicale['stripe_id']);
|
||||
}
|
||||
}
|
||||
$amicalesData = $amicales;
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les entités de type 1 pour les utilisateurs avec fk_role > 2
|
||||
$entitesData = [];
|
||||
if ($user['fk_role'] > 2) {
|
||||
$entitesStmt = $this->db->prepare(
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
WHERE e.fk_type = 1 AND e.chk_active = 1'
|
||||
);
|
||||
$entitesStmt->execute();
|
||||
$entites = $entitesStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($entites)) {
|
||||
foreach ($entites as &$entite) {
|
||||
if (!empty($entite['name'])) {
|
||||
$entite['name'] = ApiService::decryptData($entite['name']);
|
||||
}
|
||||
if (!empty($entite['email'])) {
|
||||
$decryptedEmail = ApiService::decryptSearchableData($entite['email']);
|
||||
if ($decryptedEmail) {
|
||||
$entite['email'] = $decryptedEmail;
|
||||
}
|
||||
}
|
||||
if (!empty($entite['phone'])) {
|
||||
$entite['phone'] = ApiService::decryptData($entite['phone']);
|
||||
}
|
||||
if (!empty($entite['mobile'])) {
|
||||
$entite['mobile'] = ApiService::decryptData($entite['mobile']);
|
||||
}
|
||||
if (!empty($entite['stripe_id'])) {
|
||||
$entite['stripe_id'] = ApiService::decryptData($entite['stripe_id']);
|
||||
}
|
||||
}
|
||||
$entitesData = $entites;
|
||||
}
|
||||
}
|
||||
|
||||
// Préparation de la réponse (MÊME STRUCTURE QUE LOGIN)
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'message' => 'Session rafraîchie',
|
||||
'session_id' => session_id(),
|
||||
'session_expiry' => date('c', strtotime('+24 hours')),
|
||||
'user' => $userData
|
||||
];
|
||||
|
||||
// Ajout des amicales avec logo
|
||||
if (!empty($amicalesData)) {
|
||||
$logoData = null;
|
||||
if (!empty($user['fk_entite'])) {
|
||||
$logoStmt = $this->db->prepare('
|
||||
SELECT id, fichier, file_path, file_type, mime_type, processed_width, processed_height
|
||||
FROM medias
|
||||
WHERE support = ? AND support_id = ? AND file_category = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$logoStmt->execute(['entite', $user['fk_entite'], 'logo']);
|
||||
$logo = $logoStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($logo && file_exists($logo['file_path'])) {
|
||||
$imageData = file_get_contents($logo['file_path']);
|
||||
if ($imageData !== false) {
|
||||
$base64 = base64_encode($imageData);
|
||||
$dataUrl = 'data:' . $logo['mime_type'] . ';base64,' . $base64;
|
||||
|
||||
$logoData = [
|
||||
'id' => $logo['id'],
|
||||
'data_url' => $dataUrl,
|
||||
'file_name' => $logo['fichier'],
|
||||
'mime_type' => $logo['mime_type'],
|
||||
'width' => $logo['processed_width'],
|
||||
'height' => $logo['processed_height']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($amicalesData) === 1) {
|
||||
$response['amicale'] = $amicalesData[0];
|
||||
if ($logoData !== null) {
|
||||
$response['amicale']['logo'] = $logoData;
|
||||
}
|
||||
} else {
|
||||
$response['amicale'] = $amicalesData;
|
||||
if ($logoData !== null && !empty($user['fk_entite'])) {
|
||||
foreach ($response['amicale'] as &$amicale) {
|
||||
if ($amicale['id'] == $user['fk_entite']) {
|
||||
$amicale['logo'] = $logoData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response['clients'] = $entitesData;
|
||||
|
||||
if (!empty($membresData)) {
|
||||
$response['membres'] = $membresData;
|
||||
}
|
||||
|
||||
if (!empty($operationsData)) {
|
||||
$response['operations'] = $operationsData;
|
||||
}
|
||||
|
||||
if (!empty($sectorsData)) {
|
||||
$response['sectors'] = $sectorsData;
|
||||
}
|
||||
|
||||
if (!empty($passagesData)) {
|
||||
$response['passages'] = $passagesData;
|
||||
}
|
||||
|
||||
if (!empty($usersSectorsData)) {
|
||||
$response['users_sectors'] = $usersSectorsData;
|
||||
}
|
||||
|
||||
// Récupérer les régions selon le rôle
|
||||
$regionsData = [];
|
||||
if ($user['fk_role'] <= 2 && !empty($user['fk_entite'])) {
|
||||
$amicaleStmt = $this->db->prepare('SELECT code_postal FROM entites WHERE id = ?');
|
||||
$amicaleStmt->execute([$user['fk_entite']]);
|
||||
$amicale = $amicaleStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($amicale) && !empty($amicale['code_postal'])) {
|
||||
$departement = substr($amicale['code_postal'], 0, 2);
|
||||
|
||||
$regionStmt = $this->db->prepare(
|
||||
'SELECT id, fk_pays, libelle, libelle_long, table_osm, departements, chk_active
|
||||
FROM x_regions
|
||||
WHERE FIND_IN_SET(?, departements) > 0 AND chk_active = 1'
|
||||
);
|
||||
$regionStmt->execute([$departement]);
|
||||
$regions = $regionStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($regions)) {
|
||||
$regionsData = $regions;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$regionStmt = $this->db->prepare(
|
||||
'SELECT id, fk_pays, libelle, libelle_long, table_osm, departements, chk_active
|
||||
FROM x_regions
|
||||
WHERE chk_active = 1'
|
||||
);
|
||||
$regionStmt->execute();
|
||||
$regions = $regionStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($regions)) {
|
||||
$regionsData = $regions;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($regionsData)) {
|
||||
$response['regions'] = $regionsData;
|
||||
}
|
||||
|
||||
// Ajout des informations du module chat
|
||||
$chatData = [];
|
||||
|
||||
$roomCountStmt = $this->db->prepare('
|
||||
SELECT COUNT(DISTINCT r.id) as total_rooms
|
||||
FROM chat_rooms r
|
||||
INNER JOIN chat_participants p ON r.id = p.room_id
|
||||
WHERE p.user_id = :user_id
|
||||
AND p.left_at IS NULL
|
||||
AND r.is_active = 1
|
||||
');
|
||||
$roomCountStmt->execute(['user_id' => $user['id']]);
|
||||
$roomCount = $roomCountStmt->fetch(PDO::FETCH_ASSOC);
|
||||
$chatData['total_rooms'] = (int)($roomCount['total_rooms'] ?? 0);
|
||||
|
||||
$unreadStmt = $this->db->prepare('
|
||||
SELECT COUNT(*) as unread_count
|
||||
FROM chat_messages m
|
||||
INNER JOIN chat_participants p ON m.room_id = p.room_id
|
||||
WHERE p.user_id = :user_id
|
||||
AND p.left_at IS NULL
|
||||
AND m.sender_id != :sender_id
|
||||
AND m.sent_at > COALESCE(p.last_read_at, p.joined_at)
|
||||
AND m.is_deleted = 0
|
||||
');
|
||||
$unreadStmt->execute([
|
||||
'user_id' => $user['id'],
|
||||
'sender_id' => $user['id']
|
||||
]);
|
||||
$unreadResult = $unreadStmt->fetch(PDO::FETCH_ASSOC);
|
||||
$chatData['unread_messages'] = (int)($unreadResult['unread_count'] ?? 0);
|
||||
|
||||
$lastRoomStmt = $this->db->prepare('
|
||||
SELECT
|
||||
r.id,
|
||||
r.title,
|
||||
r.type,
|
||||
(SELECT m.content
|
||||
FROM chat_messages m
|
||||
WHERE m.room_id = r.id
|
||||
AND m.is_deleted = 0
|
||||
ORDER BY m.sent_at DESC
|
||||
LIMIT 1) as last_message,
|
||||
(SELECT m.sent_at
|
||||
FROM chat_messages m
|
||||
WHERE m.room_id = r.id
|
||||
AND m.is_deleted = 0
|
||||
ORDER BY m.sent_at DESC
|
||||
LIMIT 1) as last_message_at
|
||||
FROM chat_rooms r
|
||||
INNER JOIN chat_participants p ON r.id = p.room_id
|
||||
WHERE p.user_id = :user_id
|
||||
AND p.left_at IS NULL
|
||||
AND r.is_active = 1
|
||||
ORDER BY COALESCE(
|
||||
(SELECT MAX(m.sent_at) FROM chat_messages m WHERE m.room_id = r.id),
|
||||
r.created_at
|
||||
) DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$lastRoomStmt->execute(['user_id' => $user['id']]);
|
||||
$lastRoom = $lastRoomStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($lastRoom) {
|
||||
$chatData['last_active_room'] = [
|
||||
'id' => $lastRoom['id'],
|
||||
'title' => $lastRoom['title'],
|
||||
'type' => $lastRoom['type'],
|
||||
'last_message' => $lastRoom['last_message'],
|
||||
'last_message_at' => $lastRoom['last_message_at']
|
||||
];
|
||||
}
|
||||
|
||||
$chatData['chat_enabled'] = true;
|
||||
$response['chat'] = $chatData;
|
||||
|
||||
// 6. Envoi de la réponse
|
||||
Response::json($response);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
LogService::log('Erreur base de données lors du rafraîchissement de session', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'code' => $e->getCode()
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur inattendue lors du rafraîchissement de session', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Une erreur inattendue est survenue'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function lostPassword(): void {
|
||||
try {
|
||||
$data = Request::getJson();
|
||||
|
||||
1497
api/src/Controllers/LoginController.php.backup_with_sql_fix
Executable file
1497
api/src/Controllers/LoginController.php.backup_with_sql_fix
Executable file
File diff suppressed because it is too large
Load Diff
@@ -119,9 +119,17 @@ class PassageController {
|
||||
$errors[] = 'La ville est obligatoire';
|
||||
}
|
||||
|
||||
// Validation du nom (chiffré)
|
||||
if (!isset($data['encrypted_name']) && !isset($data['name'])) {
|
||||
$errors[] = 'Le nom est obligatoire';
|
||||
// Validation du nom (chiffré) - obligatoire seulement si (type=1 Effectué ou 5 Lot) ET email présent
|
||||
$fk_type = isset($data['fk_type']) ? (int)$data['fk_type'] : 0;
|
||||
$hasEmail = (isset($data['email']) && !empty(trim($data['email']))) ||
|
||||
(isset($data['encrypted_email']) && !empty($data['encrypted_email']));
|
||||
|
||||
if (($fk_type === 1 || $fk_type === 5) && $hasEmail) {
|
||||
if (!isset($data['encrypted_name']) && !isset($data['name'])) {
|
||||
$errors[] = 'Le nom est obligatoire pour ce type de passage avec email';
|
||||
} elseif (isset($data['name']) && empty(trim($data['name']))) {
|
||||
$errors[] = 'Le nom ne peut pas être vide pour ce type de passage avec email';
|
||||
}
|
||||
}
|
||||
|
||||
// Validation du montant
|
||||
@@ -157,6 +165,15 @@ class PassageController {
|
||||
}
|
||||
}
|
||||
|
||||
// Validation de l'ID Stripe si fourni
|
||||
if (isset($data['stripe_payment_id']) && !empty($data['stripe_payment_id'])) {
|
||||
$stripeId = trim($data['stripe_payment_id']);
|
||||
// L'ID PaymentIntent Stripe doit commencer par 'pi_'
|
||||
if (!preg_match('/^pi_[a-zA-Z0-9]{24,}$/', $stripeId)) {
|
||||
$errors[] = 'Format d\'ID de paiement Stripe invalide';
|
||||
}
|
||||
}
|
||||
|
||||
return empty($errors) ? null : $errors;
|
||||
}
|
||||
|
||||
@@ -210,13 +227,13 @@ class PassageController {
|
||||
|
||||
// Requête principale avec jointures
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT
|
||||
SELECT
|
||||
p.id, p.fk_operation, p.fk_sector, p.fk_user, p.fk_adresse,
|
||||
p.passed_at, p.fk_type, p.numero, p.rue, p.rue_bis, p.ville,
|
||||
p.fk_habitat, p.appt, p.niveau, p.residence, p.gps_lat, p.gps_lng,
|
||||
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.encrypted_email, p.encrypted_phone, p.nom_recu, p.date_recu,
|
||||
p.chk_email_sent, p.docremis, p.date_repasser, p.nb_passages,
|
||||
p.chk_email_sent, p.stripe_payment_id, p.docremis, p.date_repasser, p.nb_passages,
|
||||
p.chk_mobile, p.anomalie, p.created_at, p.updated_at, p.chk_active,
|
||||
o.libelle as operation_libelle,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
@@ -389,11 +406,11 @@ class PassageController {
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT
|
||||
SELECT
|
||||
p.id, p.fk_operation, p.fk_sector, p.fk_user, p.passed_at,
|
||||
p.numero, p.rue, p.rue_bis, p.ville, p.gps_lat, p.gps_lng,
|
||||
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.encrypted_email, p.encrypted_phone, p.chk_email_sent,
|
||||
p.encrypted_email, p.encrypted_phone, p.stripe_payment_id, p.chk_email_sent,
|
||||
p.docremis, p.date_repasser, p.nb_passages, p.chk_mobile,
|
||||
p.anomalie, p.created_at, p.updated_at,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
@@ -494,7 +511,13 @@ class PassageController {
|
||||
}
|
||||
|
||||
// Chiffrement des données sensibles
|
||||
$encryptedName = isset($data['name']) ? ApiService::encryptData($data['name']) : (isset($data['encrypted_name']) ? $data['encrypted_name'] : '');
|
||||
$encryptedName = '';
|
||||
if (isset($data['name']) && !empty(trim($data['name']))) {
|
||||
$encryptedName = ApiService::encryptData($data['name']);
|
||||
} elseif (isset($data['encrypted_name']) && !empty($data['encrypted_name'])) {
|
||||
$encryptedName = $data['encrypted_name'];
|
||||
}
|
||||
// Le nom peut rester vide si les conditions ne l'exigent pas
|
||||
$encryptedEmail = isset($data['email']) && !empty($data['email']) ?
|
||||
ApiService::encryptSearchableData($data['email']) : '';
|
||||
$encryptedPhone = isset($data['phone']) && !empty($data['phone']) ?
|
||||
@@ -524,6 +547,7 @@ class PassageController {
|
||||
'remarque' => $data['remarque'] ?? '',
|
||||
'encrypted_email' => $encryptedEmail,
|
||||
'encrypted_phone' => $encryptedPhone,
|
||||
'stripe_payment_id' => isset($data['stripe_payment_id']) ? trim($data['stripe_payment_id']) : null,
|
||||
'nom_recu' => $data['nom_recu'] ?? null,
|
||||
'date_recu' => isset($data['date_recu']) ? $data['date_recu'] : null,
|
||||
'docremis' => isset($data['docremis']) ? (int)$data['docremis'] : 0,
|
||||
@@ -646,7 +670,7 @@ class PassageController {
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.id, p.fk_operation
|
||||
SELECT p.id, p.fk_operation, p.fk_type, p.fk_user
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||
@@ -673,6 +697,19 @@ class PassageController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Si le passage était de type 2 et que l'utilisateur actuel est différent du créateur
|
||||
// On force l'attribution du passage à l'utilisateur actuel
|
||||
if ((int)$passage['fk_type'] === 2 && (int)$passage['fk_user'] !== $userId) {
|
||||
$data['fk_user'] = $userId;
|
||||
|
||||
LogService::log('Attribution automatique d\'un passage type 2 à l\'utilisateur', [
|
||||
'level' => 'info',
|
||||
'passageId' => $passageId,
|
||||
'ancien_user' => $passage['fk_user'],
|
||||
'nouveau_user' => $userId
|
||||
]);
|
||||
}
|
||||
|
||||
// Construction de la requête de mise à jour dynamique
|
||||
$updateFields = [];
|
||||
$params = [];
|
||||
@@ -697,6 +734,7 @@ class PassageController {
|
||||
'montant',
|
||||
'fk_type_reglement',
|
||||
'remarque',
|
||||
'stripe_payment_id',
|
||||
'nom_recu',
|
||||
'date_recu',
|
||||
'docremis',
|
||||
@@ -714,9 +752,10 @@ class PassageController {
|
||||
}
|
||||
|
||||
// Gestion des champs chiffrés
|
||||
if (isset($data['name'])) {
|
||||
if (array_key_exists('name', $data)) {
|
||||
$updateFields[] = "encrypted_name = ?";
|
||||
$params[] = ApiService::encryptData($data['name']);
|
||||
// Permettre de vider le nom si les conditions le permettent
|
||||
$params[] = !empty(trim($data['name'])) ? ApiService::encryptData($data['name']) : '';
|
||||
}
|
||||
|
||||
if (isset($data['email'])) {
|
||||
|
||||
@@ -544,24 +544,85 @@ class SectorController
|
||||
$stmt->execute($params);
|
||||
}
|
||||
|
||||
// Gestion des membres
|
||||
if (isset($data['membres'])) {
|
||||
// Gestion des membres (reçus comme 'users' depuis Flutter)
|
||||
if (isset($data['users'])) {
|
||||
$this->logService->info('[UPDATE USERS] Début modification des membres', [
|
||||
'sector_id' => $id,
|
||||
'users_demandes' => $data['users'],
|
||||
'nb_users' => count($data['users'])
|
||||
]);
|
||||
|
||||
// Récupérer l'opération du secteur pour l'INSERT
|
||||
$opQuery = "SELECT fk_operation FROM ope_sectors WHERE id = :sector_id";
|
||||
$this->logService->info('[UPDATE USERS] SQL - Récupération fk_operation', [
|
||||
'query' => $opQuery,
|
||||
'params' => ['sector_id' => $id]
|
||||
]);
|
||||
$opStmt = $this->db->prepare($opQuery);
|
||||
$opStmt->execute(['sector_id' => $id]);
|
||||
$operationId = $opStmt->fetch()['fk_operation'];
|
||||
$this->logService->info('[UPDATE USERS] fk_operation récupéré', [
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
|
||||
// Supprimer les affectations existantes
|
||||
$deleteQuery = "DELETE FROM ope_users_sectors WHERE fk_sector = :sector_id";
|
||||
$this->logService->info('[UPDATE USERS] SQL - Suppression des anciens membres', [
|
||||
'query' => $deleteQuery,
|
||||
'params' => ['sector_id' => $id]
|
||||
]);
|
||||
$deleteStmt = $this->db->prepare($deleteQuery);
|
||||
$deleteStmt->execute(['sector_id' => $id]);
|
||||
|
||||
$deletedCount = $deleteStmt->rowCount();
|
||||
$this->logService->info('[UPDATE USERS] Membres supprimés', [
|
||||
'nb_deleted' => $deletedCount
|
||||
]);
|
||||
|
||||
// Ajouter les nouvelles affectations
|
||||
if (!empty($data['membres'])) {
|
||||
$insertQuery = "INSERT INTO ope_users_sectors (fk_user, fk_sector) VALUES (:user_id, :sector_id)";
|
||||
if (!empty($data['users'])) {
|
||||
$insertQuery = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), :user_creat, 1)";
|
||||
$this->logService->info('[UPDATE USERS] SQL - Requête INSERT préparée', [
|
||||
'query' => $insertQuery
|
||||
]);
|
||||
$insertStmt = $this->db->prepare($insertQuery);
|
||||
|
||||
foreach ($data['membres'] as $memberId) {
|
||||
$insertStmt->execute([
|
||||
'user_id' => $memberId,
|
||||
'sector_id' => $id
|
||||
]);
|
||||
|
||||
$insertedUsers = [];
|
||||
$failedUsers = [];
|
||||
foreach ($data['users'] as $memberId) {
|
||||
try {
|
||||
$params = [
|
||||
'operation_id' => $operationId,
|
||||
'user_id' => $memberId,
|
||||
'sector_id' => $id,
|
||||
'user_creat' => $_SESSION['user_id'] ?? null
|
||||
];
|
||||
$this->logService->info('[UPDATE USERS] SQL - INSERT user', [
|
||||
'params' => $params
|
||||
]);
|
||||
$insertStmt->execute($params);
|
||||
$insertedUsers[] = $memberId;
|
||||
$this->logService->info('[UPDATE USERS] User inséré avec succès', [
|
||||
'user_id' => $memberId
|
||||
]);
|
||||
} catch (\PDOException $e) {
|
||||
$failedUsers[] = $memberId;
|
||||
$this->logService->warning('[UPDATE USERS] ERREUR insertion user', [
|
||||
'sector_id' => $id,
|
||||
'user_id' => $memberId,
|
||||
'error' => $e->getMessage(),
|
||||
'error_code' => $e->getCode()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->logService->info('[UPDATE USERS] Résultat des insertions', [
|
||||
'users_demandes' => $data['users'],
|
||||
'users_inseres' => $insertedUsers,
|
||||
'users_echoues' => $failedUsers,
|
||||
'nb_succes' => count($insertedUsers),
|
||||
'nb_echecs' => count($failedUsers)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,7 +712,8 @@ class SectorController
|
||||
$this->logService->info('[UPDATE] Début mise à jour des passages', ['sector_id' => $id]);
|
||||
$passageCounters = $this->updatePassagesForSector($id, $data['sector']);
|
||||
}
|
||||
|
||||
|
||||
// Commit des modifications (users et/ou secteur)
|
||||
$this->db->commit();
|
||||
|
||||
// Récupérer le secteur mis à jour
|
||||
@@ -711,14 +773,29 @@ class SectorController
|
||||
$passagesDecrypted[] = $passage;
|
||||
}
|
||||
|
||||
// Récupérer les users affectés
|
||||
// Récupérer les users affectés (avec READ UNCOMMITTED pour forcer la lecture des données fraîches)
|
||||
$usersQuery = "SELECT u.id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
FROM ope_users_sectors ous
|
||||
JOIN users u ON ous.fk_user = u.id
|
||||
WHERE ous.fk_sector = :sector_id";
|
||||
WHERE ous.fk_sector = :sector_id
|
||||
ORDER BY u.id";
|
||||
|
||||
$this->logService->info('[UPDATE USERS] SQL - Récupération finale des users', [
|
||||
'query' => $usersQuery,
|
||||
'params' => ['sector_id' => $id]
|
||||
]);
|
||||
|
||||
$usersStmt = $this->db->prepare($usersQuery);
|
||||
$usersStmt->execute(['sector_id' => $id]);
|
||||
$usersSectors = $usersStmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
$userIds = array_column($usersSectors, 'id');
|
||||
$this->logService->info('[UPDATE USERS] Users récupérés après commit', [
|
||||
'sector_id' => $id,
|
||||
'users_ids' => $userIds,
|
||||
'nb_users' => count($userIds),
|
||||
'users_demandes_initialement' => $data['users'] ?? []
|
||||
]);
|
||||
|
||||
// Déchiffrer les noms des utilisateurs
|
||||
$usersDecrypted = [];
|
||||
@@ -1066,6 +1143,7 @@ class SectorController
|
||||
|
||||
/**
|
||||
* Mettre à jour les passages affectés à un secteur lors de la modification du périmètre
|
||||
* VERSION OPTIMISÉE avec requêtes groupées
|
||||
* Retourne un tableau avec les compteurs détaillés
|
||||
*/
|
||||
private function updatePassagesForSector($sectorId, $newSectorCoords): array
|
||||
@@ -1080,18 +1158,18 @@ class SectorController
|
||||
|
||||
try {
|
||||
// Récupérer l'opération et l'entité du secteur
|
||||
$sectorQuery = "SELECT o.id as operation_id, o.fk_entite, s.fk_operation
|
||||
$sectorQuery = "SELECT o.id as operation_id, o.fk_entite, s.fk_operation
|
||||
FROM ope_sectors s
|
||||
JOIN operations o ON s.fk_operation = o.id
|
||||
WHERE s.id = :sector_id";
|
||||
$sectorStmt = $this->db->prepare($sectorQuery);
|
||||
$sectorStmt->execute(['sector_id' => $sectorId]);
|
||||
$sectorInfo = $sectorStmt->fetch();
|
||||
|
||||
|
||||
if (!$sectorInfo) {
|
||||
return 0;
|
||||
return $counters;
|
||||
}
|
||||
|
||||
|
||||
$operationId = $sectorInfo['operation_id'];
|
||||
$entityId = $sectorInfo['fk_entite'];
|
||||
|
||||
@@ -1099,7 +1177,7 @@ class SectorController
|
||||
$points = explode('#', rtrim($newSectorCoords, '#'));
|
||||
$coordinates = [];
|
||||
$polygonPoints = [];
|
||||
|
||||
|
||||
foreach ($points as $point) {
|
||||
if (!empty($point)) {
|
||||
list($lat, $lng) = explode('/', $point);
|
||||
@@ -1110,170 +1188,249 @@ class SectorController
|
||||
$polygonPoints[] = $polygonPoints[0]; // Fermer le polygone
|
||||
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||
|
||||
// 1. VÉRIFICATION GÉOGRAPHIQUE DES PASSAGES EXISTANTS
|
||||
$checkPassagesQuery = "SELECT id, gps_lat, gps_lng, fk_type, encrypted_name
|
||||
FROM ope_pass
|
||||
WHERE fk_sector = :sector_id
|
||||
AND gps_lat IS NOT NULL
|
||||
AND gps_lng IS NOT NULL";
|
||||
// 1. VÉRIFICATION GÉOGRAPHIQUE DES PASSAGES EXISTANTS (OPTIMISÉE)
|
||||
// Utiliser une seule requête pour vérifier tous les passages
|
||||
$checkPassagesQuery = "
|
||||
SELECT
|
||||
p.id,
|
||||
p.gps_lat,
|
||||
p.gps_lng,
|
||||
p.fk_type,
|
||||
p.encrypted_name,
|
||||
ST_Contains(ST_GeomFromText(:polygon, 4326),
|
||||
POINT(CAST(p.gps_lng AS DECIMAL(10,8)),
|
||||
CAST(p.gps_lat AS DECIMAL(10,8)))) as is_inside
|
||||
FROM ope_pass p
|
||||
WHERE p.fk_sector = :sector_id
|
||||
AND p.gps_lat IS NOT NULL
|
||||
AND p.gps_lng IS NOT NULL";
|
||||
|
||||
$checkStmt = $this->db->prepare($checkPassagesQuery);
|
||||
$checkStmt->execute(['sector_id' => $sectorId]);
|
||||
$checkStmt->execute([
|
||||
'sector_id' => $sectorId,
|
||||
'polygon' => $polygonString
|
||||
]);
|
||||
$existingPassages = $checkStmt->fetchAll();
|
||||
|
||||
|
||||
$passagesToDelete = [];
|
||||
|
||||
$passagesToOrphan = [];
|
||||
|
||||
foreach ($existingPassages as $passage) {
|
||||
// Vérifier si le passage est dans le nouveau polygone
|
||||
$pointInPolygonQuery = "SELECT ST_Contains(ST_GeomFromText(:polygon, 4326),
|
||||
POINT(CAST(:lng AS DECIMAL(10,8)),
|
||||
CAST(:lat AS DECIMAL(10,8)))) as is_inside";
|
||||
$pointStmt = $this->db->prepare($pointInPolygonQuery);
|
||||
$pointStmt->execute([
|
||||
'polygon' => $polygonString,
|
||||
'lng' => $passage['gps_lng'],
|
||||
'lat' => $passage['gps_lat']
|
||||
]);
|
||||
$result = $pointStmt->fetch();
|
||||
|
||||
if ($result['is_inside'] == 0) {
|
||||
if ($passage['is_inside'] == 0) {
|
||||
// Le passage est hors du nouveau périmètre
|
||||
// Vérifier si c'est un passage non visité (fk_type=2 ET encrypted_name vide)
|
||||
if ($passage['fk_type'] == 2 && ($passage['encrypted_name'] === '' || $passage['encrypted_name'] === null)) {
|
||||
// Passage non visité : à supprimer
|
||||
$passagesToDelete[] = $passage['id'];
|
||||
$counters['passages_deleted'] = ($counters['passages_deleted'] ?? 0) + 1;
|
||||
$counters['passages_deleted']++;
|
||||
} else {
|
||||
// Passage visité : mettre en orphelin
|
||||
$orphanQuery = "UPDATE ope_pass SET fk_sector = NULL WHERE id = :passage_id";
|
||||
$orphanStmt = $this->db->prepare($orphanQuery);
|
||||
$orphanStmt->execute(['passage_id' => $passage['id']]);
|
||||
// Passage visité : à mettre en orphelin
|
||||
$passagesToOrphan[] = $passage['id'];
|
||||
$counters['passages_orphaned']++;
|
||||
}
|
||||
} else {
|
||||
$counters['passages_kept']++;
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer les passages non visités qui sont hors zone
|
||||
|
||||
// Supprimer les passages non visités en une seule requête
|
||||
if (!empty($passagesToDelete)) {
|
||||
$deleteQuery = "DELETE FROM ope_pass WHERE id IN (" . implode(',', $passagesToDelete) . ")";
|
||||
$this->db->exec($deleteQuery);
|
||||
$placeholders = str_repeat('?,', count($passagesToDelete) - 1) . '?';
|
||||
$deleteQuery = "DELETE FROM ope_pass WHERE id IN ($placeholders)";
|
||||
$deleteStmt = $this->db->prepare($deleteQuery);
|
||||
$deleteStmt->execute($passagesToDelete);
|
||||
}
|
||||
|
||||
// Mettre en orphelin les passages visités en une seule requête
|
||||
if (!empty($passagesToOrphan)) {
|
||||
$placeholders = str_repeat('?,', count($passagesToOrphan) - 1) . '?';
|
||||
$orphanQuery = "UPDATE ope_pass SET fk_sector = NULL WHERE id IN ($placeholders)";
|
||||
$orphanStmt = $this->db->prepare($orphanQuery);
|
||||
$orphanStmt->execute($passagesToOrphan);
|
||||
}
|
||||
|
||||
// 2. CRÉATION/MISE À JOUR DES PASSAGES POUR LES NOUVELLES ADRESSES
|
||||
// 2. CRÉATION/MISE À JOUR DES PASSAGES POUR LES NOUVELLES ADRESSES (OPTIMISÉE)
|
||||
// Récupérer toutes les adresses du secteur depuis sectors_adresses
|
||||
$addressesQuery = "SELECT * FROM sectors_adresses WHERE fk_sector = :sector_id";
|
||||
$addressesStmt = $this->db->prepare($addressesQuery);
|
||||
$addressesStmt->execute(['sector_id' => $sectorId]);
|
||||
$addresses = $addressesStmt->fetchAll();
|
||||
|
||||
|
||||
$this->logService->info('[updatePassagesForSector] Adresses dans sectors_adresses', [
|
||||
'sector_id' => $sectorId,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
|
||||
|
||||
// Récupérer le premier utilisateur affecté au secteur
|
||||
$userQuery = "SELECT fk_user FROM ope_users_sectors WHERE fk_sector = :sector_id LIMIT 1";
|
||||
$userStmt = $this->db->prepare($userQuery);
|
||||
$userStmt->execute(['sector_id' => $sectorId]);
|
||||
$firstUser = $userStmt->fetch();
|
||||
$firstUserId = $firstUser ? $firstUser['fk_user'] : null;
|
||||
|
||||
|
||||
if ($firstUserId && !empty($addresses)) {
|
||||
$this->logService->info('[updatePassagesForSector] Création passages pour user', [
|
||||
$this->logService->info('[updatePassagesForSector] Optimisation passages', [
|
||||
'user_id' => $firstUserId,
|
||||
'nb_addresses_to_process' => count($addresses)
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
// Préparer la requête de création de passage (même format que dans create)
|
||||
$createPassageQuery = "INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse,
|
||||
numero, rue, rue_bis, ville,
|
||||
gps_lat, gps_lng, fk_type, encrypted_name,
|
||||
created_at, fk_user_creat, chk_active
|
||||
) VALUES (
|
||||
:operation_id, :sector_id, :user_id, :fk_adresse,
|
||||
:numero, :rue, :rue_bis, :ville,
|
||||
:gps_lat, :gps_lng, 2, '',
|
||||
NOW(), :user_creat, 1
|
||||
)";
|
||||
$createStmt = $this->db->prepare($createPassageQuery);
|
||||
|
||||
|
||||
// OPTIMISATION : Récupérer TOUS les passages existants en UNE requête
|
||||
$addressIds = array_filter(array_column($addresses, 'fk_adresse'));
|
||||
|
||||
// Construire la requête pour récupérer tous les passages existants
|
||||
$existingQuery = "
|
||||
SELECT id, fk_adresse, numero, rue, rue_bis, ville
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = :operation_id
|
||||
AND (";
|
||||
|
||||
$params = ['operation_id' => $operationId];
|
||||
$conditions = [];
|
||||
|
||||
// Condition pour les fk_adresse
|
||||
if (!empty($addressIds)) {
|
||||
$placeholders = [];
|
||||
foreach ($addressIds as $idx => $addrId) {
|
||||
$key = 'addr_' . $idx;
|
||||
$placeholders[] = ':' . $key;
|
||||
$params[$key] = $addrId;
|
||||
}
|
||||
$conditions[] = "fk_adresse IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
// Condition pour les données d'adresse (numero, rue, ville)
|
||||
$addressConditions = [];
|
||||
foreach ($addresses as $idx => $addr) {
|
||||
$numKey = 'num_' . $idx;
|
||||
$rueKey = 'rue_' . $idx;
|
||||
$bisKey = 'bis_' . $idx;
|
||||
$villeKey = 'ville_' . $idx;
|
||||
|
||||
$addressConditions[] = "(numero = :$numKey AND rue = :$rueKey AND rue_bis = :$bisKey AND ville = :$villeKey)";
|
||||
$params[$numKey] = $addr['numero'];
|
||||
$params[$rueKey] = $addr['rue'];
|
||||
$params[$bisKey] = $addr['rue_bis'];
|
||||
$params[$villeKey] = $addr['ville'];
|
||||
}
|
||||
|
||||
if (!empty($addressConditions)) {
|
||||
$conditions[] = "(" . implode(' OR ', $addressConditions) . ")";
|
||||
}
|
||||
|
||||
$existingQuery .= implode(' OR ', $conditions) . ")";
|
||||
|
||||
$existingStmt = $this->db->prepare($existingQuery);
|
||||
$existingStmt->execute($params);
|
||||
$existingPassages = $existingStmt->fetchAll();
|
||||
|
||||
// Indexer les passages existants pour recherche rapide
|
||||
$passagesByAddress = [];
|
||||
$passagesByData = [];
|
||||
foreach ($existingPassages as $p) {
|
||||
if (!empty($p['fk_adresse'])) {
|
||||
$passagesByAddress[$p['fk_adresse']] = $p;
|
||||
}
|
||||
$dataKey = $p['numero'] . '|' . $p['rue'] . '|' . $p['rue_bis'] . '|' . $p['ville'];
|
||||
$passagesByData[$dataKey] = $p;
|
||||
}
|
||||
|
||||
// Préparer les listes pour batch insert/update
|
||||
$toInsert = [];
|
||||
$toUpdate = [];
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
// 2.1 Vérification primaire par fk_adresse
|
||||
if (!empty($address['fk_adresse'])) {
|
||||
$checkByAddressQuery = "SELECT id FROM ope_pass
|
||||
WHERE fk_operation = :operation_id
|
||||
AND fk_adresse = :fk_adresse";
|
||||
$checkByAddressStmt = $this->db->prepare($checkByAddressQuery);
|
||||
$checkByAddressStmt->execute([
|
||||
'operation_id' => $operationId,
|
||||
'fk_adresse' => $address['fk_adresse']
|
||||
]);
|
||||
|
||||
if ($checkByAddressStmt->fetch()) {
|
||||
continue; // Passage déjà existant, passer au suivant
|
||||
}
|
||||
// Vérification en mémoire PHP (0 requête)
|
||||
if (!empty($address['fk_adresse']) && isset($passagesByAddress[$address['fk_adresse']])) {
|
||||
continue; // Déjà existant avec bon fk_adresse
|
||||
}
|
||||
|
||||
// 2.2 Vérification secondaire par données d'adresse
|
||||
$checkByDataQuery = "SELECT id FROM ope_pass
|
||||
WHERE fk_operation = :operation_id
|
||||
AND numero = :numero
|
||||
AND rue_bis = :rue_bis
|
||||
AND rue = :rue
|
||||
AND ville = :ville";
|
||||
$checkByDataStmt = $this->db->prepare($checkByDataQuery);
|
||||
$checkByDataStmt->execute([
|
||||
'operation_id' => $operationId,
|
||||
'numero' => $address['numero'],
|
||||
'rue_bis' => $address['rue_bis'],
|
||||
'rue' => $address['rue'],
|
||||
'ville' => $address['ville']
|
||||
]);
|
||||
|
||||
$matchingPassages = $checkByDataStmt->fetchAll();
|
||||
|
||||
if (!empty($matchingPassages)) {
|
||||
// Mettre à jour les passages trouvés avec le fk_adresse
|
||||
if (!empty($address['fk_adresse'])) {
|
||||
$updateQuery = "UPDATE ope_pass SET fk_adresse = :fk_adresse WHERE id = :passage_id";
|
||||
$updateStmt = $this->db->prepare($updateQuery);
|
||||
|
||||
foreach ($matchingPassages as $matchingPassage) {
|
||||
$updateStmt->execute([
|
||||
'fk_adresse' => $address['fk_adresse'],
|
||||
'passage_id' => $matchingPassage['id']
|
||||
]);
|
||||
$counters['passages_updated']++;
|
||||
}
|
||||
|
||||
$dataKey = $address['numero'] . '|' . $address['rue'] . '|' . $address['rue_bis'] . '|' . $address['ville'];
|
||||
if (isset($passagesByData[$dataKey])) {
|
||||
// Passage existant mais sans fk_adresse ou avec fk_adresse différent
|
||||
if (!empty($address['fk_adresse']) && $passagesByData[$dataKey]['fk_adresse'] != $address['fk_adresse']) {
|
||||
$toUpdate[] = [
|
||||
'id' => $passagesByData[$dataKey]['id'],
|
||||
'fk_adresse' => $address['fk_adresse']
|
||||
];
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
// Nouveau passage à créer
|
||||
$toInsert[] = $address;
|
||||
}
|
||||
|
||||
// 2.3 Création du passage (aucun passage existant trouvé)
|
||||
}
|
||||
|
||||
// INSERT MULTIPLE en une seule requête
|
||||
if (!empty($toInsert)) {
|
||||
$values = [];
|
||||
$insertParams = [];
|
||||
$paramIndex = 0;
|
||||
|
||||
foreach ($toInsert as $addr) {
|
||||
$values[] = "(:op$paramIndex, :sect$paramIndex, :usr$paramIndex, :addr$paramIndex,
|
||||
:num$paramIndex, :rue$paramIndex, :bis$paramIndex, :ville$paramIndex,
|
||||
:lat$paramIndex, :lng$paramIndex, 2, '', NOW(), :creat$paramIndex, 1)";
|
||||
|
||||
$insertParams["op$paramIndex"] = $operationId;
|
||||
$insertParams["sect$paramIndex"] = $sectorId;
|
||||
$insertParams["usr$paramIndex"] = $firstUserId;
|
||||
$insertParams["addr$paramIndex"] = $addr['fk_adresse'];
|
||||
$insertParams["num$paramIndex"] = $addr['numero'];
|
||||
$insertParams["rue$paramIndex"] = $addr['rue'];
|
||||
$insertParams["bis$paramIndex"] = $addr['rue_bis'];
|
||||
$insertParams["ville$paramIndex"] = $addr['ville'];
|
||||
$insertParams["lat$paramIndex"] = $addr['gps_lat'];
|
||||
$insertParams["lng$paramIndex"] = $addr['gps_lng'];
|
||||
$insertParams["creat$paramIndex"] = $_SESSION['user_id'] ?? null;
|
||||
|
||||
$paramIndex++;
|
||||
}
|
||||
|
||||
$insertQuery = "INSERT INTO ope_pass
|
||||
(fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis,
|
||||
ville, gps_lat, gps_lng, fk_type, encrypted_name, created_at, fk_user_creat, chk_active)
|
||||
VALUES " . implode(',', $values);
|
||||
|
||||
try {
|
||||
$createStmt->execute([
|
||||
'operation_id' => $operationId,
|
||||
'sector_id' => $sectorId,
|
||||
'user_id' => $firstUserId,
|
||||
'fk_adresse' => $address['fk_adresse'],
|
||||
'numero' => $address['numero'],
|
||||
'rue' => $address['rue'],
|
||||
'rue_bis' => $address['rue_bis'],
|
||||
'ville' => $address['ville'],
|
||||
'gps_lat' => $address['gps_lat'],
|
||||
'gps_lng' => $address['gps_lng'],
|
||||
'user_creat' => $_SESSION['user_id'] ?? null
|
||||
]);
|
||||
$counters['passages_created']++;
|
||||
$insertStmt = $this->db->prepare($insertQuery);
|
||||
$insertStmt->execute($insertParams);
|
||||
$counters['passages_created'] = count($toInsert);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Erreur lors de la création d\'un passage pendant update', [
|
||||
$this->logService->error('Erreur lors de l\'insertion multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
'address' => $address,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// UPDATE MULTIPLE avec CASE WHEN
|
||||
if (!empty($toUpdate)) {
|
||||
$updateIds = array_column($toUpdate, 'id');
|
||||
$placeholders = str_repeat('?,', count($updateIds) - 1) . '?';
|
||||
|
||||
$caseWhen = [];
|
||||
$updateParams = [];
|
||||
|
||||
foreach ($toUpdate as $upd) {
|
||||
$caseWhen[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['fk_adresse'];
|
||||
}
|
||||
|
||||
$updateQuery = "UPDATE ope_pass
|
||||
SET fk_adresse = CASE " . implode(' ', $caseWhen) . " END
|
||||
WHERE id IN ($placeholders)";
|
||||
|
||||
try {
|
||||
$updateStmt = $this->db->prepare($updateQuery);
|
||||
$updateStmt->execute(array_merge($updateParams, $updateIds));
|
||||
$counters['passages_updated'] = count($toUpdate);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la mise à jour multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->logService->warning('[updatePassagesForSector] Pas de création de passages', [
|
||||
'reason' => !$firstUserId ? 'Pas d\'utilisateur affecté' : 'Pas d\'adresses',
|
||||
|
||||
@@ -137,111 +137,143 @@ class StripeController extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/locations
|
||||
* Créer une Location pour Terminal/Tap to Pay
|
||||
*/
|
||||
public function createLocation(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
// Vérifier le rôle de l'utilisateur
|
||||
$userId = Session::getUserId();
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$result = $stmt->fetch();
|
||||
$userRole = $result ? (int)$result['fk_role'] : 0;
|
||||
|
||||
if ($userRole < 2) {
|
||||
$this->sendError('Droits insuffisants', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
$entiteId = $data['fk_entite'] ?? Session::getEntityId();
|
||||
|
||||
$result = $this->stripeService->createLocation($entiteId);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess($result);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/terminal/connection-token
|
||||
* Créer un token de connexion pour Terminal/Tap to Pay
|
||||
*/
|
||||
public function createConnectionToken(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
$entiteId = Session::getEntityId();
|
||||
if (!$entiteId) {
|
||||
$this->sendError('Entité non définie', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->stripeService->createConnectionToken($entiteId);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess(['secret' => $result['secret']]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/payments/create-intent
|
||||
* Créer une intention de paiement
|
||||
* Créer une intention de paiement pour Tap to Pay ou paiement Web
|
||||
*
|
||||
* Payload Tap to Pay:
|
||||
* {
|
||||
* "amount": 2500,
|
||||
* "currency": "eur",
|
||||
* "description": "Calendrier pompiers - Passage #789",
|
||||
* "payment_method_types": ["card_present"],
|
||||
* "capture_method": "automatic",
|
||||
* "passage_id": 789,
|
||||
* "amicale_id": 42,
|
||||
* "member_id": 156,
|
||||
* "stripe_account": "acct_1O3ABC456DEF789",
|
||||
* "location_id": "tml_FGH123456789",
|
||||
* "metadata": {...}
|
||||
* }
|
||||
*/
|
||||
public function createPaymentIntent(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
// Validation
|
||||
$amount = $data['amount'] ?? 0;
|
||||
|
||||
// Validation des champs requis
|
||||
if (!isset($data['amount']) || !isset($data['passage_id'])) {
|
||||
$this->sendError('Montant et passage_id requis', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$amount = (int)$data['amount'];
|
||||
$passageId = (int)$data['passage_id'];
|
||||
|
||||
// Validation du passage_id (doit être > 0 car le passage est créé avant)
|
||||
if ($passageId <= 0) {
|
||||
$this->sendError('passage_id invalide. Le passage doit être créé avant le paiement', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation du montant
|
||||
if ($amount < 100) {
|
||||
$this->sendError('Le montant minimum est de 1€ (100 centimes)', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($amount > 50000) {
|
||||
$this->sendError('Le montant maximum est de 500€', 400);
|
||||
|
||||
if ($amount > 99900) { // 999€ max selon la doc
|
||||
$this->sendError('Le montant maximum est de 999€', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Vérifier que le passage existe et appartient à l'utilisateur
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.*, o.fk_entite
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND p.fk_user = ?
|
||||
');
|
||||
$stmt->execute([$passageId, Session::getUserId()]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if (!$passage) {
|
||||
$this->sendError('Passage non trouvé ou non autorisé', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier qu'il n'y a pas déjà un paiement Stripe pour ce passage
|
||||
if (!empty($passage['stripe_payment_id'])) {
|
||||
$this->sendError('Un paiement Stripe existe déjà pour ce passage', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le montant correspond (passage.montant est en euros, amount en centimes)
|
||||
$expectedAmount = (int)($passage['montant'] * 100);
|
||||
if ($amount !== $expectedAmount) {
|
||||
$this->sendError("Le montant ne correspond pas au passage (attendu: {$expectedAmount} centimes, reçu: {$amount} centimes)", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entiteId = $passage['fk_entite'];
|
||||
|
||||
// Déterminer le type de paiement (Tap to Pay ou Web)
|
||||
$paymentMethodTypes = $data['payment_method_types'] ?? ['card_present'];
|
||||
$isTapToPay = in_array('card_present', $paymentMethodTypes);
|
||||
|
||||
// Préparer les paramètres pour StripeService
|
||||
$params = [
|
||||
'amount' => $amount,
|
||||
'fk_entite' => $data['fk_entite'] ?? Session::getEntityId(),
|
||||
'fk_user' => Session::getUserId(),
|
||||
'metadata' => $data['metadata'] ?? []
|
||||
'currency' => $data['currency'] ?? 'eur',
|
||||
'description' => $data['description'] ?? "Calendrier pompiers - Passage #$passageId",
|
||||
'payment_method_types' => $paymentMethodTypes,
|
||||
'capture_method' => $data['capture_method'] ?? 'automatic',
|
||||
'passage_id' => $passageId,
|
||||
'amicale_id' => $data['amicale_id'] ?? $entiteId,
|
||||
'member_id' => $data['member_id'] ?? Session::getUserId(),
|
||||
'stripe_account' => $data['stripe_account'] ?? null,
|
||||
'metadata' => array_merge(
|
||||
[
|
||||
'passage_id' => (string)$passageId,
|
||||
'amicale_id' => (string)($data['amicale_id'] ?? $entiteId),
|
||||
'member_id' => (string)($data['member_id'] ?? Session::getUserId()),
|
||||
'type' => $isTapToPay ? 'tap_to_pay' : 'web'
|
||||
],
|
||||
$data['metadata'] ?? []
|
||||
)
|
||||
];
|
||||
|
||||
|
||||
// Ajouter location_id si fourni (pour Tap to Pay)
|
||||
if (isset($data['location_id'])) {
|
||||
$params['location_id'] = $data['location_id'];
|
||||
}
|
||||
|
||||
// Créer le PaymentIntent via StripeService
|
||||
$result = $this->stripeService->createPaymentIntent($params);
|
||||
|
||||
|
||||
if ($result['success']) {
|
||||
// Mettre à jour le passage avec le stripe_payment_id
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET stripe_payment_id = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$result['payment_intent_id'], $passageId]);
|
||||
|
||||
// Retourner la réponse
|
||||
$this->sendSuccess([
|
||||
'client_secret' => $result['client_secret'],
|
||||
'payment_intent_id' => $result['payment_intent_id'],
|
||||
'amount' => $result['amount'],
|
||||
'application_fee' => $result['application_fee']
|
||||
'currency' => $params['currency'],
|
||||
'passage_id' => $passageId,
|
||||
'type' => $isTapToPay ? 'tap_to_pay' : 'web'
|
||||
]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
@@ -249,60 +281,78 @@ class StripeController extends Controller {
|
||||
|
||||
/**
|
||||
* GET /api/stripe/payments/{paymentIntentId}
|
||||
* Récupérer le statut d'un paiement
|
||||
* Récupérer le statut d'un paiement depuis ope_pass et Stripe
|
||||
*/
|
||||
public function getPaymentStatus(string $paymentIntentId): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT spi.*, e.nom as entite_nom, u.nom as user_nom, u.prenom as user_prenom
|
||||
FROM stripe_payment_intents spi
|
||||
LEFT JOIN entites e ON spi.fk_entite = e.id
|
||||
LEFT JOIN users u ON spi.fk_user = u.id
|
||||
WHERE spi.stripe_payment_intent_id = :pi_id"
|
||||
);
|
||||
|
||||
// Récupérer les informations depuis ope_pass
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT p.*, o.fk_entite,
|
||||
e.encrypted_name as entite_nom,
|
||||
u.first_name as user_prenom, u.sect_name as user_nom
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
LEFT JOIN entites e ON o.fk_entite = e.id
|
||||
LEFT JOIN users u ON p.fk_user = u.id
|
||||
WHERE p.stripe_payment_id = :pi_id
|
||||
");
|
||||
$stmt->execute(['pi_id' => $paymentIntentId]);
|
||||
$payment = $stmt->fetch();
|
||||
|
||||
if (!$payment) {
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if (!$passage) {
|
||||
$this->sendError('Paiement non trouvé', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Vérifier les droits
|
||||
$userEntityId = Session::getEntityId();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
|
||||
// Récupérer le rôle depuis la base de données
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$result = $stmt->fetch();
|
||||
$userRole = $result ? (int)$result['fk_role'] : 0;
|
||||
|
||||
if ($payment['fk_entite'] != $userEntityId &&
|
||||
$payment['fk_user'] != $userId &&
|
||||
|
||||
if ($passage['fk_entite'] != $userEntityId &&
|
||||
$passage['fk_user'] != $userId &&
|
||||
$userRole < 3) {
|
||||
$this->sendError('Non autorisé', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Récupérer le statut en temps réel depuis Stripe
|
||||
$stripeStatus = $this->stripeService->getPaymentIntentStatus($paymentIntentId);
|
||||
|
||||
// Déchiffrer le nom de l'entité si nécessaire
|
||||
$entiteNom = '';
|
||||
if (!empty($passage['entite_nom'])) {
|
||||
try {
|
||||
$entiteNom = \ApiService::decryptData($passage['entite_nom']);
|
||||
} catch (Exception $e) {
|
||||
$entiteNom = 'Entité inconnue';
|
||||
}
|
||||
}
|
||||
|
||||
$this->sendSuccess([
|
||||
'payment_intent_id' => $payment['stripe_payment_intent_id'],
|
||||
'status' => $payment['status'],
|
||||
'amount' => $payment['amount'],
|
||||
'currency' => $payment['currency'],
|
||||
'application_fee' => $payment['application_fee'],
|
||||
'payment_intent_id' => $paymentIntentId,
|
||||
'passage_id' => $passage['id'],
|
||||
'status' => $stripeStatus['status'] ?? 'unknown',
|
||||
'amount' => (int)($passage['montant'] * 100), // montant en BDD est en euros, on convertit en centimes
|
||||
'currency' => 'eur',
|
||||
'entite' => [
|
||||
'id' => $payment['fk_entite'],
|
||||
'nom' => $payment['entite_nom']
|
||||
'id' => $passage['fk_entite'],
|
||||
'nom' => $entiteNom
|
||||
],
|
||||
'user' => [
|
||||
'id' => $payment['fk_user'],
|
||||
'nom' => $payment['user_nom'],
|
||||
'prenom' => $payment['user_prenom']
|
||||
'id' => $passage['fk_user'],
|
||||
'nom' => $passage['user_nom'],
|
||||
'prenom' => $passage['user_prenom']
|
||||
],
|
||||
'created_at' => $payment['created_at']
|
||||
'created_at' => $passage['date_creat'],
|
||||
'stripe_details' => $stripeStatus
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
@@ -419,10 +469,11 @@ class StripeController extends Controller {
|
||||
$platform = $data['platform'] ?? '';
|
||||
|
||||
if ($platform === 'ios') {
|
||||
// Pour iOS, on vérifie côté client (iPhone XS+ avec iOS 15.4+)
|
||||
// Pour iOS, on vérifie côté client (iPhone XS+ avec iOS 16.4+)
|
||||
$this->sendSuccess([
|
||||
'message' => 'Vérification iOS à faire côté client',
|
||||
'requirements' => 'iPhone XS ou plus récent avec iOS 15.4+'
|
||||
'requirements' => 'iPhone XS ou plus récent avec iOS 16.4+',
|
||||
'details' => 'iOS 16.4 minimum requis pour le support PIN complet'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -225,36 +225,32 @@ class UserController {
|
||||
'has_password' => isset($data['password'])
|
||||
]);
|
||||
|
||||
// Validation des données requises
|
||||
if (!isset($data['email']) || empty(trim($data['email']))) {
|
||||
LogService::log('Erreur création utilisateur : Email manquant', [
|
||||
'level' => 'warning',
|
||||
'createdBy' => $currentUserId
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Email requis',
|
||||
'field' => 'email'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($data['name']) || empty(trim($data['name']))) {
|
||||
LogService::log('Erreur création utilisateur : Nom manquant', [
|
||||
// Validation : au moins name OU sect_name requis
|
||||
if ((!isset($data['name']) || empty(trim($data['name']))) &&
|
||||
(!isset($data['sect_name']) || empty(trim($data['sect_name'])))) {
|
||||
LogService::log('Erreur création utilisateur : Aucun nom fourni', [
|
||||
'level' => 'warning',
|
||||
'createdBy' => $currentUserId,
|
||||
'email' => $data['email'] ?? 'non fourni'
|
||||
'email' => $data['email'] ?? 'non fourni',
|
||||
'has_name' => isset($data['name']),
|
||||
'has_sect_name' => isset($data['sect_name'])
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Nom requis',
|
||||
'field' => 'name'
|
||||
'message' => 'Au moins un nom (name ou sect_name) est requis',
|
||||
'field' => 'name_or_sect_name'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$email = trim(strtolower($data['email']));
|
||||
$name = trim($data['name']);
|
||||
// L'email est maintenant optionnel
|
||||
$email = isset($data['email']) && !empty(trim($data['email']))
|
||||
? trim(strtolower($data['email']))
|
||||
: '';
|
||||
// Le name peut être vide si sect_name est fourni
|
||||
$name = isset($data['name']) && !empty(trim($data['name']))
|
||||
? trim($data['name'])
|
||||
: '';
|
||||
$firstName = isset($data['first_name']) ? trim($data['first_name']) : '';
|
||||
$role = isset($data['role']) ? (int)$data['role'] : 1;
|
||||
$entiteId = isset($data['fk_entite']) ? (int)$data['fk_entite'] : 1;
|
||||
@@ -288,8 +284,8 @@ class UserController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation de l'email
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
// Validation de l'email seulement s'il est fourni
|
||||
if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
LogService::log('Erreur création utilisateur : Format email invalide', [
|
||||
'level' => 'warning',
|
||||
'createdBy' => $currentUserId,
|
||||
@@ -305,8 +301,10 @@ class UserController {
|
||||
}
|
||||
|
||||
// Chiffrement des données sensibles
|
||||
$encryptedEmail = ApiService::encryptSearchableData($email);
|
||||
$encryptedName = ApiService::encryptData($name);
|
||||
// Chiffrer l'email seulement s'il n'est pas vide
|
||||
$encryptedEmail = !empty($email) ? ApiService::encryptSearchableData($email) : null;
|
||||
// Chiffrer le name seulement s'il n'est pas vide
|
||||
$encryptedName = !empty($name) ? ApiService::encryptData($name) : null;
|
||||
|
||||
// Vérification de l'existence de l'email
|
||||
// DÉSACTIVÉ : Le client souhaite permettre plusieurs comptes avec le même email
|
||||
@@ -503,36 +501,43 @@ class UserController {
|
||||
]);
|
||||
$userId = $this->db->lastInsertId();
|
||||
|
||||
// Envoi des emails séparés pour plus de sécurité
|
||||
|
||||
// 1er email : TOUJOURS envoyer l'identifiant (username)
|
||||
$usernameEmailData = [
|
||||
'email' => $email,
|
||||
'username' => $username,
|
||||
'name' => $name
|
||||
];
|
||||
ApiService::sendEmail($email, $name, 'welcome_username', $usernameEmailData);
|
||||
|
||||
// 2ème email : Envoyer le mot de passe (toujours, qu'il soit manuel ou généré)
|
||||
// Attendre un peu entre les deux emails pour éviter qu'ils arrivent dans le mauvais ordre
|
||||
sleep(1);
|
||||
|
||||
$passwordEmailData = [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => $name
|
||||
];
|
||||
ApiService::sendEmail($email, $name, 'welcome_password', $passwordEmailData);
|
||||
// Envoi des emails séparés pour plus de sécurité (seulement si un email est fourni)
|
||||
if (!empty($email)) {
|
||||
// 1er email : Envoyer l'identifiant (username)
|
||||
$usernameEmailData = [
|
||||
'email' => $email,
|
||||
'username' => $username,
|
||||
'name' => $name
|
||||
];
|
||||
ApiService::sendEmail($email, $name, 'welcome_username', $usernameEmailData);
|
||||
|
||||
// 2ème email : Envoyer le mot de passe (toujours, qu'il soit manuel ou généré)
|
||||
// Attendre un peu entre les deux emails pour éviter qu'ils arrivent dans le mauvais ordre
|
||||
sleep(1);
|
||||
|
||||
$passwordEmailData = [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => $name
|
||||
];
|
||||
ApiService::sendEmail($email, $name, 'welcome_password', $passwordEmailData);
|
||||
} else {
|
||||
LogService::log('Utilisateur créé sans email - pas d\'envoi de credentials', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'username' => $username
|
||||
]);
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector créé', [
|
||||
'level' => 'info',
|
||||
'createdBy' => $currentUserId,
|
||||
'newUserId' => $userId,
|
||||
'email' => $email,
|
||||
'email' => !empty($email) ? $email : 'non fourni',
|
||||
'username' => $username,
|
||||
'usernameManual' => $chkUsernameManuel === 1 ? 'oui' : 'non',
|
||||
'passwordManual' => $chkMdpManuel === 1 ? 'oui' : 'non',
|
||||
'emailsSent' => '2 emails (username + password)'
|
||||
'emailsSent' => !empty($email) ? '2 emails (username + password)' : 'aucun (pas d\'email)'
|
||||
]);
|
||||
|
||||
// Préparer la réponse avec les informations de connexion si générées automatiquement
|
||||
@@ -639,6 +644,58 @@ class UserController {
|
||||
$params['encrypted_mobile'] = ApiService::encryptData(trim($data['mobile']));
|
||||
}
|
||||
|
||||
// Gestion de la modification du username
|
||||
if (isset($data['username'])) {
|
||||
$username = trim($data['username']);
|
||||
|
||||
// Validation de la longueur en caractères UTF-8
|
||||
$usernameLength = mb_strlen($username, 'UTF-8');
|
||||
|
||||
if ($usernameLength < 8) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Identifiant trop court',
|
||||
'field' => 'username',
|
||||
'details' => 'Minimum 8 caractères'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($usernameLength > 30) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Identifiant trop long',
|
||||
'field' => 'username',
|
||||
'details' => 'Maximum 30 caractères'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$encryptedUsername = ApiService::encryptSearchableData($username);
|
||||
|
||||
// Vérifier l'unicité du username (sauf pour l'utilisateur courant)
|
||||
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_user_name = ? AND id != ?');
|
||||
$checkStmt->execute([$encryptedUsername, $id]);
|
||||
if ($checkStmt->fetch()) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Cet identifiant est déjà utilisé par un autre utilisateur',
|
||||
'field' => 'username'
|
||||
], 409);
|
||||
return;
|
||||
}
|
||||
|
||||
$updateFields[] = "encrypted_user_name = :encrypted_user_name";
|
||||
$params['encrypted_user_name'] = $encryptedUsername;
|
||||
|
||||
LogService::log('Modification du username', [
|
||||
'level' => 'info',
|
||||
'userId' => $id,
|
||||
'newUsername' => $username,
|
||||
'modifiedBy' => $currentUserId
|
||||
]);
|
||||
}
|
||||
|
||||
// Traitement des champs non chiffrés
|
||||
$nonEncryptedFields = [
|
||||
'first_name',
|
||||
@@ -1142,6 +1199,209 @@ class UserController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre les informations du device de l'utilisateur
|
||||
* POST /api/users/device-info
|
||||
*/
|
||||
public function saveDeviceInfo(): void {
|
||||
Session::requireAuth();
|
||||
|
||||
$userId = Session::getUserId();
|
||||
if (!$userId) {
|
||||
LogService::log('Device info error: Invalid session', [
|
||||
'level' => 'error',
|
||||
'session_id' => session_id()
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Session invalide'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupération des données du payload
|
||||
$data = Request::getJson();
|
||||
|
||||
// Validation des données requises
|
||||
if (empty($data['platform'])) {
|
||||
LogService::log('Device info error: Missing platform', [
|
||||
'level' => 'error',
|
||||
'user_id' => $userId,
|
||||
'data' => $data
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Platform requis',
|
||||
'field' => 'platform'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation des IPs (IPv4 uniquement)
|
||||
// Pour la plateforme Web, device_ip_local contient "Web Platform" → on le traite comme NULL
|
||||
$deviceIpLocal = $data['device_ip_local'] ?? null;
|
||||
$deviceIpPublic = $data['device_ip_public'] ?? null;
|
||||
|
||||
if ($deviceIpLocal && !filter_var($deviceIpLocal, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
LogService::log('Device IP local invalide, ignorée', [
|
||||
'level' => 'debug',
|
||||
'user_id' => $userId,
|
||||
'device_ip_local' => $deviceIpLocal
|
||||
]);
|
||||
$deviceIpLocal = null; // Ignorer si ce n'est pas une IPv4 valide
|
||||
}
|
||||
|
||||
if ($deviceIpPublic && !filter_var($deviceIpPublic, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
LogService::log('Device IP public invalide, ignorée', [
|
||||
'level' => 'debug',
|
||||
'user_id' => $userId,
|
||||
'device_ip_public' => $deviceIpPublic
|
||||
]);
|
||||
$deviceIpPublic = null; // Ignorer si ce n'est pas une IPv4 valide
|
||||
}
|
||||
|
||||
// Validation du niveau de batterie (0-100)
|
||||
$batteryLevel = isset($data['battery_level']) ? intval($data['battery_level']) : null;
|
||||
if ($batteryLevel !== null && ($batteryLevel < 0 || $batteryLevel > 100)) {
|
||||
$batteryLevel = null;
|
||||
}
|
||||
|
||||
// Conversion des booléens
|
||||
$nfcCapable = isset($data['device_nfc_capable']) ? ($data['device_nfc_capable'] ? 1 : 0) : null;
|
||||
$tapToPay = isset($data['device_supports_tap_to_pay']) ? ($data['device_supports_tap_to_pay'] ? 1 : 0) : null;
|
||||
$batteryCharging = isset($data['battery_charging']) ? ($data['battery_charging'] ? 1 : 0) : null;
|
||||
|
||||
// Conversion de la date de check
|
||||
$lastDeviceInfoCheck = null;
|
||||
if (!empty($data['last_device_info_check'])) {
|
||||
try {
|
||||
$date = new \DateTime($data['last_device_info_check']);
|
||||
$lastDeviceInfoCheck = $date->format('Y-m-d H:i:s');
|
||||
} catch (\Exception $e) {
|
||||
// Ignorer si le format de date est invalide
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->db->beginTransaction();
|
||||
|
||||
// Pour les plateformes Web sans device_identifier, générer un identifiant unique basé sur user_id + platform
|
||||
$deviceIdentifier = $data['device_identifier'] ?? null;
|
||||
|
||||
// Si device_identifier est vide ou NULL pour la plateforme Web, générer un identifiant
|
||||
if (empty($deviceIdentifier)) {
|
||||
$platform = $data['platform'] ?? 'unknown';
|
||||
|
||||
if ($platform === 'Web') {
|
||||
// Un seul device Web par utilisateur
|
||||
$deviceIdentifier = 'web_' . $userId . '_' . md5($userId . '_web');
|
||||
} else {
|
||||
// Pour les autres plateformes sans identifier, générer un identifiant aléatoire
|
||||
$deviceIdentifier = strtolower($platform) . '_' . $userId . '_' . uniqid();
|
||||
}
|
||||
|
||||
LogService::log('Device identifier généré automatiquement', [
|
||||
'level' => 'debug',
|
||||
'user_id' => $userId,
|
||||
'platform' => $platform,
|
||||
'device_identifier' => $deviceIdentifier
|
||||
]);
|
||||
}
|
||||
|
||||
// Requête INSERT ... ON DUPLICATE KEY UPDATE
|
||||
$sql = "INSERT INTO user_devices (
|
||||
fk_user, platform, device_model, device_name, device_manufacturer,
|
||||
device_identifier, device_ip_local, device_ip_public, device_wifi_name,
|
||||
device_wifi_bssid, ios_version, device_nfc_capable, device_supports_tap_to_pay,
|
||||
battery_level, battery_charging, battery_state, app_version, app_build,
|
||||
last_device_info_check
|
||||
) VALUES (
|
||||
:fk_user, :platform, :device_model, :device_name, :device_manufacturer,
|
||||
:device_identifier, :device_ip_local, :device_ip_public, :device_wifi_name,
|
||||
:device_wifi_bssid, :ios_version, :device_nfc_capable, :device_supports_tap_to_pay,
|
||||
:battery_level, :battery_charging, :battery_state, :app_version, :app_build,
|
||||
:last_device_info_check
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
platform = VALUES(platform),
|
||||
device_model = VALUES(device_model),
|
||||
device_name = VALUES(device_name),
|
||||
device_manufacturer = VALUES(device_manufacturer),
|
||||
device_ip_local = VALUES(device_ip_local),
|
||||
device_ip_public = VALUES(device_ip_public),
|
||||
device_wifi_name = VALUES(device_wifi_name),
|
||||
device_wifi_bssid = VALUES(device_wifi_bssid),
|
||||
ios_version = VALUES(ios_version),
|
||||
device_nfc_capable = VALUES(device_nfc_capable),
|
||||
device_supports_tap_to_pay = VALUES(device_supports_tap_to_pay),
|
||||
battery_level = VALUES(battery_level),
|
||||
battery_charging = VALUES(battery_charging),
|
||||
battery_state = VALUES(battery_state),
|
||||
app_version = VALUES(app_version),
|
||||
app_build = VALUES(app_build),
|
||||
last_device_info_check = VALUES(last_device_info_check),
|
||||
updated_at = CURRENT_TIMESTAMP";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
$params = [
|
||||
':fk_user' => $userId,
|
||||
':platform' => $data['platform'] ?? null,
|
||||
':device_model' => $data['device_model'] ?? null,
|
||||
':device_name' => $data['device_name'] ?? null,
|
||||
':device_manufacturer' => $data['device_manufacturer'] ?? null,
|
||||
':device_identifier' => $deviceIdentifier,
|
||||
':device_ip_local' => $deviceIpLocal,
|
||||
':device_ip_public' => $deviceIpPublic,
|
||||
':device_wifi_name' => $data['device_wifi_name'] ?? null,
|
||||
':device_wifi_bssid' => $data['device_wifi_bssid'] ?? null,
|
||||
':ios_version' => $data['ios_version'] ?? null,
|
||||
':device_nfc_capable' => $nfcCapable,
|
||||
':device_supports_tap_to_pay' => $tapToPay,
|
||||
':battery_level' => $batteryLevel,
|
||||
':battery_charging' => $batteryCharging,
|
||||
':battery_state' => $data['battery_state'] ?? null,
|
||||
':app_version' => $data['app_version'] ?? null,
|
||||
':app_build' => $data['app_build'] ?? null,
|
||||
':last_device_info_check' => $lastDeviceInfoCheck
|
||||
];
|
||||
|
||||
$stmt->execute($params);
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
LogService::log('Device info enregistrées avec succès', [
|
||||
'level' => 'info',
|
||||
'user_id' => $userId,
|
||||
'platform' => $data['platform'],
|
||||
'device_identifier' => $deviceIdentifier
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Informations du device enregistrées'
|
||||
], 200);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
if ($this->db->inTransaction()) {
|
||||
$this->db->rollBack();
|
||||
}
|
||||
|
||||
LogService::log('Error saving device info', [
|
||||
'level' => 'error',
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
'platform' => $data['platform'] ?? 'unknown'
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de l\'enregistrement des informations du device',
|
||||
'details' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes auxiliaires
|
||||
private function validateUpdateData(array $data): ?string {
|
||||
// Validation de l'email
|
||||
|
||||
Reference in New Issue
Block a user