feat: Livraison version 3.0.6
- Amélioration de la gestion des entités et des utilisateurs - Mise à jour des modèles Amicale et Client avec champs supplémentaires - Ajout du service de logging et amélioration du chargement UI - Refactoring des formulaires utilisateur et amicale - Intégration de file_picker et image_picker pour la gestion des fichiers - Amélioration de la gestion des membres et de leur suppression - Optimisation des performances de l'API - Mise à jour de la documentation technique 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -415,7 +415,7 @@ class EntiteController {
|
||||
}
|
||||
|
||||
$userRole = (int)$user['fk_role'];
|
||||
$isAdmin = $userRole > 2;
|
||||
$isAdmin = $userRole > 1;
|
||||
|
||||
// Récupérer les données de la requête
|
||||
$data = Request::getJson();
|
||||
@@ -577,6 +577,16 @@ class EntiteController {
|
||||
$updateFields[] = 'chk_stripe = ?';
|
||||
$params[] = $data['chk_stripe'] ? 1 : 0;
|
||||
}
|
||||
|
||||
if (isset($data['chk_mdp_manuel'])) {
|
||||
$updateFields[] = 'chk_mdp_manuel = ?';
|
||||
$params[] = $data['chk_mdp_manuel'] ? 1 : 0;
|
||||
}
|
||||
|
||||
if (isset($data['chk_username_manuel'])) {
|
||||
$updateFields[] = 'chk_username_manuel = ?';
|
||||
$params[] = $data['chk_username_manuel'] ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Si aucun champ à mettre à jour, retourner une erreur
|
||||
@@ -631,4 +641,341 @@ class EntiteController {
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload et traite le logo d'une entité
|
||||
* Réservé aux administrateurs d'amicale (fk_role == 2)
|
||||
*
|
||||
* @param string $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function uploadLogo(string $id): void {
|
||||
try {
|
||||
// Vérifier l'authentification
|
||||
$userId = Session::getUserId();
|
||||
if (!$userId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les infos de l'utilisateur
|
||||
$stmt = $this->db->prepare('SELECT fk_role, fk_entite FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur non trouvé'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est admin d'amicale (fk_role == 2)
|
||||
if ((int)$user['fk_role'] !== 2) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seuls les administrateurs d\'amicale peuvent uploader un logo'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que l'entité correspond à celle de l'utilisateur
|
||||
$entiteId = (int)$id;
|
||||
if ($entiteId !== (int)$user['fk_entite']) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous ne pouvez modifier que le logo de votre propre amicale'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier qu'un fichier a été envoyé
|
||||
if (!isset($_FILES['logo']) || $_FILES['logo']['error'] !== UPLOAD_ERR_OK) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Aucun fichier reçu ou erreur lors de l\'upload'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$uploadedFile = $_FILES['logo'];
|
||||
|
||||
// Vérifier le type MIME
|
||||
$allowedMimes = ['image/jpeg', 'image/jpg', 'image/png'];
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $uploadedFile['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mimeType, $allowedMimes)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Format de fichier non autorisé. Seuls PNG, JPG et JPEG sont acceptés'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Déterminer l'extension
|
||||
$extension = match($mimeType) {
|
||||
'image/jpeg', 'image/jpg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
default => 'jpg'
|
||||
};
|
||||
|
||||
// Créer le dossier de destination
|
||||
require_once __DIR__ . '/../Services/FileService.php';
|
||||
$fileService = new \FileService();
|
||||
$uploadPath = "/entites/{$entiteId}/logo";
|
||||
$fullPath = $fileService->createDirectory($entiteId, $uploadPath);
|
||||
|
||||
// Nom du fichier final
|
||||
$fileName = "logo_{$entiteId}_" . time() . ".{$extension}";
|
||||
$filePath = $fullPath . '/' . $fileName;
|
||||
|
||||
// Charger l'image avec GD
|
||||
$sourceImage = match($mimeType) {
|
||||
'image/jpeg', 'image/jpg' => imagecreatefromjpeg($uploadedFile['tmp_name']),
|
||||
'image/png' => imagecreatefrompng($uploadedFile['tmp_name']),
|
||||
default => false
|
||||
};
|
||||
|
||||
if (!$sourceImage) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Impossible de traiter l\'image'
|
||||
], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtenir les dimensions originales
|
||||
$originalWidth = imagesx($sourceImage);
|
||||
$originalHeight = imagesy($sourceImage);
|
||||
|
||||
// Calculer les nouvelles dimensions (max 250px) en gardant le ratio
|
||||
$maxSize = 250;
|
||||
$ratio = min($maxSize / $originalWidth, $maxSize / $originalHeight, 1);
|
||||
$newWidth = (int)($originalWidth * $ratio);
|
||||
$newHeight = (int)($originalHeight * $ratio);
|
||||
|
||||
// Créer l'image redimensionnée
|
||||
$resizedImage = imagecreatetruecolor($newWidth, $newHeight);
|
||||
|
||||
// Préserver la transparence pour les PNG
|
||||
if ($mimeType === 'image/png') {
|
||||
imagealphablending($resizedImage, false);
|
||||
imagesavealpha($resizedImage, true);
|
||||
$transparent = imagecolorallocatealpha($resizedImage, 255, 255, 255, 127);
|
||||
imagefilledrectangle($resizedImage, 0, 0, $newWidth, $newHeight, $transparent);
|
||||
}
|
||||
|
||||
// Redimensionner
|
||||
imagecopyresampled(
|
||||
$resizedImage, $sourceImage,
|
||||
0, 0, 0, 0,
|
||||
$newWidth, $newHeight,
|
||||
$originalWidth, $originalHeight
|
||||
);
|
||||
|
||||
// Sauvegarder l'image (72 DPI est la résolution standard web)
|
||||
$saved = match($mimeType) {
|
||||
'image/jpeg', 'image/jpg' => imagejpeg($resizedImage, $filePath, 85),
|
||||
'image/png' => imagepng($resizedImage, $filePath, 8),
|
||||
default => false
|
||||
};
|
||||
|
||||
// Libérer la mémoire
|
||||
imagedestroy($sourceImage);
|
||||
imagedestroy($resizedImage);
|
||||
|
||||
if (!$saved) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Impossible de sauvegarder l\'image'
|
||||
], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Appliquer les permissions
|
||||
$fileService->setFilePermissions($filePath);
|
||||
|
||||
// Supprimer l'ancien logo s'il existe
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, file_path FROM medias
|
||||
WHERE support = ? AND support_id = ? AND file_category = ?
|
||||
');
|
||||
$stmt->execute(['entite', $entiteId, 'logo']);
|
||||
$oldLogo = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($oldLogo && !empty($oldLogo['file_path']) && file_exists($oldLogo['file_path'])) {
|
||||
unlink($oldLogo['file_path']);
|
||||
// Supprimer l'entrée de la base
|
||||
$stmt = $this->db->prepare('DELETE FROM medias WHERE id = ?');
|
||||
$stmt->execute([$oldLogo['id']]);
|
||||
}
|
||||
|
||||
// Enregistrer dans la table medias
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO medias (
|
||||
support, support_id, fichier, file_type, file_category,
|
||||
file_size, mime_type, original_name, fk_entite,
|
||||
file_path, original_width, original_height,
|
||||
processed_width, processed_height, is_processed,
|
||||
description, created_at, fk_user_creat
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
'entite', // support
|
||||
$entiteId, // support_id
|
||||
$fileName, // fichier
|
||||
$extension, // file_type
|
||||
'logo', // file_category
|
||||
filesize($filePath), // file_size
|
||||
$mimeType, // mime_type
|
||||
$uploadedFile['name'], // original_name
|
||||
$entiteId, // fk_entite
|
||||
$filePath, // file_path
|
||||
$originalWidth, // original_width
|
||||
$originalHeight, // original_height
|
||||
$newWidth, // processed_width
|
||||
$newHeight, // processed_height
|
||||
1, // is_processed
|
||||
'Logo de l\'entité', // description
|
||||
$userId // fk_user_creat
|
||||
]);
|
||||
|
||||
$mediaId = $this->db->lastInsertId();
|
||||
|
||||
LogService::log('Upload de logo réussi', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'mediaId' => $mediaId,
|
||||
'fileName' => $fileName,
|
||||
'originalSize' => "{$originalWidth}x{$originalHeight}",
|
||||
'newSize' => "{$newWidth}x{$newHeight}"
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Logo uploadé avec succès',
|
||||
'media_id' => $mediaId,
|
||||
'file_name' => $fileName,
|
||||
'file_path' => $uploadPath . '/' . $fileName,
|
||||
'dimensions' => [
|
||||
'width' => $newWidth,
|
||||
'height' => $newHeight
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de l\'upload du logo', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entiteId' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de l\'upload du logo'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le logo d'une entité
|
||||
*
|
||||
* @param string $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getLogo(string $id): void {
|
||||
try {
|
||||
// Vérifier l'authentification
|
||||
$userId = Session::getUserId();
|
||||
if (!$userId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$entiteId = (int)$id;
|
||||
|
||||
// Récupérer le logo depuis la base
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, fichier, file_path, file_type, mime_type,
|
||||
processed_width, processed_height, file_size
|
||||
FROM medias
|
||||
WHERE support = ? AND support_id = ? AND file_category = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute(['entite', $entiteId, 'logo']);
|
||||
$logo = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$logo) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Aucun logo trouvé pour cette entité'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file_exists($logo['file_path'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Fichier logo introuvable'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Option 1 : Retourner l'image directement (pour une URL séparée)
|
||||
// header('Content-Type: ' . $logo['mime_type']);
|
||||
// header('Content-Length: ' . $logo['file_size']);
|
||||
// header('Cache-Control: public, max-age=86400'); // Cache 24h
|
||||
// readfile($logo['file_path']);
|
||||
|
||||
// Option 2 : Retourner en base64 dans JSON (recommandé pour Flutter)
|
||||
$imageData = file_get_contents($logo['file_path']);
|
||||
if ($imageData === false) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Impossible de lire le fichier logo'
|
||||
], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
$base64 = base64_encode($imageData);
|
||||
$dataUrl = 'data:' . $logo['mime_type'] . ';base64,' . $base64;
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'logo' => [
|
||||
'id' => $logo['id'],
|
||||
'data_url' => $dataUrl,
|
||||
'file_name' => $logo['fichier'],
|
||||
'mime_type' => $logo['mime_type'],
|
||||
'width' => $logo['processed_width'],
|
||||
'height' => $logo['processed_height'],
|
||||
'size' => $logo['file_size']
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération du logo', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entiteId' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la récupération du logo'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ class LoginController {
|
||||
$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.chk_active, u.sect_name,
|
||||
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
|
||||
@@ -432,7 +433,7 @@ class LoginController {
|
||||
}
|
||||
}
|
||||
|
||||
// Section "clients" supprimée car redondante avec le nouveau groupe "amicales"
|
||||
// 5. Section clients gérée plus bas pour les super-administrateurs
|
||||
|
||||
// 6. Récupérer les membres (users de l'entité du user) si nécessaire
|
||||
if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
||||
@@ -518,7 +519,8 @@ class LoginController {
|
||||
'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_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
|
||||
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
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
WHERE e.id = ? AND e.chk_active = 1'
|
||||
@@ -531,7 +533,8 @@ class LoginController {
|
||||
'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_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
|
||||
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
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
WHERE e.id != 1 AND e.chk_active = 1'
|
||||
@@ -583,7 +586,8 @@ class LoginController {
|
||||
'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_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
|
||||
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
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
WHERE e.fk_type = 1 AND e.chk_active = 1'
|
||||
@@ -636,12 +640,58 @@ class LoginController {
|
||||
|
||||
// Ajout des amicales à la racine de la réponse si disponibles
|
||||
if (!empty($amicalesData)) {
|
||||
// Récupérer le logo de l'entité de l'utilisateur si elle existe
|
||||
$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'])) {
|
||||
// Lire le fichier et l'encoder en base64
|
||||
$imageData = file_get_contents($logo['file_path']);
|
||||
if ($imageData !== false) {
|
||||
$base64 = base64_encode($imageData);
|
||||
// Format data URL pour usage direct dans Flutter
|
||||
$dataUrl = 'data:' . $logo['mime_type'] . ';base64,' . $base64;
|
||||
|
||||
$logoData = [
|
||||
'id' => $logo['id'],
|
||||
'data_url' => $dataUrl, // Image encodée en base64
|
||||
'file_name' => $logo['fichier'],
|
||||
'mime_type' => $logo['mime_type'],
|
||||
'width' => $logo['processed_width'],
|
||||
'height' => $logo['processed_height']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si c'est un tableau avec un seul élément, on envoie directement l'objet
|
||||
// pour que le client reçoive un objet et non un tableau avec un seul objet
|
||||
if (count($amicalesData) === 1) {
|
||||
$response['amicale'] = $amicalesData[0];
|
||||
// Ajouter le logo à l'amicale si disponible
|
||||
if ($logoData !== null) {
|
||||
$response['amicale']['logo'] = $logoData;
|
||||
}
|
||||
} else {
|
||||
$response['amicale'] = $amicalesData;
|
||||
// Pour plusieurs amicales, ajouter le logo à celle de l'utilisateur
|
||||
if ($logoData !== null && !empty($user['fk_entite'])) {
|
||||
foreach ($response['amicale'] as &$amicale) {
|
||||
if ($amicale['id'] == $user['fk_entite']) {
|
||||
$amicale['logo'] = $logoData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,7 +723,7 @@ class LoginController {
|
||||
$response['users_sectors'] = $usersSectorsData;
|
||||
}
|
||||
|
||||
// Section "clients" supprimée car redondante avec le nouveau groupe "amicales"
|
||||
// 5. Section clients gérée plus bas pour les super-administrateurs
|
||||
|
||||
// 9. Récupérer les régions selon le rôle de l'utilisateur
|
||||
$regionsData = [];
|
||||
|
||||
@@ -227,6 +227,26 @@ class UserController {
|
||||
$role = isset($data['role']) ? (int)$data['role'] : 1;
|
||||
$entiteId = isset($data['fk_entite']) ? (int)$data['fk_entite'] : 1;
|
||||
|
||||
// Récupérer les paramètres de gestion de l'entité
|
||||
$entiteStmt = $this->db->prepare('
|
||||
SELECT chk_mdp_manuel, chk_username_manuel, code_postal, ville
|
||||
FROM entites
|
||||
WHERE id = ?
|
||||
');
|
||||
$entiteStmt->execute([$entiteId]);
|
||||
$entiteConfig = $entiteStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$entiteConfig) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Entité non trouvée'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$chkMdpManuel = (int)$entiteConfig['chk_mdp_manuel'];
|
||||
$chkUsernameManuel = (int)$entiteConfig['chk_username_manuel'];
|
||||
|
||||
// Vérification des longueurs d'entrée
|
||||
if (strlen($email) > 75 || strlen($name) > 50) {
|
||||
Response::json([
|
||||
@@ -260,9 +280,83 @@ class UserController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Génération du mot de passe
|
||||
$password = ApiService::generateSecurePassword();
|
||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||
// Gestion du USERNAME selon chk_username_manuel
|
||||
$encryptedUsername = '';
|
||||
if ($chkUsernameManuel === 1) {
|
||||
// Username manuel obligatoire
|
||||
if (!isset($data['username']) || empty(trim($data['username']))) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le nom d\'utilisateur est requis pour cette entité'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$username = trim(strtolower($data['username']));
|
||||
|
||||
// Validation du format du username
|
||||
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Format du nom d\'utilisateur invalide (10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _)'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$encryptedUsername = ApiService::encryptSearchableData($username);
|
||||
|
||||
// Vérification de l'unicité du username
|
||||
$checkUsernameStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_user_name = ?');
|
||||
$checkUsernameStmt->execute([$encryptedUsername]);
|
||||
if ($checkUsernameStmt->fetch()) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Ce nom d\'utilisateur est déjà utilisé dans GeoSector'
|
||||
], 409);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Génération automatique du username
|
||||
$username = ApiService::generateUserName(
|
||||
$this->db,
|
||||
$name,
|
||||
$entiteConfig['code_postal'] ?? '00000',
|
||||
$entiteConfig['ville'] ?? 'ville',
|
||||
10
|
||||
);
|
||||
$encryptedUsername = ApiService::encryptSearchableData($username);
|
||||
}
|
||||
|
||||
// Gestion du MOT DE PASSE selon chk_mdp_manuel
|
||||
$password = '';
|
||||
$passwordHash = '';
|
||||
if ($chkMdpManuel === 1) {
|
||||
// Mot de passe manuel obligatoire
|
||||
if (!isset($data['password']) || empty($data['password'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le mot de passe est requis pour cette entité'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$password = $data['password'];
|
||||
|
||||
// Validation du mot de passe (minimum 8 caractères)
|
||||
if (strlen($password) < 8) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le mot de passe doit contenir au moins 8 caractères'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||
} else {
|
||||
// Génération automatique du mot de passe
|
||||
$password = ApiService::generateSecurePassword();
|
||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
// Préparation des champs optionnels
|
||||
$phone = isset($data['phone']) ? ApiService::encryptData(trim($data['phone'])) : null;
|
||||
@@ -276,13 +370,13 @@ class UserController {
|
||||
// Insertion en base de données
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO users (
|
||||
encrypted_email, user_pass_hash, encrypted_name, first_name,
|
||||
encrypted_email, encrypted_user_name, user_pass_hash, encrypted_name, first_name,
|
||||
sect_name, encrypted_phone, encrypted_mobile, fk_role,
|
||||
fk_entite, chk_alert_email, chk_suivi,
|
||||
date_naissance, date_embauche,
|
||||
created_at, fk_user_creat, chk_active
|
||||
) VALUES (
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?,
|
||||
?, ?,
|
||||
@@ -291,6 +385,7 @@ class UserController {
|
||||
');
|
||||
$stmt->execute([
|
||||
$encryptedEmail,
|
||||
$encryptedUsername,
|
||||
$passwordHash,
|
||||
$encryptedName,
|
||||
$firstName,
|
||||
@@ -307,21 +402,54 @@ class UserController {
|
||||
]);
|
||||
$userId = $this->db->lastInsertId();
|
||||
|
||||
// Envoi de l'email avec les identifiants
|
||||
ApiService::sendEmail($email, $name, 'welcome', ['password' => $password]);
|
||||
// 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);
|
||||
|
||||
LogService::log('Utilisateur GeoSector créé', [
|
||||
'level' => 'info',
|
||||
'createdBy' => $currentUserId,
|
||||
'newUserId' => $userId,
|
||||
'email' => $email
|
||||
'email' => $email,
|
||||
'username' => $username,
|
||||
'usernameManual' => $chkUsernameManuel === 1 ? 'oui' : 'non',
|
||||
'passwordManual' => $chkMdpManuel === 1 ? 'oui' : 'non',
|
||||
'emailsSent' => '2 emails (username + password)'
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
// Préparer la réponse avec les informations de connexion si générées automatiquement
|
||||
$responseData = [
|
||||
'status' => 'success',
|
||||
'message' => 'Utilisateur créé avec succès',
|
||||
'id' => $userId
|
||||
], 201);
|
||||
];
|
||||
|
||||
// Ajouter le username dans la réponse (toujours, car nécessaire pour la connexion)
|
||||
$responseData['username'] = $username;
|
||||
|
||||
// Ajouter le mot de passe seulement si généré automatiquement
|
||||
if ($chkMdpManuel === 0) {
|
||||
$responseData['password'] = $password;
|
||||
}
|
||||
|
||||
Response::json($responseData, 201);
|
||||
} catch (PDOException $e) {
|
||||
LogService::log('Erreur lors de la création d\'un utilisateur GeoSector', [
|
||||
'level' => 'error',
|
||||
@@ -756,6 +884,106 @@ class UserController {
|
||||
}
|
||||
}
|
||||
|
||||
public function checkUsername(): void {
|
||||
Session::requireAuth();
|
||||
|
||||
try {
|
||||
$data = Request::getJson();
|
||||
|
||||
// Validation de la présence du username
|
||||
if (!isset($data['username']) || empty(trim($data['username']))) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Username requis pour la vérification'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$username = trim(strtolower($data['username']));
|
||||
|
||||
// Validation du format du username
|
||||
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Format invalide : 10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _',
|
||||
'available' => false
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chiffrement du username pour la recherche
|
||||
$encryptedUsername = ApiService::encryptSearchableData($username);
|
||||
|
||||
// Vérification de l'existence dans la base
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, encrypted_name, fk_entite
|
||||
FROM users
|
||||
WHERE encrypted_user_name = ?
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([$encryptedUsername]);
|
||||
$existingUser = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existingUser) {
|
||||
// Username déjà pris - générer des suggestions
|
||||
$baseName = substr($username, 0, -2); // Enlever les 2 derniers caractères
|
||||
$suggestions = [];
|
||||
|
||||
// Génération de 3 suggestions
|
||||
$suggestions[] = $username . '_' . rand(10, 99);
|
||||
$suggestions[] = $baseName . rand(100, 999);
|
||||
|
||||
// Suggestion avec l'année courante
|
||||
$year = date('y');
|
||||
$suggestions[] = $username . $year;
|
||||
|
||||
// Vérifier que les suggestions sont aussi disponibles
|
||||
$availableSuggestions = [];
|
||||
foreach ($suggestions as $suggestion) {
|
||||
$encryptedSuggestion = ApiService::encryptSearchableData($suggestion);
|
||||
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_user_name = ?');
|
||||
$checkStmt->execute([$encryptedSuggestion]);
|
||||
if (!$checkStmt->fetch()) {
|
||||
$availableSuggestions[] = $suggestion;
|
||||
}
|
||||
}
|
||||
|
||||
// Si aucune suggestion n'est disponible, en générer d'autres
|
||||
if (empty($availableSuggestions)) {
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$randomSuffix = rand(1000, 9999);
|
||||
$availableSuggestions[] = $baseName . $randomSuffix;
|
||||
}
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'available' => false,
|
||||
'message' => 'Ce nom d\'utilisateur est déjà utilisé',
|
||||
'suggestions' => array_slice($availableSuggestions, 0, 3)
|
||||
]);
|
||||
} else {
|
||||
// Username disponible
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'available' => true,
|
||||
'message' => 'Nom d\'utilisateur disponible',
|
||||
'username' => $username
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
LogService::log('Erreur lors de la vérification du username', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur lors de la vérification'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes auxiliaires
|
||||
private function validateUpdateData(array $data): ?string {
|
||||
// Validation de l'email
|
||||
|
||||
Reference in New Issue
Block a user