feat: Release version 3.1.4 - Mode terrain et génération PDF
✨ Nouvelles fonctionnalités: - Ajout du mode terrain pour utilisation mobile hors connexion - Génération automatique de reçus PDF avec template personnalisé - Révision complète du système de cartes avec amélioration des performances 🔧 Améliorations techniques: - Refactoring du module chat avec architecture simplifiée - Optimisation du système de sécurité NIST SP 800-63B - Amélioration de la gestion des secteurs géographiques - Support UTF-8 étendu pour les noms d'utilisateurs 📱 Application mobile: - Nouveau mode terrain dans user_field_mode_page - Interface utilisateur adaptative pour conditions difficiles - Synchronisation offline améliorée 🗺️ Cartographie: - Optimisation des performances MapBox - Meilleure gestion des tuiles hors ligne - Amélioration de l'affichage des secteurs 📄 Documentation: - Ajout guide Android (ANDROID-GUIDE.md) - Documentation sécurité API (API-SECURITY.md) - Guide module chat (CHAT_MODULE.md) 🐛 Corrections: - Résolution des erreurs 400 lors de la création d'utilisateurs - Correction de la validation des noms d'utilisateurs - Fix des problèmes de synchronisation chat 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
150
api/scripts/cron/cleanup_security_data.php
Normal file
150
api/scripts/cron/cleanup_security_data.php
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script de nettoyage des données de sécurité
|
||||
* À exécuter via cron quotidiennement
|
||||
* Exemple crontab: 0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Configuration
|
||||
require_once __DIR__ . '/../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||
|
||||
// Initialiser la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$config = $appConfig->getFullConfig();
|
||||
|
||||
// Initialiser la base de données
|
||||
Database::init($config['database']);
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Configuration de rétention (en jours)
|
||||
$RETENTION_DAYS = [
|
||||
'performance_metrics' => 30, // Garder 30 jours de métriques
|
||||
'failed_login_attempts' => 7, // Garder 7 jours de tentatives
|
||||
'resolved_alerts' => 90, // Garder 90 jours d'alertes résolues
|
||||
'expired_blocks' => 0 // Débloquer immédiatement les IPs expirées
|
||||
];
|
||||
|
||||
echo "[" . date('Y-m-d H:i:s') . "] Début du nettoyage des données de sécurité\n";
|
||||
|
||||
try {
|
||||
$totalDeleted = 0;
|
||||
|
||||
// 1. Nettoyer les métriques de performance
|
||||
echo "- Nettoyage des métriques de performance (>" . $RETENTION_DAYS['performance_metrics'] . " jours)...\n";
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM sec_performance_metrics
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||
');
|
||||
$stmt->execute(['days' => $RETENTION_DAYS['performance_metrics']]);
|
||||
$deleted = $stmt->rowCount();
|
||||
echo " → $deleted lignes supprimées\n";
|
||||
$totalDeleted += $deleted;
|
||||
|
||||
// 2. Nettoyer les tentatives de login échouées
|
||||
echo "- Nettoyage des tentatives de login (>" . $RETENTION_DAYS['failed_login_attempts'] . " jours)...\n";
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM sec_failed_login_attempts
|
||||
WHERE attempt_time < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||
');
|
||||
$stmt->execute(['days' => $RETENTION_DAYS['failed_login_attempts']]);
|
||||
$deleted = $stmt->rowCount();
|
||||
echo " → $deleted lignes supprimées\n";
|
||||
$totalDeleted += $deleted;
|
||||
|
||||
// 3. Nettoyer les alertes résolues
|
||||
echo "- Nettoyage des alertes résolues (>" . $RETENTION_DAYS['resolved_alerts'] . " jours)...\n";
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM sec_alerts
|
||||
WHERE resolved = 1
|
||||
AND resolved_at < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||
');
|
||||
$stmt->execute(['days' => $RETENTION_DAYS['resolved_alerts']]);
|
||||
$deleted = $stmt->rowCount();
|
||||
echo " → $deleted lignes supprimées\n";
|
||||
$totalDeleted += $deleted;
|
||||
|
||||
// 4. Débloquer les IPs expirées
|
||||
echo "- Déblocage des IPs expirées...\n";
|
||||
$stmt = $db->prepare('
|
||||
UPDATE sec_blocked_ips
|
||||
SET unblocked_at = NOW()
|
||||
WHERE blocked_until <= NOW()
|
||||
AND unblocked_at IS NULL
|
||||
AND permanent = 0
|
||||
');
|
||||
$stmt->execute();
|
||||
$unblocked = $stmt->rowCount();
|
||||
echo " → $unblocked IPs débloquées\n";
|
||||
|
||||
// 5. Supprimer les anciennes IPs débloquées (optionnel, garder 180 jours d'historique)
|
||||
echo "- Suppression des anciennes IPs débloquées (>180 jours)...\n";
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM sec_blocked_ips
|
||||
WHERE unblocked_at IS NOT NULL
|
||||
AND unblocked_at < DATE_SUB(NOW(), INTERVAL 180 DAY)
|
||||
');
|
||||
$stmt->execute();
|
||||
$deleted = $stmt->rowCount();
|
||||
echo " → $deleted lignes supprimées\n";
|
||||
$totalDeleted += $deleted;
|
||||
|
||||
// 6. Optimiser les tables (optionnel, peut être long sur de grosses tables)
|
||||
if ($totalDeleted > 1000) {
|
||||
echo "- Optimisation des tables...\n";
|
||||
$tables = [
|
||||
'sec_performance_metrics',
|
||||
'sec_failed_login_attempts',
|
||||
'sec_alerts',
|
||||
'sec_blocked_ips'
|
||||
];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
try {
|
||||
$db->exec("OPTIMIZE TABLE $table");
|
||||
echo " → Table $table optimisée\n";
|
||||
} catch (Exception $e) {
|
||||
echo " ⚠ Impossible d'optimiser $table: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Statistiques finales
|
||||
echo "\n=== RÉSUMÉ ===\n";
|
||||
echo "Total supprimé: $totalDeleted lignes\n";
|
||||
echo "IPs débloquées: $unblocked\n";
|
||||
|
||||
// Obtenir les statistiques actuelles
|
||||
$stats = [];
|
||||
$tables = [
|
||||
'sec_alerts' => "SELECT COUNT(*) as total, SUM(resolved = 0) as active FROM sec_alerts",
|
||||
'sec_performance_metrics' => "SELECT COUNT(*) as total FROM sec_performance_metrics",
|
||||
'sec_failed_login_attempts' => "SELECT COUNT(*) as total FROM sec_failed_login_attempts",
|
||||
'sec_blocked_ips' => "SELECT COUNT(*) as total, SUM(permanent = 1) as permanent FROM sec_blocked_ips WHERE unblocked_at IS NULL"
|
||||
];
|
||||
|
||||
echo "\nÉtat actuel des tables:\n";
|
||||
foreach ($tables as $table => $query) {
|
||||
$result = $db->query($query)->fetch(PDO::FETCH_ASSOC);
|
||||
if ($table === 'sec_alerts') {
|
||||
echo "- $table: {$result['total']} total, {$result['active']} actives\n";
|
||||
} elseif ($table === 'sec_blocked_ips') {
|
||||
$permanent = $result['permanent'] ?? 0;
|
||||
echo "- $table: {$result['total']} bloquées, $permanent permanentes\n";
|
||||
} else {
|
||||
echo "- $table: {$result['total']} enregistrements\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n[" . date('Y-m-d H:i:s') . "] Nettoyage terminé avec succès\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "\n❌ ERREUR: " . $e->getMessage() . "\n";
|
||||
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
249
api/scripts/php/init_security_tables.php
Normal file
249
api/scripts/php/init_security_tables.php
Normal file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script d'initialisation des tables de sécurité
|
||||
* Crée les tables si elles n'existent pas
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||
|
||||
// Initialiser la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$config = $appConfig->getFullConfig();
|
||||
|
||||
// Initialiser la base de données
|
||||
Database::init($config['database']);
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "\n========================================\n";
|
||||
echo " CRÉATION DES TABLES DE SÉCURITÉ\n";
|
||||
echo "========================================\n\n";
|
||||
|
||||
try {
|
||||
// Désactiver temporairement le mode strict pour les clés étrangères
|
||||
$db->exec("SET FOREIGN_KEY_CHECKS = 0");
|
||||
|
||||
// 1. Table des alertes
|
||||
echo "1. Création de la table sec_alerts...\n";
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `sec_alerts` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`alert_type` VARCHAR(50) NOT NULL COMMENT 'Type d\'alerte (BRUTE_FORCE, SQL_ERROR, etc.)',
|
||||
`alert_level` ENUM('INFO', 'WARNING', 'ERROR', 'CRITICAL', 'SECURITY') NOT NULL DEFAULT 'INFO',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP source',
|
||||
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté ou utilisé',
|
||||
`endpoint` VARCHAR(255) DEFAULT NULL COMMENT 'Endpoint API concerné',
|
||||
`method` VARCHAR(10) DEFAULT NULL COMMENT 'Méthode HTTP',
|
||||
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels en JSON',
|
||||
`occurrences` INT(11) DEFAULT 1 COMMENT 'Nombre d\'occurrences',
|
||||
`first_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`email_sent` TINYINT(1) DEFAULT 0 COMMENT 'Email d\'alerte envoyé',
|
||||
`email_sent_at` TIMESTAMP NULL DEFAULT NULL,
|
||||
`resolved` TINYINT(1) DEFAULT 0 COMMENT 'Alerte résolue',
|
||||
`resolved_at` TIMESTAMP NULL DEFAULT NULL,
|
||||
`resolved_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID admin qui a résolu',
|
||||
`notes` TEXT DEFAULT NULL COMMENT 'Notes de résolution',
|
||||
KEY `idx_ip` (`ip_address`),
|
||||
KEY `idx_type_time` (`alert_type`, `last_seen`),
|
||||
KEY `idx_level` (`alert_level`),
|
||||
KEY `idx_resolved` (`resolved`),
|
||||
KEY `idx_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Alertes de sécurité et monitoring'
|
||||
");
|
||||
echo " ✓ Table sec_alerts créée\n";
|
||||
|
||||
// 2. Table des métriques de performance (SANS PARTITIONNEMENT)
|
||||
echo "2. Création de la table sec_performance_metrics...\n";
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `sec_performance_metrics` (
|
||||
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`endpoint` VARCHAR(255) NOT NULL COMMENT 'Endpoint API',
|
||||
`method` VARCHAR(10) NOT NULL COMMENT 'Méthode HTTP',
|
||||
`response_time_ms` INT(11) NOT NULL COMMENT 'Temps de réponse total en ms',
|
||||
`db_time_ms` INT(11) DEFAULT 0 COMMENT 'Temps cumulé des requêtes DB en ms',
|
||||
`db_queries_count` INT(11) DEFAULT 0 COMMENT 'Nombre de requêtes DB',
|
||||
`memory_peak_mb` FLOAT DEFAULT NULL COMMENT 'Pic mémoire en MB',
|
||||
`memory_start_mb` FLOAT DEFAULT NULL COMMENT 'Mémoire au début en MB',
|
||||
`http_status` INT(11) DEFAULT NULL COMMENT 'Code HTTP de réponse',
|
||||
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP',
|
||||
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent complet',
|
||||
`request_size` INT(11) DEFAULT NULL COMMENT 'Taille de la requête en octets',
|
||||
`response_size` INT(11) DEFAULT NULL COMMENT 'Taille de la réponse en octets',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY `idx_endpoint_time` (`endpoint`, `created_at`),
|
||||
KEY `idx_response_time` (`response_time_ms`),
|
||||
KEY `idx_created` (`created_at`),
|
||||
KEY `idx_status` (`http_status`),
|
||||
KEY `idx_user` (`user_id`),
|
||||
KEY `idx_date_endpoint` (`created_at`, `endpoint`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Métriques de performance des requêtes'
|
||||
");
|
||||
echo " ✓ Table sec_performance_metrics créée\n";
|
||||
|
||||
// 3. Table des tentatives de login échouées
|
||||
echo "3. Création de la table sec_failed_login_attempts...\n";
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `sec_failed_login_attempts` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté',
|
||||
`encrypted_username` VARCHAR(255) DEFAULT NULL COMMENT 'Username chiffré si trouvé',
|
||||
`ip_address` VARCHAR(45) NOT NULL COMMENT 'Adresse IP',
|
||||
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent',
|
||||
`attempt_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`error_type` VARCHAR(50) DEFAULT NULL COMMENT 'Type d\'erreur (invalid_password, user_not_found, etc.)',
|
||||
`country_code` VARCHAR(2) DEFAULT NULL COMMENT 'Code pays de l\'IP (si géoloc activée)',
|
||||
KEY `idx_ip_time` (`ip_address`, `attempt_time`),
|
||||
KEY `idx_username` (`username`),
|
||||
KEY `idx_encrypted_username` (`encrypted_username`),
|
||||
KEY `idx_time` (`attempt_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Tentatives de connexion échouées'
|
||||
");
|
||||
echo " ✓ Table sec_failed_login_attempts créée\n";
|
||||
|
||||
// 4. Table des IPs bloquées
|
||||
echo "4. Création de la table sec_blocked_ips...\n";
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `sec_blocked_ips` (
|
||||
`ip_address` VARCHAR(45) NOT NULL PRIMARY KEY COMMENT 'Adresse IP bloquée',
|
||||
`reason` VARCHAR(255) NOT NULL COMMENT 'Raison du blocage',
|
||||
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels',
|
||||
`blocked_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`blocked_until` TIMESTAMP NOT NULL COMMENT 'Bloqué jusqu\'à',
|
||||
`blocked_by` VARCHAR(50) DEFAULT 'system' COMMENT 'Qui a bloqué (system ou user ID)',
|
||||
`permanent` TINYINT(1) DEFAULT 0 COMMENT 'Blocage permanent',
|
||||
`unblocked_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Date de déblocage effectif',
|
||||
`unblocked_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'Qui a débloqué',
|
||||
`block_count` INT(11) DEFAULT 1 COMMENT 'Nombre de fois bloquée',
|
||||
KEY `idx_blocked_until` (`blocked_until`),
|
||||
KEY `idx_permanent` (`permanent`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IPs bloquées temporairement ou définitivement'
|
||||
");
|
||||
echo " ✓ Table sec_blocked_ips créée\n";
|
||||
|
||||
// 5. Créer les vues
|
||||
echo "5. Création des vues...\n";
|
||||
|
||||
// Vue pour les alertes actives
|
||||
$db->exec("
|
||||
CREATE OR REPLACE VIEW sec_active_alerts AS
|
||||
SELECT
|
||||
a.*,
|
||||
u.encrypted_name as user_name,
|
||||
r.encrypted_name as resolver_name
|
||||
FROM sec_alerts a
|
||||
LEFT JOIN users u ON a.user_id = u.id
|
||||
LEFT JOIN users r ON a.resolved_by = r.id
|
||||
WHERE a.resolved = 0
|
||||
OR (a.resolved = 1 AND a.resolved_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR))
|
||||
ORDER BY
|
||||
CASE a.alert_level
|
||||
WHEN 'SECURITY' THEN 1
|
||||
WHEN 'CRITICAL' THEN 2
|
||||
WHEN 'ERROR' THEN 3
|
||||
WHEN 'WARNING' THEN 4
|
||||
WHEN 'INFO' THEN 5
|
||||
END,
|
||||
a.last_seen DESC
|
||||
");
|
||||
echo " ✓ Vue sec_active_alerts créée\n";
|
||||
|
||||
// Vue pour les IPs suspectes
|
||||
$db->exec("
|
||||
CREATE OR REPLACE VIEW sec_suspicious_ips AS
|
||||
SELECT
|
||||
ip_address,
|
||||
COUNT(*) as total_attempts,
|
||||
COUNT(DISTINCT username) as unique_usernames,
|
||||
MIN(attempt_time) as first_attempt,
|
||||
MAX(attempt_time) as last_attempt,
|
||||
TIMESTAMPDIFF(MINUTE, MIN(attempt_time), MAX(attempt_time)) as timespan_minutes
|
||||
FROM sec_failed_login_attempts
|
||||
WHERE attempt_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||
GROUP BY ip_address
|
||||
HAVING total_attempts >= 5
|
||||
OR unique_usernames >= 3
|
||||
ORDER BY total_attempts DESC
|
||||
");
|
||||
echo " ✓ Vue sec_suspicious_ips créée\n";
|
||||
|
||||
// 6. Créer les index additionnels
|
||||
echo "6. Création des index additionnels...\n";
|
||||
|
||||
// Index pour les requêtes fréquentes
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_sec_metrics_recent ON sec_performance_metrics(created_at DESC, endpoint)");
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_sec_alerts_recent ON sec_alerts(last_seen DESC, alert_level)");
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_sec_failed_recent ON sec_failed_login_attempts(attempt_time DESC, ip_address)");
|
||||
echo " ✓ Index créés\n";
|
||||
|
||||
// 7. Créer la procédure de nettoyage
|
||||
echo "7. Création de la procédure de nettoyage...\n";
|
||||
$db->exec("DROP PROCEDURE IF EXISTS sec_cleanup_old_data");
|
||||
$db->exec("
|
||||
CREATE PROCEDURE sec_cleanup_old_data(IN days_to_keep INT)
|
||||
BEGIN
|
||||
-- Nettoyer les métriques de performance
|
||||
DELETE FROM sec_performance_metrics
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
|
||||
-- Nettoyer les tentatives de login
|
||||
DELETE FROM sec_failed_login_attempts
|
||||
WHERE attempt_time < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
|
||||
-- Nettoyer les alertes résolues
|
||||
DELETE FROM sec_alerts
|
||||
WHERE resolved = 1
|
||||
AND resolved_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
|
||||
-- Retourner le nombre de lignes supprimées
|
||||
SELECT ROW_COUNT() as deleted_rows;
|
||||
END
|
||||
");
|
||||
echo " ✓ Procédure sec_cleanup_old_data créée\n";
|
||||
|
||||
// Réactiver les clés étrangères
|
||||
$db->exec("SET FOREIGN_KEY_CHECKS = 1");
|
||||
|
||||
// 8. Vérifier que tout est créé
|
||||
echo "\n8. Vérification finale...\n";
|
||||
$tables = ['sec_alerts', 'sec_performance_metrics', 'sec_failed_login_attempts', 'sec_blocked_ips'];
|
||||
$allOk = true;
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$stmt = $db->query("SELECT COUNT(*) as count FROM $table");
|
||||
if ($stmt) {
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
echo " ✓ Table $table : OK ({$result['count']} enregistrements)\n";
|
||||
} else {
|
||||
echo " ✗ Table $table : ERREUR\n";
|
||||
$allOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($allOk) {
|
||||
echo "\n========================================\n";
|
||||
echo "✅ TOUTES LES TABLES ONT ÉTÉ CRÉÉES AVEC SUCCÈS\n";
|
||||
echo "========================================\n\n";
|
||||
echo "Le système de sécurité est maintenant prêt à être utilisé.\n";
|
||||
echo "Vous pouvez tester avec : php test_security.php\n\n";
|
||||
} else {
|
||||
echo "\n⚠️ Certaines tables n'ont pas pu être créées.\n";
|
||||
echo "Vérifiez les erreurs ci-dessus.\n\n";
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "\n❌ ERREUR SQL : " . $e->getMessage() . "\n\n";
|
||||
echo "Code d'erreur : " . $e->getCode() . "\n";
|
||||
echo "Vérifiez les permissions et la configuration de la base de données.\n\n";
|
||||
exit(1);
|
||||
} catch (Exception $e) {
|
||||
echo "\n❌ ERREUR : " . $e->getMessage() . "\n\n";
|
||||
exit(1);
|
||||
}
|
||||
157
api/scripts/sql/create_chat_tables.sql
Normal file
157
api/scripts/sql/create_chat_tables.sql
Normal file
@@ -0,0 +1,157 @@
|
||||
-- Script de création des tables pour le module Chat
|
||||
-- Date : 2025-01-17
|
||||
-- Version : 1.0
|
||||
|
||||
-- Tables préfixées "chat_" pour le module de messagerie
|
||||
|
||||
-- ============================================
|
||||
-- SUPPRESSION DES TABLES EXISTANTES
|
||||
-- ============================================
|
||||
-- Attention : Ceci supprimera toutes les données existantes du chat !
|
||||
|
||||
-- Désactiver temporairement les contraintes de clés étrangères
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- Supprimer la vue si elle existe
|
||||
DROP VIEW IF EXISTS chat_rooms_with_last_message;
|
||||
|
||||
-- Supprimer les tables dans l'ordre inverse des dépendances
|
||||
DROP TABLE IF EXISTS `chat_read_receipts`;
|
||||
DROP TABLE IF EXISTS `chat_participants`;
|
||||
DROP TABLE IF EXISTS `chat_messages`;
|
||||
DROP TABLE IF EXISTS `chat_rooms`;
|
||||
|
||||
-- Supprimer toute autre table commençant par chat_ qui pourrait exister
|
||||
-- Note : Cette procédure supprime dynamiquement toutes les tables avec le préfixe chat_
|
||||
DELIMITER $$
|
||||
DROP PROCEDURE IF EXISTS drop_chat_tables$$
|
||||
CREATE PROCEDURE drop_chat_tables()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
DECLARE tableName VARCHAR(255);
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name LIKE 'chat_%';
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
OPEN cur;
|
||||
|
||||
read_loop: LOOP
|
||||
FETCH cur INTO tableName;
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
SET @sql = CONCAT('DROP TABLE IF EXISTS `', tableName, '`');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END LOOP;
|
||||
|
||||
CLOSE cur;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
-- Exécuter la procédure
|
||||
CALL drop_chat_tables();
|
||||
|
||||
-- Supprimer la procédure après utilisation
|
||||
DROP PROCEDURE IF EXISTS drop_chat_tables;
|
||||
|
||||
-- Réactiver les contraintes de clés étrangères
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ============================================
|
||||
-- CRÉATION DES NOUVELLES TABLES
|
||||
-- ============================================
|
||||
|
||||
-- Table des salles de conversation
|
||||
CREATE TABLE IF NOT EXISTS `chat_rooms` (
|
||||
`id` VARCHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID de la salle',
|
||||
`title` VARCHAR(255) DEFAULT NULL COMMENT 'Titre de la conversation',
|
||||
`type` ENUM('private', 'group', 'broadcast') NOT NULL DEFAULT 'private' COMMENT 'Type de conversation',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`created_by` INT(11) UNSIGNED NOT NULL COMMENT 'ID du créateur',
|
||||
`updated_at` TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Dernière modification',
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Conversation active',
|
||||
KEY `idx_created_by` (`created_by`),
|
||||
KEY `idx_type` (`type`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `fk_chat_rooms_creator` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Salles de conversation';
|
||||
|
||||
-- Table des messages
|
||||
CREATE TABLE IF NOT EXISTS `chat_messages` (
|
||||
`id` VARCHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID du message',
|
||||
`room_id` VARCHAR(36) NOT NULL COMMENT 'ID de la salle',
|
||||
`content` TEXT NOT NULL COMMENT 'Contenu du message',
|
||||
`sender_id` INT(11) UNSIGNED NOT NULL COMMENT 'ID de l\'expéditeur',
|
||||
`sent_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date d\'envoi',
|
||||
`edited_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Date de modification',
|
||||
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Message supprimé',
|
||||
KEY `idx_room_id` (`room_id`),
|
||||
KEY `idx_sender_id` (`sender_id`),
|
||||
KEY `idx_sent_at` (`sent_at`),
|
||||
KEY `idx_room_sent` (`room_id`, `sent_at`),
|
||||
CONSTRAINT `fk_chat_messages_room` FOREIGN KEY (`room_id`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_chat_messages_sender` FOREIGN KEY (`sender_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Messages du chat';
|
||||
|
||||
-- Table des participants
|
||||
CREATE TABLE IF NOT EXISTS `chat_participants` (
|
||||
`room_id` VARCHAR(36) NOT NULL COMMENT 'ID de la salle',
|
||||
`user_id` INT(11) UNSIGNED NOT NULL COMMENT 'ID de l\'utilisateur',
|
||||
`role` INT(11) DEFAULT NULL COMMENT 'Rôle de l\'utilisateur (fk_role)',
|
||||
`entite_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID de l\'entité',
|
||||
`joined_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date d\'adhésion',
|
||||
`left_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Date de départ',
|
||||
`is_admin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Admin de la salle',
|
||||
`last_read_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Dernière lecture',
|
||||
PRIMARY KEY (`room_id`, `user_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_entite_id` (`entite_id`),
|
||||
KEY `idx_joined_at` (`joined_at`),
|
||||
CONSTRAINT `fk_chat_participants_room` FOREIGN KEY (`room_id`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_chat_participants_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_chat_participants_entite` FOREIGN KEY (`entite_id`) REFERENCES `entites` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Participants aux conversations';
|
||||
|
||||
-- Table des accusés de lecture
|
||||
CREATE TABLE IF NOT EXISTS `chat_read_receipts` (
|
||||
`message_id` VARCHAR(36) NOT NULL COMMENT 'ID du message',
|
||||
`user_id` INT(11) UNSIGNED NOT NULL COMMENT 'ID de l\'utilisateur',
|
||||
`read_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de lecture',
|
||||
PRIMARY KEY (`message_id`, `user_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_read_at` (`read_at`),
|
||||
CONSTRAINT `fk_chat_read_message` FOREIGN KEY (`message_id`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_chat_read_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Accusés de lecture';
|
||||
|
||||
-- Index supplémentaires pour les performances
|
||||
CREATE INDEX idx_chat_active_rooms ON chat_rooms(is_active, created_at DESC);
|
||||
CREATE INDEX idx_chat_user_rooms ON chat_participants(user_id, left_at, joined_at DESC);
|
||||
CREATE INDEX idx_chat_unread ON chat_messages(room_id, sent_at) WHERE id NOT IN (SELECT message_id FROM chat_read_receipts);
|
||||
|
||||
-- Vue pour faciliter la récupération des conversations avec le dernier message
|
||||
CREATE OR REPLACE VIEW chat_rooms_with_last_message AS
|
||||
SELECT
|
||||
r.*,
|
||||
m.content as last_message_content,
|
||||
m.sender_id as last_message_sender,
|
||||
m.sent_at as last_message_at,
|
||||
u.encrypted_name as last_message_sender_name
|
||||
FROM chat_rooms r
|
||||
LEFT JOIN (
|
||||
SELECT m1.*
|
||||
FROM chat_messages m1
|
||||
INNER JOIN (
|
||||
SELECT room_id, MAX(sent_at) as max_sent_at
|
||||
FROM chat_messages
|
||||
WHERE is_deleted = 0
|
||||
GROUP BY room_id
|
||||
) m2 ON m1.room_id = m2.room_id AND m1.sent_at = m2.max_sent_at
|
||||
) m ON r.id = m.room_id
|
||||
LEFT JOIN users u ON m.sender_id = u.id
|
||||
WHERE r.is_active = 1;
|
||||
123
api/scripts/sql/create_security_tables.sql
Normal file
123
api/scripts/sql/create_security_tables.sql
Normal file
@@ -0,0 +1,123 @@
|
||||
-- Script de création des tables pour le module Security & Monitoring
|
||||
-- Date : 2025-01-17
|
||||
-- Version : 1.0
|
||||
-- Préfixe : sec_ (security)
|
||||
|
||||
-- ============================================
|
||||
-- SUPPRESSION DES TABLES EXISTANTES (OPTIONNEL)
|
||||
-- ============================================
|
||||
-- Décommenter si vous voulez recréer les tables
|
||||
|
||||
-- DROP TABLE IF EXISTS `sec_blocked_ips`;
|
||||
-- DROP TABLE IF EXISTS `sec_failed_login_attempts`;
|
||||
-- DROP TABLE IF EXISTS `sec_performance_metrics`;
|
||||
-- DROP TABLE IF EXISTS `sec_alerts`;
|
||||
|
||||
-- ============================================
|
||||
-- CRÉATION DES TABLES DE SÉCURITÉ ET MONITORING
|
||||
-- ============================================
|
||||
|
||||
-- Table principale des alertes de sécurité
|
||||
CREATE TABLE IF NOT EXISTS `sec_alerts` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`alert_type` VARCHAR(50) NOT NULL COMMENT 'Type d\'alerte (BRUTE_FORCE, SQL_ERROR, etc.)',
|
||||
`alert_level` ENUM('INFO', 'WARNING', 'ERROR', 'CRITICAL', 'SECURITY') NOT NULL DEFAULT 'INFO',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP source',
|
||||
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté ou utilisé',
|
||||
`endpoint` VARCHAR(255) DEFAULT NULL COMMENT 'Endpoint API concerné',
|
||||
`method` VARCHAR(10) DEFAULT NULL COMMENT 'Méthode HTTP',
|
||||
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels en JSON',
|
||||
`occurrences` INT(11) DEFAULT 1 COMMENT 'Nombre d\'occurrences',
|
||||
`first_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`email_sent` TINYINT(1) DEFAULT 0 COMMENT 'Email d\'alerte envoyé',
|
||||
`email_sent_at` TIMESTAMP NULL DEFAULT NULL,
|
||||
`resolved` TINYINT(1) DEFAULT 0 COMMENT 'Alerte résolue',
|
||||
`resolved_at` TIMESTAMP NULL DEFAULT NULL,
|
||||
`resolved_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID admin qui a résolu',
|
||||
`notes` TEXT DEFAULT NULL COMMENT 'Notes de résolution',
|
||||
KEY `idx_ip` (`ip_address`),
|
||||
KEY `idx_type_time` (`alert_type`, `last_seen`),
|
||||
KEY `idx_level` (`alert_level`),
|
||||
KEY `idx_resolved` (`resolved`),
|
||||
KEY `idx_user` (`user_id`),
|
||||
CONSTRAINT `fk_sec_alerts_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `fk_sec_alerts_resolver` FOREIGN KEY (`resolved_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Alertes de sécurité et monitoring';
|
||||
|
||||
-- Table des métriques de performance
|
||||
CREATE TABLE IF NOT EXISTS `sec_performance_metrics` (
|
||||
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`endpoint` VARCHAR(255) NOT NULL COMMENT 'Endpoint API',
|
||||
`method` VARCHAR(10) NOT NULL COMMENT 'Méthode HTTP',
|
||||
`response_time_ms` INT(11) NOT NULL COMMENT 'Temps de réponse total en ms',
|
||||
`db_time_ms` INT(11) DEFAULT 0 COMMENT 'Temps cumulé des requêtes DB en ms',
|
||||
`db_queries_count` INT(11) DEFAULT 0 COMMENT 'Nombre de requêtes DB',
|
||||
`memory_peak_mb` FLOAT DEFAULT NULL COMMENT 'Pic mémoire en MB',
|
||||
`memory_start_mb` FLOAT DEFAULT NULL COMMENT 'Mémoire au début en MB',
|
||||
`http_status` INT(11) DEFAULT NULL COMMENT 'Code HTTP de réponse',
|
||||
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP',
|
||||
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent complet',
|
||||
`request_size` INT(11) DEFAULT NULL COMMENT 'Taille de la requête en octets',
|
||||
`response_size` INT(11) DEFAULT NULL COMMENT 'Taille de la réponse en octets',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY `idx_endpoint_time` (`endpoint`, `created_at`),
|
||||
KEY `idx_response_time` (`response_time_ms`),
|
||||
KEY `idx_created` (`created_at`),
|
||||
KEY `idx_status` (`http_status`),
|
||||
KEY `idx_user` (`user_id`),
|
||||
KEY `idx_date_endpoint` (`created_at`, `endpoint`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Métriques de performance des requêtes';
|
||||
|
||||
-- Table des tentatives de login échouées
|
||||
CREATE TABLE IF NOT EXISTS `sec_failed_login_attempts` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté',
|
||||
`encrypted_username` VARCHAR(255) DEFAULT NULL COMMENT 'Username chiffré si trouvé',
|
||||
`ip_address` VARCHAR(45) NOT NULL COMMENT 'Adresse IP',
|
||||
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent',
|
||||
`attempt_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`error_type` VARCHAR(50) DEFAULT NULL COMMENT 'Type d\'erreur (invalid_password, user_not_found, etc.)',
|
||||
`country_code` VARCHAR(2) DEFAULT NULL COMMENT 'Code pays de l\'IP (si géoloc activée)',
|
||||
KEY `idx_ip_time` (`ip_address`, `attempt_time`),
|
||||
KEY `idx_username` (`username`),
|
||||
KEY `idx_encrypted_username` (`encrypted_username`),
|
||||
KEY `idx_time` (`attempt_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Tentatives de connexion échouées';
|
||||
|
||||
-- Table des IPs bloquées
|
||||
CREATE TABLE IF NOT EXISTS `sec_blocked_ips` (
|
||||
`ip_address` VARCHAR(45) NOT NULL PRIMARY KEY COMMENT 'Adresse IP bloquée',
|
||||
`reason` VARCHAR(255) NOT NULL COMMENT 'Raison du blocage',
|
||||
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels',
|
||||
`blocked_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`blocked_until` TIMESTAMP NOT NULL COMMENT 'Bloqué jusqu\'à',
|
||||
`blocked_by` VARCHAR(50) DEFAULT 'system' COMMENT 'Qui a bloqué (system ou user ID)',
|
||||
`permanent` TINYINT(1) DEFAULT 0 COMMENT 'Blocage permanent',
|
||||
`unblocked_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Date de déblocage effectif',
|
||||
`unblocked_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'Qui a débloqué',
|
||||
`block_count` INT(11) DEFAULT 1 COMMENT 'Nombre de fois bloquée',
|
||||
KEY `idx_blocked_until` (`blocked_until`),
|
||||
KEY `idx_permanent` (`permanent`),
|
||||
CONSTRAINT `fk_sec_blocked_unblocked_by` FOREIGN KEY (`unblocked_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IPs bloquées temporairement ou définitivement';
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- INDEX ADDITIONNELS POUR PERFORMANCES
|
||||
-- ============================================
|
||||
|
||||
-- Index pour requêtes de monitoring fréquentes
|
||||
CREATE INDEX idx_sec_metrics_recent ON sec_performance_metrics(created_at DESC, endpoint);
|
||||
CREATE INDEX idx_sec_alerts_recent ON sec_alerts(last_seen DESC, alert_level);
|
||||
CREATE INDEX idx_sec_failed_recent ON sec_failed_login_attempts(attempt_time DESC, ip_address);
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- FIN DU SCRIPT
|
||||
-- ============================================
|
||||
-- Note: La purge des données anciennes doit être gérée par:
|
||||
-- 1. Un cron qui appelle l'endpoint API /api/admin/cleanup
|
||||
-- 2. Ou directement via les méthodes cleanup des services PHP
|
||||
35
api/scripts/sql/migration_username_utf8_support.sql
Normal file
35
api/scripts/sql/migration_username_utf8_support.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- Migration pour supporter les usernames UTF-8 avec jusqu'à 30 caractères
|
||||
-- Date : 2025-01-17
|
||||
-- Objectif : Permettre des usernames plus souples (émojis, accents, espaces, etc.)
|
||||
|
||||
-- IMPORTANT : Faire une sauvegarde avant d'exécuter ce script !
|
||||
-- mysqldump -u root -p geo_app > backup_geo_app_$(date +%Y%m%d).sql
|
||||
|
||||
-- Augmenter la taille de la colonne encrypted_user_name pour supporter
|
||||
-- les usernames UTF-8 de 30 caractères maximum une fois chiffrés
|
||||
-- Un username de 30 caractères UTF-8 peut faire jusqu'à 120 octets
|
||||
-- Après chiffrement AES-256-CBC + base64, cela peut atteindre ~200 caractères
|
||||
|
||||
ALTER TABLE `users`
|
||||
MODIFY COLUMN `encrypted_user_name` varchar(255) DEFAULT ''
|
||||
COMMENT 'Username chiffré - Supporte UTF-8 30 caractères maximum';
|
||||
|
||||
-- Vérifier que la modification a bien été appliquée
|
||||
SELECT
|
||||
COLUMN_NAME,
|
||||
COLUMN_TYPE,
|
||||
CHARACTER_MAXIMUM_LENGTH,
|
||||
COLUMN_COMMENT
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'users'
|
||||
AND COLUMN_NAME = 'encrypted_user_name';
|
||||
|
||||
-- Note : Les nouvelles règles de validation des usernames sont :
|
||||
-- - Minimum : 8 caractères UTF-8
|
||||
-- - Maximum : 30 caractères UTF-8
|
||||
-- - Accepte TOUS les caractères (lettres, chiffres, espaces, émojis, accents, etc.)
|
||||
-- - Trim automatique des espaces en début/fin
|
||||
-- - Unicité vérifiée dans toute la base
|
||||
Reference in New Issue
Block a user