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

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