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:
2025-08-08 20:33:54 +02:00
parent 599b9fcda0
commit 206c76c7db
69 changed files with 203569 additions and 174972 deletions

View File

@@ -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);
}
}
}

View File

@@ -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 = [];

View File

@@ -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

View File

@@ -38,6 +38,7 @@ class Router {
$this->put('users/:id', ['UserController', 'updateUser']);
$this->delete('users/:id', ['UserController', 'deleteUser']);
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
$this->post('users/check-username', ['UserController', 'checkUsername']);
$this->post('logout', ['LoginController', 'logout']);
// Routes entités
@@ -45,6 +46,8 @@ class Router {
$this->get('entites/:id', ['EntiteController', 'getEntiteById']);
$this->get('entites/postal/:code', ['EntiteController', 'getEntiteByPostalCode']);
$this->put('entites/:id', ['EntiteController', 'updateEntite']);
$this->post('entites/:id/logo', ['EntiteController', 'uploadLogo']);
$this->get('entites/:id/logo', ['EntiteController', 'getLogo']);
// Routes opérations
$this->get('operations', ['OperationController', 'getOperations']);

View File

@@ -52,6 +52,16 @@ class ApiService {
$mail->Subject = 'Bienvenue sur GEOSECTOR';
$mail->Body = EmailTemplates::getWelcomeTemplate($name, $data['username'] ?? '', $data['password']);
break;
case 'welcome_username':
$mail->Subject = 'GEOSECTOR - Votre identifiant de connexion';
$mail->Body = EmailTemplates::getWelcomeUsernameTemplate($name, $data['username'] ?? '');
break;
case 'welcome_password':
$mail->Subject = 'GEOSECTOR - Votre mot de passe';
$mail->Body = EmailTemplates::getWelcomePasswordTemplate($name, $data['password'] ?? '');
break;
case 'lostpwd':
$mail->Subject = 'Réinitialisation de votre mot de passe GEOSECTOR';

View File

@@ -17,6 +17,59 @@ class EmailTemplates {
L'équipe GeoSector";
}
/**
* Template d'email de bienvenue - Identifiant uniquement
*/
public static function getWelcomeUsernameTemplate(string $name, string $username): string {
return "
Bonjour $name,<br><br>
Votre compte a été créé avec succès sur <b>GeoSector</b>.<br><br>
Voici votre identifiant de connexion :<br>
<div style='background:#f5f5f5; padding:15px; margin:20px 0; border-left:4px solid #007bff;'>
<b style='font-size:16px;'>Identifiant :</b> <span style='font-size:18px; color:#333;'>$username</span>
</div>
<p style='color:#666; font-size:14px;'>
<b>Important :</b> Conservez précieusement cet identifiant, vous en aurez besoin pour vous connecter.
</p>
<p>
Votre mot de passe vous sera communiqué dans un email séparé pour des raisons de sécurité.
</p>
<p>
Une fois que vous aurez reçu votre mot de passe, vous pourrez vous connecter sur
<a href=\"https://app.geosector.fr\" style='color:#007bff;'>app.geosector.fr</a>
</p>
<br>
À très bientôt,<br>
L'équipe GeoSector";
}
/**
* Template d'email de bienvenue - Mot de passe uniquement
*/
public static function getWelcomePasswordTemplate(string $name, string $password): string {
return "
Bonjour $name,<br><br>
Suite à la création de votre compte <b>GeoSector</b>, voici votre mot de passe :<br><br>
<div style='background:#f5f5f5; padding:15px; margin:20px 0; border-left:4px solid #28a745;'>
<b style='font-size:16px;'>Mot de passe :</b> <span style='font-family:monospace; font-size:18px; color:#333;'>$password</span>
</div>
<p style='color:#d73502; font-size:14px;'>
<b>⚠ Sécurité :</b> Pour garantir la sécurité de votre compte, nous vous recommandons
de conserver ce mot de passe en lieu sûr et de ne jamais le partager.
</p>
<p>
Vous pouvez maintenant vous connecter avec votre identifiant (reçu dans un email précédent)
et ce mot de passe sur <a href=\"https://app.geosector.fr\" style='color:#007bff;'>app.geosector.fr</a>
</p>
<p style='background:#fff3cd; padding:10px; border-radius:5px; margin-top:20px;'>
<b>Rappel :</b> Ne communiquez jamais votre mot de passe à un tiers. L'équipe GeoSector
ne vous demandera jamais votre mot de passe par email ou téléphone.
</p>
<br>
À très bientôt,<br>
L'équipe GeoSector";
}
/**
* Template d'email pour mot de passe perdu
*/