Compare commits
8 Commits
feature/3.
...
feature/ch
| Author | SHA1 | Date | |
|---|---|---|---|
| 604294af96 | |||
| 41a4505b4b | |||
| 6c8853e553 | |||
| 4c2e809a35 | |||
| 890da22329 | |||
| 5ab03751e1 | |||
| c1f23c4345 | |||
| 5e255ebf5e |
40
CHANGELOG-v3.1.6.md
Normal file
40
CHANGELOG-v3.1.6.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Changelog Version 3.1.6
|
||||||
|
|
||||||
|
## Date: 2025-08-21
|
||||||
|
|
||||||
|
### Améliorations des flux de passages
|
||||||
|
|
||||||
|
#### Interfaces utilisateur
|
||||||
|
- Optimisation de l'affichage des listes de passages
|
||||||
|
- Amélioration de l'ergonomie de navigation
|
||||||
|
- Ajout de filtres avancés pour la recherche de passages
|
||||||
|
- Mise à jour de l'interface responsive mobile
|
||||||
|
|
||||||
|
#### Flux de création
|
||||||
|
- Simplification du processus de création de passage
|
||||||
|
- Validation en temps réel des données saisies
|
||||||
|
- Ajout de modèles de passages prédéfinis
|
||||||
|
- Amélioration de la gestion des erreurs
|
||||||
|
|
||||||
|
#### Flux de consultation
|
||||||
|
- Affichage optimisé des détails de passage
|
||||||
|
- Historique complet des modifications
|
||||||
|
- Export des données en plusieurs formats
|
||||||
|
- Amélioration des performances de chargement
|
||||||
|
|
||||||
|
#### Flux de modification
|
||||||
|
- Interface de modification intuitive
|
||||||
|
- Suivi des changements avec comparaison avant/après
|
||||||
|
- Validation multi-niveaux des modifications
|
||||||
|
- Notifications automatiques des mises à jour
|
||||||
|
|
||||||
|
### Corrections de bugs
|
||||||
|
- Correction de l'affichage sur écrans de petite taille
|
||||||
|
- Résolution des problèmes de synchronisation
|
||||||
|
- Amélioration de la stabilité générale
|
||||||
|
|
||||||
|
### Améliorations techniques
|
||||||
|
- Optimisation des requêtes base de données
|
||||||
|
- Mise en cache des données fréquemment consultées
|
||||||
|
- Amélioration des temps de réponse API
|
||||||
|
- Refactoring du code pour une meilleure maintenabilité
|
||||||
204
PLANNING-STRIPE-ADMIN.md
Normal file
204
PLANNING-STRIPE-ADMIN.md
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
# PLANNING STRIPE - TÂCHES ADMINISTRATIVES
|
||||||
|
## Intégration Stripe Connect + Terminal pour Calendriers Pompiers
|
||||||
|
### Période : 25/08/2024 - 05/09/2024
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 LUNDI 25/08 - Préparation (4h)
|
||||||
|
|
||||||
|
### ✅ Compte Stripe Platform
|
||||||
|
- [ ] Créer compte Stripe sur https://dashboard.stripe.com/register
|
||||||
|
- [ ] Remplir informations entreprise (SIRET, adresse, etc.)
|
||||||
|
- [ ] Vérifier email de confirmation
|
||||||
|
- [ ] Activer l'authentification 2FA
|
||||||
|
|
||||||
|
### ✅ Activation des produits
|
||||||
|
- [ ] Activer Stripe Connect dans Dashboard → Products
|
||||||
|
- [ ] Choisir type "Express accounts" pour les amicales
|
||||||
|
- [ ] Activer Stripe Terminal dans Dashboard
|
||||||
|
- [ ] Demander accès "Tap to Pay on iPhone" via formulaire support
|
||||||
|
|
||||||
|
### ✅ Configuration initiale
|
||||||
|
- [ ] Définir les frais de plateforme (suggestion: 2.5% ou 0.50€ fixe)
|
||||||
|
- [ ] Configurer les paramètres de virement (J+2 recommandé)
|
||||||
|
- [ ] Ajouter logo et branding pour les pages Stripe
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 MARDI 26/08 - Setup environnements (2h)
|
||||||
|
|
||||||
|
### ✅ Clés API et Webhooks
|
||||||
|
- [ ] Récupérer clés TEST (pk_test_... et sk_test_...)
|
||||||
|
- [ ] Créer endpoint webhook : https://votreapi.com/webhooks/stripe
|
||||||
|
- [ ] Sélectionner événements webhook :
|
||||||
|
- `account.updated`
|
||||||
|
- `account.application.authorized`
|
||||||
|
- `payment_intent.succeeded`
|
||||||
|
- `payment_intent.payment_failed`
|
||||||
|
- `charge.dispute.created`
|
||||||
|
- [ ] Noter le Webhook signing secret (whsec_...)
|
||||||
|
|
||||||
|
### ✅ Documentation amicales
|
||||||
|
- [ ] Préparer template email pour amicales
|
||||||
|
- [ ] Créer guide PDF "Activer les paiements CB"
|
||||||
|
- [ ] Lister documents requis :
|
||||||
|
- Statuts association
|
||||||
|
- RIB avec IBAN/BIC
|
||||||
|
- Pièce identité responsable
|
||||||
|
- Justificatif adresse siège
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 MERCREDI 27/08 - Amicale pilote (3h)
|
||||||
|
|
||||||
|
### ✅ Onboarding première amicale
|
||||||
|
- [ ] Contacter amicale pilote
|
||||||
|
- [ ] Créer compte Connect Express via API ou Dashboard
|
||||||
|
- [ ] Envoyer lien onboarding à l'amicale
|
||||||
|
- [ ] Suivre progression dans Dashboard → Connect → Accounts
|
||||||
|
- [ ] Vérifier statut "Charges enabled"
|
||||||
|
|
||||||
|
### ✅ Configuration compte amicale
|
||||||
|
- [ ] Vérifier informations bancaires (IBAN)
|
||||||
|
- [ ] Configurer email notifications
|
||||||
|
- [ ] Tester micro-virement de vérification
|
||||||
|
- [ ] Noter le compte ID : acct_...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 JEUDI 28/08 - Tests paiements (2h)
|
||||||
|
|
||||||
|
### ✅ Configuration Terminal Test
|
||||||
|
- [ ] Créer "Location" test dans Dashboard → Terminal
|
||||||
|
- [ ] Générer reader test virtuel pour Simulator
|
||||||
|
- [ ] Configurer les montants de test (10€, 20€, 30€)
|
||||||
|
|
||||||
|
### ✅ Cartes de test
|
||||||
|
- [ ] Préparer liste cartes test :
|
||||||
|
- 4242 4242 4242 4242 : Succès
|
||||||
|
- 4000 0000 0000 9995 : Refus
|
||||||
|
- 4000 0025 0000 3155 : Authentification requise
|
||||||
|
- [ ] Documenter processus de test pour développeurs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 VENDREDI 29/08 - Compliance et sécurité (2h)
|
||||||
|
|
||||||
|
### ✅ Conformité légale
|
||||||
|
- [ ] Vérifier statut PCI DSS (auto-évaluation SAQ A)
|
||||||
|
- [ ] Préparer mentions légales paiement
|
||||||
|
- [ ] Créer template CGV pour paiements
|
||||||
|
- [ ] Documenter process RGPD
|
||||||
|
|
||||||
|
### ✅ Limites et sécurité
|
||||||
|
- [ ] Configurer limites de paiement (max 500€/transaction ?)
|
||||||
|
- [ ] Activer Radar (protection fraude) rules
|
||||||
|
- [ ] Configurer alertes email pour transactions > 100€
|
||||||
|
- [ ] Définir politique remboursements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 SAMEDI 30/08 - Monitoring (1h)
|
||||||
|
|
||||||
|
### ✅ Dashboard et alertes
|
||||||
|
- [ ] Créer vues personnalisées Dashboard
|
||||||
|
- [ ] Configurer alertes :
|
||||||
|
- Taux d'échec > 10%
|
||||||
|
- Nouveau litige (chargeback)
|
||||||
|
- Compte amicale suspendu
|
||||||
|
- [ ] Installer app mobile Stripe (iOS/Android)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 LUNDI 02/09 - Préparation production (3h)
|
||||||
|
|
||||||
|
### ✅ Amicales supplémentaires
|
||||||
|
- [ ] Onboarder 2-3 amicales test supplémentaires
|
||||||
|
- [ ] Vérifier leurs statuts de compte
|
||||||
|
- [ ] Former les responsables à l'interface Stripe
|
||||||
|
|
||||||
|
### ✅ Documentation finale
|
||||||
|
- [ ] Guide administrateur plateforme
|
||||||
|
- [ ] FAQ amicales (comment voir mes ventes ?)
|
||||||
|
- [ ] Process de support niveau 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 MARDI 03/09 - Tests grandeur nature (2h)
|
||||||
|
|
||||||
|
### ✅ Simulation production
|
||||||
|
- [ ] Paiement test avec vraie carte (sera remboursé)
|
||||||
|
- [ ] Vérifier apparition dans Dashboard amicale
|
||||||
|
- [ ] Tester virement vers compte bancaire
|
||||||
|
- [ ] Vérifier commissions plateforme
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 MERCREDI 04/09 - Go/No-Go Production (2h)
|
||||||
|
|
||||||
|
### ✅ Checklist production
|
||||||
|
- [ ] Obtenir clés PRODUCTION (pk_live_... et sk_live_...)
|
||||||
|
- [ ] ⚠️ JAMAIS les commiter dans le code
|
||||||
|
- [ ] Configurer webhook production
|
||||||
|
- [ ] Vérifier tous les comptes amicales "enabled"
|
||||||
|
- [ ] Backup des configurations
|
||||||
|
|
||||||
|
### ✅ Plan de bascule
|
||||||
|
- [ ] Planifier fenêtre de maintenance
|
||||||
|
- [ ] Préparer rollback si besoin
|
||||||
|
- [ ] Numéro hotline Stripe : +33 1 88 45 05 35
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 JEUDI 05/09 - Support jour J (4h)
|
||||||
|
|
||||||
|
### ✅ Surveillance active
|
||||||
|
- [ ] Monitoring Dashboard en temps réel
|
||||||
|
- [ ] Vérifier premiers paiements réels
|
||||||
|
- [ ] Support hotline pour amicales
|
||||||
|
- [ ] Documenter issues rencontrées
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 RÉCAPITULATIF TEMPS ADMIN
|
||||||
|
- **Préparation** : 4h
|
||||||
|
- **Configuration** : 7h
|
||||||
|
- **Tests** : 4h
|
||||||
|
- **Production** : 6h
|
||||||
|
- **TOTAL** : 21h sur 11 jours
|
||||||
|
|
||||||
|
## 🔐 INFORMATIONS SENSIBLES À STOCKER
|
||||||
|
|
||||||
|
```env
|
||||||
|
# JAMAIS dans le code source !
|
||||||
|
STRIPE_PUBLIC_KEY_TEST=pk_test_...
|
||||||
|
STRIPE_SECRET_KEY_TEST=sk_test_...
|
||||||
|
STRIPE_WEBHOOK_SECRET_TEST=whsec_...
|
||||||
|
|
||||||
|
STRIPE_PUBLIC_KEY_LIVE=pk_live_...
|
||||||
|
STRIPE_SECRET_KEY_LIVE=sk_live_...
|
||||||
|
STRIPE_WEBHOOK_SECRET_LIVE=whsec_...
|
||||||
|
|
||||||
|
STRIPE_PLATFORM_ACCOUNT_ID=acct_...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 CONTACTS UTILES
|
||||||
|
|
||||||
|
- **Support Stripe France** : +33 1 88 45 05 35
|
||||||
|
- **Email support** : support@stripe.com
|
||||||
|
- **Dashboard** : https://dashboard.stripe.com
|
||||||
|
- **Statut système** : https://status.stripe.com
|
||||||
|
|
||||||
|
## ⚠️ POINTS D'ATTENTION CRITIQUES
|
||||||
|
|
||||||
|
1. **NE JAMAIS** partager les clés secrètes (sk_)
|
||||||
|
2. **TOUJOURS** commencer en mode TEST
|
||||||
|
3. **VÉRIFIER** 2x avant passage en LIVE
|
||||||
|
4. Les virements vers comptes amicales prennent 2-7 jours
|
||||||
|
5. Garder 1 personne dispo pour support le jour J
|
||||||
|
6. **Android Tap to Pay** : Vérifier certification SoftPOS des appareils
|
||||||
|
7. **Maintenir** liste des modèles Android certifiés à jour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document créé le 24/08/2024 - À tenir à jour quotidiennement*
|
||||||
117
api/TODO-API.md
Normal file
117
api/TODO-API.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# TODO-API.md
|
||||||
|
|
||||||
|
## 📋 Liste des tâches à implémenter
|
||||||
|
|
||||||
|
### 🔴 PRIORITÉ HAUTE
|
||||||
|
|
||||||
|
#### 1. Système de backup pour les suppressions (DELETE)
|
||||||
|
**Demandé le :** 20/08/2025
|
||||||
|
**Objectif :** Sauvegarder toutes les données supprimées (soft delete) dans un fichier SQL pour pouvoir les restaurer en cas d'erreur humaine.
|
||||||
|
|
||||||
|
**Détails techniques :**
|
||||||
|
- Créer un système de backup automatique lors de chaque DELETE
|
||||||
|
- Stocker les données dans un fichier SQL avec structure permettant la réintégration facile
|
||||||
|
- Format suggéré : `/backups/deleted/{année}/{mois}/deleted_{table}_{YYYYMMDD}.sql`
|
||||||
|
|
||||||
|
**Tables concernées :**
|
||||||
|
- `ope_pass` (passages) - DELETE /passages/{id}
|
||||||
|
- `users` (utilisateurs) - DELETE /users/{id}
|
||||||
|
- `operations` (opérations) - DELETE /operations/{id}
|
||||||
|
- `ope_sectors` (secteurs) - DELETE /sectors/{id}
|
||||||
|
|
||||||
|
**Structure du backup suggérée :**
|
||||||
|
```sql
|
||||||
|
-- Backup deletion: ope_pass
|
||||||
|
-- Date: 2025-08-20 14:30:45
|
||||||
|
-- User: 9999985 (cv_mobile)
|
||||||
|
-- Entity: 5
|
||||||
|
-- Original ID: 19500576
|
||||||
|
|
||||||
|
INSERT INTO ope_pass_backup (
|
||||||
|
original_id,
|
||||||
|
deleted_at,
|
||||||
|
deleted_by_user_id,
|
||||||
|
deleted_by_entity_id,
|
||||||
|
-- tous les champs originaux
|
||||||
|
fk_operation,
|
||||||
|
fk_sector,
|
||||||
|
fk_user,
|
||||||
|
montant,
|
||||||
|
encrypted_name,
|
||||||
|
encrypted_email,
|
||||||
|
-- etc...
|
||||||
|
) VALUES (
|
||||||
|
19500576,
|
||||||
|
'2025-08-20 14:30:45',
|
||||||
|
9999985,
|
||||||
|
5,
|
||||||
|
-- valeurs originales
|
||||||
|
...
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Pour restauration facile :
|
||||||
|
-- UPDATE ope_pass SET chk_active = 1 WHERE id = 19500576;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fonctionnalités à implémenter :**
|
||||||
|
1. **Service de backup** : `BackupService.php`
|
||||||
|
- Méthode `backupDeletedRecord($table, $id, $data)`
|
||||||
|
- Génération automatique du SQL de restauration
|
||||||
|
- Rotation des fichiers (garder 90 jours)
|
||||||
|
|
||||||
|
2. **Intégration dans les controllers**
|
||||||
|
- Ajouter l'appel au BackupService avant chaque soft delete
|
||||||
|
- Logger l'emplacement du backup
|
||||||
|
|
||||||
|
3. **Interface de restauration** (optionnel)
|
||||||
|
- Endpoint GET /api/backups/deleted pour lister les backups
|
||||||
|
- Endpoint POST /api/backups/restore/{backup_id} pour restaurer
|
||||||
|
|
||||||
|
4. **Commande de restauration manuelle**
|
||||||
|
- Script PHP : `php scripts/restore_deleted.php --table=ope_pass --id=19500576`
|
||||||
|
|
||||||
|
**Avantages :**
|
||||||
|
- Traçabilité complète des suppressions
|
||||||
|
- Restauration rapide en cas d'erreur
|
||||||
|
- Audit trail pour conformité
|
||||||
|
- Tranquillité d'esprit pour le client
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟡 PRIORITÉ MOYENNE
|
||||||
|
|
||||||
|
#### 2. Amélioration des logs
|
||||||
|
- Ajouter plus de contexte dans les logs
|
||||||
|
- Rotation automatique des logs
|
||||||
|
- Dashboard de monitoring
|
||||||
|
|
||||||
|
#### 3. Optimisation des performances
|
||||||
|
- Cache des requêtes fréquentes
|
||||||
|
- Index sur les tables volumineuses
|
||||||
|
- Pagination optimisée
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟢 PRIORITÉ BASSE
|
||||||
|
|
||||||
|
#### 4. Documentation API
|
||||||
|
- Génération automatique OpenAPI/Swagger
|
||||||
|
- Documentation interactive
|
||||||
|
- Exemples de code pour chaque endpoint
|
||||||
|
|
||||||
|
#### 5. Tests automatisés
|
||||||
|
- Tests unitaires pour les services critiques
|
||||||
|
- Tests d'intégration pour les endpoints
|
||||||
|
- Tests de charge
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- Les tâches marquées 🔴 doivent être traitées en priorité
|
||||||
|
- Chaque tâche implémentée doit être documentée
|
||||||
|
- Prévoir des tests pour chaque nouvelle fonctionnalité
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 20/08/2025
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"phpmailer/phpmailer": "^6.8",
|
"phpmailer/phpmailer": "^6.8",
|
||||||
"phpoffice/phpspreadsheet": "^2.0"
|
"phpoffice/phpspreadsheet": "^2.0",
|
||||||
|
"setasign/fpdf": "^1.8"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
|
|||||||
@@ -141,6 +141,11 @@ $SSH_JUMP_CMD "
|
|||||||
incus exec ${INCUS_CONTAINER} -- chmod -R 775 ${FINAL_PATH}/uploads || exit 1
|
incus exec ${INCUS_CONTAINER} -- chmod -R 775 ${FINAL_PATH}/uploads || exit 1
|
||||||
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH}/uploads -type f -exec chmod -R 664 {} \; || exit 1
|
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH}/uploads -type f -exec chmod -R 664 {} \; || exit 1
|
||||||
|
|
||||||
|
echo '📦 Mise à jour des dépendances Composer...'
|
||||||
|
incus exec ${INCUS_CONTAINER} -- bash -c 'cd ${FINAL_PATH} && composer update --no-dev --optimize-autoloader' || {
|
||||||
|
echo '⚠️ Composer non disponible ou échec, poursuite sans mise à jour des dépendances'
|
||||||
|
}
|
||||||
|
|
||||||
echo '🧹 Nettoyage...'
|
echo '🧹 Nettoyage...'
|
||||||
incus exec ${INCUS_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
incus exec ${INCUS_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
||||||
rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
||||||
|
|||||||
334
api/docs/API-SECURITY.md
Normal file
334
api/docs/API-SECURITY.md
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# API Security & Performance Monitoring
|
||||||
|
|
||||||
|
## 📋 Vue d'ensemble
|
||||||
|
|
||||||
|
Système complet de sécurité et monitoring pour l'API GeoSector implémenté et opérationnel.
|
||||||
|
|
||||||
|
### ✅ Fonctionnalités implémentées
|
||||||
|
|
||||||
|
- **Détection d'intrusions** : Brute force, SQL injection, patterns de scan
|
||||||
|
- **Monitoring des performances** : Temps de réponse, utilisation mémoire, requêtes DB
|
||||||
|
- **Alertes email intelligentes** : Throttling, niveaux de priorité
|
||||||
|
- **Blocage d'IP automatique** : Temporaire ou permanent
|
||||||
|
- **Traçabilité complète** : Historique pour audit et analyse
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Tables de base de données (préfixe `sec_`)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 4 tables créées dans scripts/sql/create_security_tables.sql
|
||||||
|
sec_alerts -- Alertes de sécurité
|
||||||
|
sec_performance_metrics -- Métriques de performance
|
||||||
|
sec_failed_login_attempts -- Tentatives de connexion échouées
|
||||||
|
sec_blocked_ips -- IPs bloquées
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services PHP implémentés
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Services/Security/
|
||||||
|
├── AlertService.php # Gestion centralisée des alertes
|
||||||
|
├── EmailThrottler.php # Anti-spam pour emails
|
||||||
|
├── SecurityMonitor.php # Détection des menaces
|
||||||
|
├── PerformanceMonitor.php # Monitoring des temps
|
||||||
|
└── IPBlocker.php # Gestion des blocages IP
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contrôleur d'administration
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Controllers/SecurityController.php # Interface d'administration
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### 1. Créer les tables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Exécuter le script SQL sur chaque environnement
|
||||||
|
mysql -u root -p geo_app < scripts/sql/create_security_tables.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurer le cron de purge
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ajouter dans crontab (crontab -e)
|
||||||
|
0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php >> /var/log/security_cleanup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Tester l'installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php test_security.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Fonctionnement
|
||||||
|
|
||||||
|
### Détection automatique
|
||||||
|
|
||||||
|
Le système détecte et bloque automatiquement :
|
||||||
|
|
||||||
|
- **Brute force** : 5 tentatives échouées en 5 minutes → IP bloquée 1h
|
||||||
|
- **SQL injection** : Patterns suspects → IP bloquée définitivement
|
||||||
|
- **Scan de vulnérabilités** : Accès aux fichiers sensibles → IP bloquée 1h
|
||||||
|
- **Rate limiting** : Plus de 60 requêtes/minute → Rejet temporaire
|
||||||
|
|
||||||
|
### Monitoring de performance
|
||||||
|
|
||||||
|
Chaque requête est automatiquement monitorée :
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Dans index.php
|
||||||
|
PerformanceMonitor::startRequest();
|
||||||
|
// ... traitement ...
|
||||||
|
PerformanceMonitor::endRequest($endpoint, $method, $statusCode);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alertes email
|
||||||
|
|
||||||
|
Configuration des niveaux :
|
||||||
|
|
||||||
|
- **INFO** : Log uniquement
|
||||||
|
- **WARNING** : Email avec throttling 1h
|
||||||
|
- **ERROR** : Email avec throttling 15min
|
||||||
|
- **CRITICAL** : Email avec throttling 5min
|
||||||
|
- **SECURITY** : Email immédiat, priorité haute
|
||||||
|
|
||||||
|
## 📊 Endpoints d'administration
|
||||||
|
|
||||||
|
Tous les endpoints nécessitent une authentification admin (role >= 2) :
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/admin/metrics # Métriques de performance
|
||||||
|
GET /api/admin/alerts # Alertes actives
|
||||||
|
POST /api/admin/alerts/:id/resolve # Résoudre une alerte
|
||||||
|
GET /api/admin/blocked-ips # IPs bloquées
|
||||||
|
POST /api/admin/unblock-ip # Débloquer une IP
|
||||||
|
POST /api/admin/block-ip # Bloquer une IP manuellement
|
||||||
|
GET /api/admin/security-report # Rapport complet
|
||||||
|
POST /api/admin/cleanup # Nettoyer les anciennes données
|
||||||
|
POST /api/admin/test-alert # Tester les alertes
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Seuils par défaut (modifiables dans les services)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// PerformanceMonitor.php
|
||||||
|
const DEFAULT_THRESHOLDS = [
|
||||||
|
'response_time_warning' => 1000, // 1 seconde
|
||||||
|
'response_time_critical' => 3000, // 3 secondes
|
||||||
|
'db_time_warning' => 500, // 500ms
|
||||||
|
'db_time_critical' => 1000, // 1 seconde
|
||||||
|
'memory_warning' => 64, // 64 MB
|
||||||
|
'memory_critical' => 128 // 128 MB
|
||||||
|
];
|
||||||
|
|
||||||
|
// SecurityMonitor.php
|
||||||
|
- Brute force : 5 tentatives en 5 minutes
|
||||||
|
- Rate limit : 60 requêtes par minute
|
||||||
|
- 404 pattern : 10 erreurs 404 en 10 minutes
|
||||||
|
|
||||||
|
// EmailThrottler.php
|
||||||
|
const DEFAULT_CONFIG = [
|
||||||
|
'max_per_hour' => 10,
|
||||||
|
'max_per_day' => 50,
|
||||||
|
'digest_after' => 5,
|
||||||
|
'cooldown_minutes' => 60
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rétention des données
|
||||||
|
|
||||||
|
Configurée dans `scripts/cron/cleanup_security_data.php` :
|
||||||
|
|
||||||
|
```php
|
||||||
|
$RETENTION_DAYS = [
|
||||||
|
'performance_metrics' => 30, // 30 jours
|
||||||
|
'failed_login_attempts' => 7, // 7 jours
|
||||||
|
'resolved_alerts' => 90, // 90 jours
|
||||||
|
'expired_blocks' => 0 // Déblocage immédiat
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 Métriques surveillées
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Temps de réponse total
|
||||||
|
- Temps cumulé des requêtes DB
|
||||||
|
- Nombre de requêtes DB
|
||||||
|
- Utilisation mémoire (pic et moyenne)
|
||||||
|
- Codes HTTP de réponse
|
||||||
|
|
||||||
|
### Sécurité
|
||||||
|
- Tentatives de connexion échouées
|
||||||
|
- IPs bloquées (temporaires/permanentes)
|
||||||
|
- Patterns d'attaque détectés
|
||||||
|
- Alertes par type et niveau
|
||||||
|
|
||||||
|
## 🛡️ Patterns de détection
|
||||||
|
|
||||||
|
### SQL Injection
|
||||||
|
```php
|
||||||
|
// Patterns détectés dans SecurityMonitor.php
|
||||||
|
- UNION SELECT
|
||||||
|
- DROP TABLE
|
||||||
|
- INSERT INTO
|
||||||
|
- UPDATE SET
|
||||||
|
- DELETE FROM
|
||||||
|
- Script tags
|
||||||
|
- OR 1=1
|
||||||
|
- Commentaires SQL (--)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fichiers sensibles
|
||||||
|
```php
|
||||||
|
// Patterns de scan détectés
|
||||||
|
- admin, administrator
|
||||||
|
- wp-admin, phpmyadmin
|
||||||
|
- .git, .env
|
||||||
|
- config.php
|
||||||
|
- backup, .sql, .zip
|
||||||
|
- shell.php, eval.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Exemples d'utilisation
|
||||||
|
|
||||||
|
### Déclencher une alerte manuelle
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\Security\AlertService;
|
||||||
|
|
||||||
|
AlertService::trigger('CUSTOM_ALERT', [
|
||||||
|
'message' => 'Événement important détecté',
|
||||||
|
'details' => ['user' => $userId, 'action' => $action]
|
||||||
|
], 'WARNING');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bloquer une IP manuellement
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\Security\IPBlocker;
|
||||||
|
|
||||||
|
// Blocage temporaire (1 heure)
|
||||||
|
IPBlocker::block('192.168.1.100', 3600, 'Comportement suspect');
|
||||||
|
|
||||||
|
// Blocage permanent
|
||||||
|
IPBlocker::blockPermanent('192.168.1.100', 'Attaque confirmée');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Obtenir les statistiques
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
use App\Services\Security\PerformanceMonitor;
|
||||||
|
|
||||||
|
$securityStats = SecurityMonitor::getSecurityStats();
|
||||||
|
$perfStats = PerformanceMonitor::getStats(null, 24); // 24h
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Points d'attention
|
||||||
|
|
||||||
|
### RGPD
|
||||||
|
- Les IPs sont des données personnelles
|
||||||
|
- Durée de conservation limitée (voir rétention)
|
||||||
|
- Anonymisation après traitement
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Overhead < 5ms par requête
|
||||||
|
- Optimisation des tables avec index
|
||||||
|
- Purge automatique des anciennes données
|
||||||
|
|
||||||
|
### Sécurité
|
||||||
|
- Pas d'exposition de données sensibles dans les alertes
|
||||||
|
- Chiffrement des données utilisateur
|
||||||
|
- Whitelist pour IPs de confiance (localhost)
|
||||||
|
|
||||||
|
## 🔄 Maintenance
|
||||||
|
|
||||||
|
### Quotidienne (cron)
|
||||||
|
```bash
|
||||||
|
# Purge automatique à 2h du matin
|
||||||
|
0 2 * * * php /var/www/geosector/api/scripts/cron/cleanup_security_data.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hebdomadaire
|
||||||
|
- Vérifier les alertes actives
|
||||||
|
- Analyser les tendances de performance
|
||||||
|
- Ajuster les seuils si nécessaire
|
||||||
|
|
||||||
|
### Mensuelle
|
||||||
|
- Analyser le rapport de sécurité
|
||||||
|
- Mettre à jour les IPs whitelist/blacklist
|
||||||
|
- Optimiser les tables si nécessaire
|
||||||
|
|
||||||
|
## 🐛 Dépannage
|
||||||
|
|
||||||
|
### Les tables n'existent pas
|
||||||
|
```bash
|
||||||
|
# Créer les tables
|
||||||
|
mysql -u root -p geo_app < scripts/sql/create_security_tables.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pas d'alertes email
|
||||||
|
- Vérifier la configuration email dans `AppConfig`
|
||||||
|
- Vérifier les logs : `tail -f logs/geosector-*.log`
|
||||||
|
- Tester avec : `POST /api/admin/test-alert`
|
||||||
|
|
||||||
|
### IP bloquée par erreur
|
||||||
|
```bash
|
||||||
|
# Via API
|
||||||
|
curl -X POST https://dapp.geosector.fr/api/admin/unblock-ip \
|
||||||
|
-H "Authorization: Bearer TOKEN" \
|
||||||
|
-d '{"ip": "192.168.1.100"}'
|
||||||
|
|
||||||
|
# Via MySQL
|
||||||
|
UPDATE sec_blocked_ips SET unblocked_at = NOW() WHERE ip_address = '192.168.1.100';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Ressources
|
||||||
|
|
||||||
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||||
|
- [PHP Security Best Practices](https://www.php.net/manual/en/security.php)
|
||||||
|
- Code source : `/src/Services/Security/`
|
||||||
|
- Tests : `test_security.php`
|
||||||
|
- Logs : `/logs/geosector-*.log`
|
||||||
|
|
||||||
|
## 🎯 Statut d'implémentation
|
||||||
|
|
||||||
|
✅ **Phase 1** : Infrastructure de base - COMPLÉTÉ
|
||||||
|
- Tables créées avec préfixe `sec_`
|
||||||
|
- Services PHP implémentés
|
||||||
|
- Intégration dans index.php et Database.php
|
||||||
|
|
||||||
|
✅ **Phase 2** : Monitoring de Performance - COMPLÉTÉ
|
||||||
|
- Chronométrage automatique des requêtes
|
||||||
|
- Monitoring des requêtes DB
|
||||||
|
- Alertes sur dégradation
|
||||||
|
|
||||||
|
✅ **Phase 3** : Détection d'intrusions - COMPLÉTÉ
|
||||||
|
- Détection brute force
|
||||||
|
- Détection SQL injection
|
||||||
|
- Blocage IP automatique
|
||||||
|
|
||||||
|
✅ **Phase 4** : Alertes Email - COMPLÉTÉ
|
||||||
|
- Service d'alertes avec throttling
|
||||||
|
- Templates d'emails
|
||||||
|
- Niveaux de priorité
|
||||||
|
|
||||||
|
✅ **Phase 5** : Administration - COMPLÉTÉ
|
||||||
|
- Endpoints d'administration
|
||||||
|
- Interface de gestion
|
||||||
|
- Rapports de sécurité
|
||||||
|
|
||||||
|
✅ **Phase 6** : Maintenance - COMPLÉTÉ
|
||||||
|
- Script de purge automatique
|
||||||
|
- Optimisation des tables
|
||||||
|
- Documentation complète
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour : 2025-01-17*
|
||||||
|
*Version : 1.0.0*
|
||||||
419
api/docs/CHAT_MODULE.md
Normal file
419
api/docs/CHAT_MODULE.md
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
# Module Chat - Documentation API
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Le module Chat permet aux utilisateurs de l'application GeoSector de communiquer entre eux via une messagerie intégrée. Il supporte les conversations privées, de groupe et les diffusions (broadcast).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Tables de base de données
|
||||||
|
|
||||||
|
- `chat_rooms` : Salles de conversation
|
||||||
|
- `chat_messages` : Messages échangés
|
||||||
|
- `chat_participants` : Participants aux conversations
|
||||||
|
- `chat_read_receipts` : Accusés de lecture
|
||||||
|
|
||||||
|
### Permissions par rôle
|
||||||
|
|
||||||
|
| Rôle | Permissions |
|
||||||
|
|------|------------|
|
||||||
|
| **1 - Utilisateur** | Conversations privées et groupes avec membres de son entité |
|
||||||
|
| **2 - Admin entité** | Toutes conversations de son entité + création de diffusions |
|
||||||
|
| **> 2 - Super admin** | Accès total à toutes les conversations |
|
||||||
|
|
||||||
|
## Flux d'utilisation du module Chat
|
||||||
|
|
||||||
|
### 📱 Vue d'ensemble du flux
|
||||||
|
|
||||||
|
Le module Chat fonctionne en mode **chargement dynamique** : les données sont récupérées à la demande, pas toutes en une fois au login.
|
||||||
|
|
||||||
|
### 1. Au login (`/api/login`)
|
||||||
|
|
||||||
|
La réponse du login contient un objet `chat` avec les informations de base :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"user": {...},
|
||||||
|
"amicale": {...},
|
||||||
|
"chat": {
|
||||||
|
"total_rooms": 5, // Nombre total de conversations
|
||||||
|
"unread_messages": 12, // Total messages non lus
|
||||||
|
"chat_enabled": true, // Module activé pour cet utilisateur
|
||||||
|
"last_active_room": { // Dernière conversation active
|
||||||
|
"id": "uuid-room-123",
|
||||||
|
"title": "Discussion équipe",
|
||||||
|
"type": "group",
|
||||||
|
"last_message": "À demain !",
|
||||||
|
"last_message_at": "2025-01-17 18:30:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Permet d'afficher un **badge de notification** et de savoir si le chat est disponible
|
||||||
|
|
||||||
|
### 2. Ouverture de la page Chat
|
||||||
|
|
||||||
|
#### Étape 1 : Chargement initial
|
||||||
|
```
|
||||||
|
GET /api/chat/rooms
|
||||||
|
```
|
||||||
|
→ Récupère la liste des conversations avec aperçu du dernier message
|
||||||
|
|
||||||
|
#### Étape 2 : Sélection d'une conversation
|
||||||
|
```
|
||||||
|
GET /api/chat/rooms/{room_id}/messages?limit=50
|
||||||
|
```
|
||||||
|
→ Charge les 50 derniers messages (pagination disponible)
|
||||||
|
|
||||||
|
#### Étape 3 : Marquage comme lu
|
||||||
|
```
|
||||||
|
POST /api/chat/rooms/{room_id}/read
|
||||||
|
```
|
||||||
|
→ Met à jour les compteurs de messages non lus
|
||||||
|
|
||||||
|
### 3. Actions utilisateur
|
||||||
|
|
||||||
|
| Action | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| **Envoyer un message** | `POST /api/chat/rooms/{id}/messages` | Envoie et retourne le message créé |
|
||||||
|
| **Créer une conversation** | `POST /api/chat/rooms` | Crée une nouvelle room |
|
||||||
|
| **Obtenir les destinataires** | `GET /api/chat/recipients` | Liste des contacts disponibles |
|
||||||
|
| **Charger plus de messages** | `GET /api/chat/rooms/{id}/messages?before={msg_id}` | Pagination |
|
||||||
|
|
||||||
|
### 4. Stratégies de rafraîchissement
|
||||||
|
|
||||||
|
#### Polling (recommandé pour débuter)
|
||||||
|
- Rafraîchir `/api/chat/rooms` toutes les 30 secondes
|
||||||
|
- Rafraîchir les messages de la conversation active toutes les 10 secondes
|
||||||
|
|
||||||
|
#### Pull to refresh
|
||||||
|
- Permettre à l'utilisateur de rafraîchir manuellement
|
||||||
|
|
||||||
|
#### Lifecycle events
|
||||||
|
- Recharger quand l'app revient au premier plan
|
||||||
|
- Rafraîchir après envoi d'un message
|
||||||
|
|
||||||
|
### 5. Exemple d'implémentation Flutter
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class ChatService {
|
||||||
|
Timer? _roomsTimer;
|
||||||
|
Timer? _messagesTimer;
|
||||||
|
|
||||||
|
// 1. Au login, stocker les infos de base
|
||||||
|
void initFromLogin(Map<String, dynamic> chatData) {
|
||||||
|
_unreadCount = chatData['unread_messages'];
|
||||||
|
_chatEnabled = chatData['chat_enabled'];
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. À l'ouverture du chat
|
||||||
|
Future<void> openChatPage() async {
|
||||||
|
// Charger les conversations
|
||||||
|
final rooms = await api.get('/api/chat/rooms');
|
||||||
|
_rooms = rooms['rooms'];
|
||||||
|
|
||||||
|
// Démarrer le polling
|
||||||
|
_startPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Sélection d'une conversation
|
||||||
|
Future<void> selectRoom(String roomId) async {
|
||||||
|
// Charger les messages
|
||||||
|
final response = await api.get('/api/chat/rooms/$roomId/messages');
|
||||||
|
_currentMessages = response['messages'];
|
||||||
|
|
||||||
|
// Marquer comme lu
|
||||||
|
await api.post('/api/chat/rooms/$roomId/read');
|
||||||
|
|
||||||
|
// Rafraîchir plus fréquemment cette conversation
|
||||||
|
_startMessagePolling(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Polling automatique
|
||||||
|
void _startPolling() {
|
||||||
|
_roomsTimer = Timer.periodic(Duration(seconds: 30), (_) {
|
||||||
|
_refreshRooms();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Nettoyage
|
||||||
|
void dispose() {
|
||||||
|
_roomsTimer?.cancel();
|
||||||
|
_messagesTimer?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints API
|
||||||
|
|
||||||
|
### 1. GET /api/chat/rooms
|
||||||
|
**Description** : Récupère la liste des conversations de l'utilisateur
|
||||||
|
|
||||||
|
**Réponse** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"rooms": [
|
||||||
|
{
|
||||||
|
"id": "uuid-room-1",
|
||||||
|
"title": "Discussion équipe",
|
||||||
|
"type": "group",
|
||||||
|
"created_at": "2025-01-17 10:00:00",
|
||||||
|
"created_by": 123,
|
||||||
|
"updated_at": "2025-01-17 14:30:00",
|
||||||
|
"last_message": "Bonjour tout le monde",
|
||||||
|
"last_message_at": "2025-01-17 14:30:00",
|
||||||
|
"unread_count": 3,
|
||||||
|
"participant_count": 5,
|
||||||
|
"participants": [
|
||||||
|
{
|
||||||
|
"user_id": 123,
|
||||||
|
"name": "Jean Dupont",
|
||||||
|
"first_name": "Jean",
|
||||||
|
"is_admin": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. POST /api/chat/rooms
|
||||||
|
**Description** : Crée une nouvelle conversation
|
||||||
|
|
||||||
|
**Body** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "private|group|broadcast",
|
||||||
|
"title": "Titre optionnel (requis pour group/broadcast)",
|
||||||
|
"participants": [456, 789], // IDs des participants
|
||||||
|
"initial_message": "Message initial optionnel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Règles** :
|
||||||
|
- `private` : Maximum 2 participants (incluant le créateur)
|
||||||
|
- `group` : Plusieurs participants possibles
|
||||||
|
- `broadcast` : Réservé aux admins (rôle >= 2)
|
||||||
|
|
||||||
|
**Réponse** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"room": {
|
||||||
|
"id": "uuid-new-room",
|
||||||
|
"title": "Nouvelle conversation",
|
||||||
|
"type": "group",
|
||||||
|
"created_at": "2025-01-17 15:00:00",
|
||||||
|
"participants": [...]
|
||||||
|
},
|
||||||
|
"existing": false // true si conversation privée existante trouvée
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. GET /api/chat/rooms/{id}/messages
|
||||||
|
**Description** : Récupère les messages d'une conversation
|
||||||
|
|
||||||
|
**Paramètres** :
|
||||||
|
- `limit` : Nombre de messages (défaut: 50, max: 100)
|
||||||
|
- `before` : ID du message pour pagination
|
||||||
|
|
||||||
|
**Réponse** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"id": "uuid-message-1",
|
||||||
|
"content": "Bonjour !",
|
||||||
|
"sender_id": 123,
|
||||||
|
"sender_name": "Jean Dupont",
|
||||||
|
"sender_first_name": "Jean",
|
||||||
|
"sent_at": "2025-01-17 14:00:00",
|
||||||
|
"edited_at": null,
|
||||||
|
"is_deleted": false,
|
||||||
|
"is_read": true,
|
||||||
|
"is_mine": false,
|
||||||
|
"read_count": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_more": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. POST /api/chat/rooms/{id}/messages
|
||||||
|
**Description** : Envoie un message dans une conversation
|
||||||
|
|
||||||
|
**Body** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "Contenu du message (max 5000 caractères)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": {
|
||||||
|
"id": "uuid-new-message",
|
||||||
|
"content": "Message envoyé",
|
||||||
|
"sender_id": 123,
|
||||||
|
"sender_name": "Jean Dupont",
|
||||||
|
"sent_at": "2025-01-17 15:30:00",
|
||||||
|
"is_mine": true,
|
||||||
|
"is_read": false,
|
||||||
|
"read_count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. POST /api/chat/rooms/{id}/read
|
||||||
|
**Description** : Marque les messages comme lus
|
||||||
|
|
||||||
|
**Body (optionnel)** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_ids": ["uuid-1", "uuid-2"] // Si omis, marque tous les messages
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"unread_count": 0 // Nombre de messages non lus restants
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. GET /api/chat/recipients
|
||||||
|
**Description** : Liste des destinataires possibles pour créer une conversation
|
||||||
|
|
||||||
|
**Réponse** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"recipients": [
|
||||||
|
{
|
||||||
|
"id": 456,
|
||||||
|
"name": "Marie Martin",
|
||||||
|
"first_name": "Marie",
|
||||||
|
"role": 1,
|
||||||
|
"entite_id": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recipients_by_entity": {
|
||||||
|
"Amicale de Grenoble": [
|
||||||
|
{...}
|
||||||
|
],
|
||||||
|
"Amicale de Lyon": [
|
||||||
|
{...}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fonctionnalités clés
|
||||||
|
|
||||||
|
### 1. Types de conversations
|
||||||
|
|
||||||
|
#### Private (Conversation privée)
|
||||||
|
- Entre 2 utilisateurs uniquement
|
||||||
|
- Détection automatique de conversation existante
|
||||||
|
- Pas de titre requis
|
||||||
|
|
||||||
|
#### Group (Groupe)
|
||||||
|
- Plusieurs participants
|
||||||
|
- Titre optionnel mais recommandé
|
||||||
|
- Admin de groupe (créateur)
|
||||||
|
|
||||||
|
#### Broadcast (Diffusion)
|
||||||
|
- Réservé aux admins (rôle >= 2)
|
||||||
|
- Communication unidirectionnelle possible
|
||||||
|
- Pour annonces importantes
|
||||||
|
|
||||||
|
### 2. Gestion des permissions
|
||||||
|
|
||||||
|
Le système vérifie automatiquement :
|
||||||
|
- L'appartenance à une conversation avant lecture/écriture
|
||||||
|
- Les droits de création selon le type de conversation
|
||||||
|
- La visibilité des destinataires selon le rôle
|
||||||
|
|
||||||
|
### 3. Statuts de lecture
|
||||||
|
|
||||||
|
- **Accusés de lecture individuels** : Chaque message peut être marqué comme lu
|
||||||
|
- **Compteur de non-lus** : Par conversation et global
|
||||||
|
- **Last read** : Timestamp de dernière lecture par participant
|
||||||
|
|
||||||
|
### 4. Optimisations
|
||||||
|
|
||||||
|
- **Pagination** : Chargement progressif des messages
|
||||||
|
- **Index optimisés** : Pour les requêtes fréquentes
|
||||||
|
- **Vue SQL** : Pour récupération rapide du dernier message
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Chiffrement
|
||||||
|
- Les noms d'utilisateurs sont stockés chiffrés (AES-256)
|
||||||
|
- Déchiffrement à la volée lors de la lecture
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
- Longueur maximale des messages : 5000 caractères
|
||||||
|
- Trim automatique du contenu
|
||||||
|
- Vérification des permissions à chaque action
|
||||||
|
|
||||||
|
### Isolation
|
||||||
|
- Les utilisateurs ne voient que leurs conversations autorisées
|
||||||
|
- Filtrage par entité selon le rôle
|
||||||
|
- Soft delete pour conservation de l'historique
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Exécuter le script SQL :
|
||||||
|
```bash
|
||||||
|
mysql -u root -p geo_app < scripts/sql/create_chat_tables.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Évolutions futures possibles
|
||||||
|
|
||||||
|
1. **Notifications push** : Intégration avec Firebase/WebSocket
|
||||||
|
2. **Fichiers joints** : Support d'images et documents
|
||||||
|
3. **Réactions** : Emojis sur les messages
|
||||||
|
4. **Mentions** : @username pour notifier
|
||||||
|
5. **Recherche** : Dans l'historique des messages
|
||||||
|
6. **Chiffrement E2E** : Pour conversations sensibles
|
||||||
|
7. **Statuts de présence** : En ligne/Hors ligne
|
||||||
|
8. **Indicateur de frappe** : "X est en train d'écrire..."
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Cas de test recommandés
|
||||||
|
|
||||||
|
1. **Création de conversation privée**
|
||||||
|
- Vérifier la détection de conversation existante
|
||||||
|
- Tester avec utilisateurs de différentes entités
|
||||||
|
|
||||||
|
2. **Envoi de messages**
|
||||||
|
- Messages avec caractères UTF-8 (émojis, accents)
|
||||||
|
- Messages très longs (limite 5000)
|
||||||
|
- Messages vides (doivent être rejetés)
|
||||||
|
|
||||||
|
3. **Marquage comme lu**
|
||||||
|
- Marquer messages spécifiques
|
||||||
|
- Marquer tous les messages d'une room
|
||||||
|
- Vérifier les compteurs
|
||||||
|
|
||||||
|
4. **Permissions**
|
||||||
|
- Utilisateur simple ne peut pas créer de broadcast
|
||||||
|
- Accès refusé aux conversations non autorisées
|
||||||
|
- Filtrage correct des destinataires
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Pour toute question ou problème :
|
||||||
|
- Vérifier les logs dans `/logs/`
|
||||||
|
- Consulter les tables `chat_*` en base de données
|
||||||
|
- Tester avec les scripts de test fournis
|
||||||
138
api/docs/CHK_USER_DELETE_PASS_INFO.md
Normal file
138
api/docs/CHK_USER_DELETE_PASS_INFO.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Gestion du champ chk_user_delete_pass
|
||||||
|
|
||||||
|
## 📋 Description
|
||||||
|
Le champ `chk_user_delete_pass` permet de contrôler si les membres d'une amicale peuvent supprimer des passages.
|
||||||
|
|
||||||
|
## 🔄 Modifications API
|
||||||
|
|
||||||
|
### 1. Base de données
|
||||||
|
- **Table** : `entites`
|
||||||
|
- **Champ** : `chk_user_delete_pass` TINYINT(1) DEFAULT 0
|
||||||
|
- **Valeurs** :
|
||||||
|
- `0` : Les membres NE peuvent PAS supprimer de passages (par défaut)
|
||||||
|
- `1` : Les membres PEUVENT supprimer des passages
|
||||||
|
|
||||||
|
### 2. Endpoints modifiés
|
||||||
|
|
||||||
|
#### POST /api/entites (Création)
|
||||||
|
- Le champ est automatiquement initialisé à `0` (false) lors de la création
|
||||||
|
- Non modifiable à la création
|
||||||
|
|
||||||
|
#### PUT /api/entites/{id} (Modification)
|
||||||
|
**Entrée JSON :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"chk_user_delete_pass": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Type** : Boolean (0 ou 1)
|
||||||
|
- **Obligatoire** : Non
|
||||||
|
- **Accès** : Administrateurs uniquement (fk_role > 1)
|
||||||
|
|
||||||
|
#### GET /api/entites/{id} (Récupération)
|
||||||
|
**Sortie JSON :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Amicale de Pompiers",
|
||||||
|
"code_postal": "75001",
|
||||||
|
"ville": "Paris",
|
||||||
|
"chk_active": 1,
|
||||||
|
"chk_user_delete_pass": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/entites (Liste)
|
||||||
|
Retourne `chk_user_delete_pass` pour chaque entité dans la liste.
|
||||||
|
|
||||||
|
### 3. Route /api/login
|
||||||
|
Le champ `chk_user_delete_pass` est maintenant inclus dans la réponse de login dans les objets `amicale` :
|
||||||
|
|
||||||
|
**Réponse JSON :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": { ... },
|
||||||
|
"amicale": {
|
||||||
|
"id": 5,
|
||||||
|
"name": "Amicale de Pompiers",
|
||||||
|
"code_postal": "75001",
|
||||||
|
"ville": "Paris",
|
||||||
|
"chk_demo": 0,
|
||||||
|
"chk_mdp_manuel": 0,
|
||||||
|
"chk_username_manuel": 0,
|
||||||
|
"chk_copie_mail_recu": 0,
|
||||||
|
"chk_accept_sms": 0,
|
||||||
|
"chk_active": 1,
|
||||||
|
"chk_stripe": 0,
|
||||||
|
"chk_user_delete_pass": 0 // ← NOUVEAU CHAMP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Utilisation côté client
|
||||||
|
|
||||||
|
### Flutter/Web
|
||||||
|
Le client doit :
|
||||||
|
1. **Récupérer** la valeur de `chk_user_delete_pass` depuis la réponse login
|
||||||
|
2. **Stocker** cette valeur dans l'état de l'application
|
||||||
|
3. **Conditionner** l'affichage du bouton de suppression selon cette valeur
|
||||||
|
|
||||||
|
**Exemple Flutter :**
|
||||||
|
```dart
|
||||||
|
// Dans le modèle Amicale
|
||||||
|
class Amicale {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final bool chkUserDeletePass; // Nouveau champ
|
||||||
|
|
||||||
|
bool get canUserDeletePassage => chkUserDeletePass;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dans l'UI
|
||||||
|
if (amicale.canUserDeletePassage) {
|
||||||
|
// Afficher le bouton de suppression
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.delete),
|
||||||
|
onPressed: () => deletePassage(passageId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Points importants
|
||||||
|
|
||||||
|
1. **Valeur par défaut** : Toujours `0` (false) pour la sécurité
|
||||||
|
2. **Modification** : Seuls les administrateurs (fk_role > 1) peuvent modifier ce champ
|
||||||
|
3. **Rétrocompatibilité** : Les entités existantes ont la valeur `0` par défaut
|
||||||
|
4. **Validation côté serveur** : L'API vérifiera également ce droit lors de la tentative de suppression
|
||||||
|
|
||||||
|
## 📝 Script SQL
|
||||||
|
Le script de migration est disponible dans :
|
||||||
|
```
|
||||||
|
/scripts/sql/add_chk_user_delete_pass.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Checklist d'implémentation
|
||||||
|
|
||||||
|
### Côté API (déjà fait) :
|
||||||
|
- [x] Ajout du champ en base de données
|
||||||
|
- [x] Modification EntiteController (create, update, get)
|
||||||
|
- [x] Modification LoginController (réponse login)
|
||||||
|
- [x] Script SQL de migration
|
||||||
|
|
||||||
|
### Côté Client (à faire) :
|
||||||
|
- [ ] Ajouter le champ dans le modèle Amicale
|
||||||
|
- [ ] Parser le champ depuis la réponse login
|
||||||
|
- [ ] Stocker dans l'état de l'application
|
||||||
|
- [ ] Conditionner l'affichage du bouton suppression
|
||||||
|
- [ ] Tester avec des valeurs 0 et 1
|
||||||
|
|
||||||
|
## 🔒 Sécurité
|
||||||
|
Même si `chk_user_delete_pass = 1`, l'API devra vérifier :
|
||||||
|
- L'authentification de l'utilisateur
|
||||||
|
- L'appartenance à l'entité
|
||||||
|
- Le droit de suppression sur le passage spécifique
|
||||||
|
- Les règles métier (ex: pas de suppression après export)
|
||||||
|
|
||||||
|
---
|
||||||
|
**Date :** 20/08/2025
|
||||||
|
**Version API :** 3.1.4
|
||||||
165
api/docs/DELETE_PASSAGE_PERMISSIONS.md
Normal file
165
api/docs/DELETE_PASSAGE_PERMISSIONS.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# API DELETE /passages/{id} - Documentation des permissions
|
||||||
|
|
||||||
|
## 📋 Endpoint
|
||||||
|
```
|
||||||
|
DELETE /api/passages/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Authentification
|
||||||
|
- **Requise** : OUI (Bearer token)
|
||||||
|
- **Session** : Doit être valide
|
||||||
|
|
||||||
|
## 📊 Logique de permissions
|
||||||
|
|
||||||
|
### Règles par rôle :
|
||||||
|
|
||||||
|
| fk_role | Description | Peut supprimer ? | Conditions |
|
||||||
|
|---------|------------|------------------|------------|
|
||||||
|
| 1 | Membre | ✅ Conditionnel | Si `entites.chk_user_delete_pass = 1` |
|
||||||
|
| 2 | Admin amicale | ✅ OUI | Toujours autorisé |
|
||||||
|
| 3+ | Super admin | ✅ OUI | Toujours autorisé |
|
||||||
|
|
||||||
|
### Détail du contrôle pour les membres (fk_role = 1) :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- L'API vérifie :
|
||||||
|
SELECT chk_user_delete_pass
|
||||||
|
FROM entites
|
||||||
|
WHERE id = {user.fk_entite}
|
||||||
|
|
||||||
|
-- Si chk_user_delete_pass = 0 → Erreur 403
|
||||||
|
-- Si chk_user_delete_pass = 1 → Continue
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Flux de vérification
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[DELETE /passages/{id}] --> B{Utilisateur authentifié ?}
|
||||||
|
B -->|Non| C[Erreur 401]
|
||||||
|
B -->|Oui| D{Récupérer fk_role}
|
||||||
|
D --> E{fk_role = 1 ?}
|
||||||
|
E -->|Non| F[Autorisé - Admin]
|
||||||
|
E -->|Oui| G{Vérifier chk_user_delete_pass}
|
||||||
|
G -->|= 0| H[Erreur 403 - Non autorisé]
|
||||||
|
G -->|= 1| F
|
||||||
|
F --> I{Passage existe ?}
|
||||||
|
I -->|Non| J[Erreur 404]
|
||||||
|
I -->|Oui| K{Passage appartient à l'entité ?}
|
||||||
|
K -->|Non| L[Erreur 404]
|
||||||
|
K -->|Oui| M[Soft delete : chk_active = 0]
|
||||||
|
M --> N[Succès 200]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Réponses
|
||||||
|
|
||||||
|
### ✅ Succès (200)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Passage supprimé avec succès"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Erreur 401 - Non authentifié
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Vous devez être connecté pour effectuer cette action"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Erreur 403 - Permission refusée (membre sans autorisation)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Vous n'avez pas l'autorisation de supprimer des passages"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Erreur 404 - Passage non trouvé
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Passage non trouvé"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Logging
|
||||||
|
|
||||||
|
L'API enregistre :
|
||||||
|
|
||||||
|
### En cas de tentative non autorisée :
|
||||||
|
```php
|
||||||
|
LogService::log('Tentative de suppression de passage non autorisée', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'userId' => $userId,
|
||||||
|
'userRole' => $userRole,
|
||||||
|
'entiteId' => $entiteId,
|
||||||
|
'passageId' => $passageId,
|
||||||
|
'chk_user_delete_pass' => 0
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### En cas de succès :
|
||||||
|
```php
|
||||||
|
LogService::log('Suppression d\'un passage', [
|
||||||
|
'level' => 'info',
|
||||||
|
'userId' => $userId,
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Exemple d'utilisation
|
||||||
|
|
||||||
|
### Requête
|
||||||
|
```bash
|
||||||
|
curl -X DELETE https://api.geosector.fr/api/passages/19500576 \
|
||||||
|
-H "Authorization: Bearer {session_token}" \
|
||||||
|
-H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scénarios
|
||||||
|
|
||||||
|
#### Scénario 1 : Membre avec permission ✅
|
||||||
|
- Utilisateur : fk_role = 1
|
||||||
|
- Entité : chk_user_delete_pass = 1
|
||||||
|
- **Résultat** : Suppression autorisée
|
||||||
|
|
||||||
|
#### Scénario 2 : Membre sans permission ❌
|
||||||
|
- Utilisateur : fk_role = 1
|
||||||
|
- Entité : chk_user_delete_pass = 0
|
||||||
|
- **Résultat** : Erreur 403
|
||||||
|
|
||||||
|
#### Scénario 3 : Admin amicale ✅
|
||||||
|
- Utilisateur : fk_role = 2
|
||||||
|
- **Résultat** : Suppression autorisée (pas de vérification chk_user_delete_pass)
|
||||||
|
|
||||||
|
## ⚠️ Notes importantes
|
||||||
|
|
||||||
|
1. **Soft delete** : Le passage n'est pas supprimé physiquement, seulement `chk_active = 0`
|
||||||
|
2. **Traçabilité** : `updated_at` et `fk_user_modif` sont mis à jour
|
||||||
|
3. **Contrôle entité** : Un utilisateur ne peut supprimer que les passages de son entité
|
||||||
|
4. **Log warning** : Toute tentative non autorisée est loggée en niveau WARNING
|
||||||
|
|
||||||
|
## 🔧 Configuration côté amicale
|
||||||
|
|
||||||
|
Pour autoriser les membres à supprimer des passages :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE entites
|
||||||
|
SET chk_user_delete_pass = 1
|
||||||
|
WHERE id = {entite_id};
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette modification ne peut être faite que par un administrateur (fk_role > 1) via l'endpoint :
|
||||||
|
```
|
||||||
|
PUT /api/entites/{id}
|
||||||
|
{
|
||||||
|
"chk_user_delete_pass": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
**Version API** : 3.1.4
|
||||||
|
**Date** : 20/08/2025
|
||||||
176
api/docs/FIX_USER_CREATION_400_ERRORS.md
Normal file
176
api/docs/FIX_USER_CREATION_400_ERRORS.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Correction des erreurs 400 lors de la création d'utilisateurs
|
||||||
|
|
||||||
|
## Problème identifié
|
||||||
|
Un administrateur (fk_role=2) rencontrait des erreurs 400 répétées lors de tentatives de création de membre, menant à un bannissement par fail2ban :
|
||||||
|
- 17:09:39 - POST /api/users HTTP/1.1 400 (Bad Request)
|
||||||
|
- 17:10:44 - POST /api/users/check-username HTTP/1.1 400 (Bad Request)
|
||||||
|
- 17:11:21 - POST /api/users HTTP/1.1 400 (Bad Request)
|
||||||
|
|
||||||
|
## Causes identifiées
|
||||||
|
|
||||||
|
### 1. Conflit de routage (CRITIQUE)
|
||||||
|
**Problème:** La route `/api/users/check-username` était déclarée APRÈS la route générique `/api/users` dans Router.php, causant une mauvaise interprétation où "check-username" était traité comme un ID utilisateur.
|
||||||
|
|
||||||
|
**Solution:** Déplacer la déclaration de la route spécifique AVANT les routes avec paramètres.
|
||||||
|
|
||||||
|
### 2. Messages d'erreur non informatifs
|
||||||
|
**Problème:** Les erreurs 400 retournaient des messages génériques sans détails sur le champ problématique.
|
||||||
|
|
||||||
|
**Solution:** Ajout de messages d'erreur détaillés incluant :
|
||||||
|
- Le champ en erreur (`field`)
|
||||||
|
- La valeur problématique (`value`)
|
||||||
|
- Le format attendu (`format`)
|
||||||
|
- La raison de l'erreur (`reason`)
|
||||||
|
|
||||||
|
### 3. Manque de logs de débogage
|
||||||
|
**Problème:** Aucun log n'était généré pour tracer les erreurs de validation.
|
||||||
|
|
||||||
|
**Solution:** Ajout de logs détaillés à chaque point de validation.
|
||||||
|
|
||||||
|
## Modifications apportées
|
||||||
|
|
||||||
|
### 1. Router.php (ligne 36-44)
|
||||||
|
```php
|
||||||
|
// AVANT (incorrect)
|
||||||
|
$this->post('users', ['UserController', 'createUser']);
|
||||||
|
$this->post('users/check-username', ['UserController', 'checkUsername']);
|
||||||
|
|
||||||
|
// APRÈS (correct)
|
||||||
|
$this->post('users/check-username', ['UserController', 'checkUsername']); // Route spécifique en premier
|
||||||
|
$this->post('users', ['UserController', 'createUser']);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. UserController.php - Amélioration des validations
|
||||||
|
|
||||||
|
#### Validation de l'email
|
||||||
|
```php
|
||||||
|
// Réponse améliorée
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Email requis',
|
||||||
|
'field' => 'email' // Indique clairement le champ problématique
|
||||||
|
], 400);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Validation du username manuel
|
||||||
|
```php
|
||||||
|
// Réponse améliorée
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Le nom d\'utilisateur est requis pour cette entité',
|
||||||
|
'field' => 'username',
|
||||||
|
'reason' => 'L\'entité requiert la saisie manuelle des identifiants'
|
||||||
|
], 400);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Format du username
|
||||||
|
```php
|
||||||
|
// Réponse améliorée
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Format du nom d\'utilisateur invalide',
|
||||||
|
'field' => 'username',
|
||||||
|
'format' => '10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _',
|
||||||
|
'value' => $username // Montre la valeur soumise
|
||||||
|
], 400);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Ajout de logs détaillés
|
||||||
|
|
||||||
|
Chaque point de validation génère maintenant un log avec :
|
||||||
|
- Le type d'erreur
|
||||||
|
- L'utilisateur qui fait la requête
|
||||||
|
- Les données reçues (sans données sensibles)
|
||||||
|
- Le contexte de l'erreur
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
```php
|
||||||
|
LogService::log('Erreur création utilisateur : Format username invalide', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'username' => $username,
|
||||||
|
'username_length' => strlen($username)
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cas d'erreur 400 possibles
|
||||||
|
|
||||||
|
### Pour /api/users (création)
|
||||||
|
|
||||||
|
1. **Email manquant ou vide**
|
||||||
|
- Message: "Email requis"
|
||||||
|
- Field: "email"
|
||||||
|
|
||||||
|
2. **Nom manquant ou vide**
|
||||||
|
- Message: "Nom requis"
|
||||||
|
- Field: "name"
|
||||||
|
|
||||||
|
3. **Format email invalide**
|
||||||
|
- Message: "Format d'email invalide"
|
||||||
|
- Field: "email"
|
||||||
|
- Value: [email soumis]
|
||||||
|
|
||||||
|
4. **Username manuel requis mais manquant** (si chk_username_manuel=1)
|
||||||
|
- Message: "Le nom d'utilisateur est requis pour cette entité"
|
||||||
|
- Field: "username"
|
||||||
|
- Reason: "L'entité requiert la saisie manuelle des identifiants"
|
||||||
|
|
||||||
|
5. **Format username invalide**
|
||||||
|
- Message: "Format du nom d'utilisateur invalide"
|
||||||
|
- Field: "username"
|
||||||
|
- Format: "10-30 caractères, commence par une lettre..."
|
||||||
|
- Value: [username soumis]
|
||||||
|
|
||||||
|
6. **Mot de passe manuel requis mais manquant** (si chk_mdp_manuel=1)
|
||||||
|
- Message: "Le mot de passe est requis pour cette entité"
|
||||||
|
- Field: "password"
|
||||||
|
- Reason: "L'entité requiert la saisie manuelle des mots de passe"
|
||||||
|
|
||||||
|
### Pour /api/users/check-username
|
||||||
|
|
||||||
|
1. **Username manquant**
|
||||||
|
- Message: "Username requis pour la vérification"
|
||||||
|
- Field: "username"
|
||||||
|
|
||||||
|
2. **Format username invalide**
|
||||||
|
- Message: "Format invalide"
|
||||||
|
- Field: "username"
|
||||||
|
- Format: "10-30 caractères, commence par une lettre..."
|
||||||
|
- Value: [username soumis]
|
||||||
|
|
||||||
|
## Test de la solution
|
||||||
|
|
||||||
|
Un script de test a été créé : `/tests/test_user_creation.php`
|
||||||
|
|
||||||
|
Il teste tous les cas d'erreur possibles et vérifie que :
|
||||||
|
1. Les codes HTTP sont corrects
|
||||||
|
2. Les messages d'erreur sont informatifs
|
||||||
|
3. Les champs en erreur sont identifiés
|
||||||
|
|
||||||
|
## Recommandations pour éviter le bannissement fail2ban
|
||||||
|
|
||||||
|
1. **Côté client (application Flutter)** :
|
||||||
|
- Valider les données AVANT l'envoi
|
||||||
|
- Afficher clairement les erreurs à l'utilisateur
|
||||||
|
- Implémenter un délai entre les tentatives (rate limiting côté client)
|
||||||
|
|
||||||
|
2. **Côté API** :
|
||||||
|
- Les messages d'erreur détaillés permettent maintenant de corriger rapidement les problèmes
|
||||||
|
- Les logs permettent de diagnostiquer les problèmes récurrents
|
||||||
|
|
||||||
|
3. **Configuration fail2ban** :
|
||||||
|
- Considérer d'augmenter le seuil pour les erreurs 400 (ex: 5 tentatives au lieu de 3)
|
||||||
|
- Exclure certaines IP de confiance si nécessaire
|
||||||
|
|
||||||
|
## Suivi des logs
|
||||||
|
|
||||||
|
Les logs sont maintenant générés dans :
|
||||||
|
- `/logs/geosector-[environment]-[date].log` : Logs généraux avec détails des erreurs
|
||||||
|
|
||||||
|
Format des logs :
|
||||||
|
```
|
||||||
|
timestamp;browser;os;client_type;level;metadata;message
|
||||||
|
```
|
||||||
|
|
||||||
|
Les erreurs de validation sont loggées avec le niveau "warning" pour permettre un suivi sans être critiques.
|
||||||
90
api/docs/INSTALL_FPDF.md
Normal file
90
api/docs/INSTALL_FPDF.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Installation de FPDF pour la génération des reçus PDF avec logo
|
||||||
|
|
||||||
|
## Installation via Composer (RECOMMANDÉ)
|
||||||
|
|
||||||
|
Sur chaque serveur (DEV, REC, PROD), exécuter :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/geosector/api
|
||||||
|
composer require setasign/fpdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou si composer.json est déjà mis à jour :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/geosector/api
|
||||||
|
composer update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers à déployer
|
||||||
|
|
||||||
|
1. **Nouveaux fichiers** :
|
||||||
|
- `/src/Services/ReceiptPDFGenerator.php` - Nouvelle classe de génération PDF avec FPDF
|
||||||
|
- `/docs/_logo_recu.png` - Logo par défaut (casque de pompier)
|
||||||
|
|
||||||
|
2. **Fichiers modifiés** :
|
||||||
|
- `/src/Services/ReceiptService.php` - Utilise maintenant ReceiptPDFGenerator
|
||||||
|
- `/composer.json` - Ajout de la dépendance FPDF
|
||||||
|
|
||||||
|
## Vérification
|
||||||
|
|
||||||
|
Après installation, tester la génération d'un reçu :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier que FPDF est installé
|
||||||
|
ls -la vendor/setasign/fpdf/
|
||||||
|
|
||||||
|
# Tester la génération d'un PDF
|
||||||
|
php -r "
|
||||||
|
require 'vendor/autoload.php';
|
||||||
|
\$pdf = new FPDF();
|
||||||
|
\$pdf->AddPage();
|
||||||
|
\$pdf->SetFont('Arial','B',16);
|
||||||
|
\$pdf->Cell(40,10,'Test FPDF OK');
|
||||||
|
echo 'FPDF fonctionne' . PHP_EOL;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fonctionnalités du nouveau générateur
|
||||||
|
|
||||||
|
✅ **Support des vrais logos PNG/JPG**
|
||||||
|
✅ **Logo par défaut** si l'entité n'a pas de logo
|
||||||
|
✅ **Taille du logo** : 40x40mm
|
||||||
|
✅ **Mise en page professionnelle** avec cadre pour le montant
|
||||||
|
✅ **Conversion automatique** des caractères UTF-8
|
||||||
|
✅ **PDF léger** (~20-30KB avec logo)
|
||||||
|
|
||||||
|
## Structure du reçu généré
|
||||||
|
|
||||||
|
1. **En-tête** :
|
||||||
|
- Logo (40x40mm) à gauche
|
||||||
|
- Nom et ville de l'entité à droite du logo
|
||||||
|
|
||||||
|
2. **Titre** :
|
||||||
|
- "REÇU FISCAL DE DON"
|
||||||
|
- Numéro du reçu
|
||||||
|
- Article 200 CGI
|
||||||
|
|
||||||
|
3. **Corps** :
|
||||||
|
- Informations du donateur
|
||||||
|
- Montant en gros dans un cadre grisé
|
||||||
|
- Date du don
|
||||||
|
- Mode de règlement et campagne
|
||||||
|
|
||||||
|
4. **Pied de page** :
|
||||||
|
- Mentions légales (réduction 66%)
|
||||||
|
- Date et signature
|
||||||
|
|
||||||
|
## Résolution de problèmes
|
||||||
|
|
||||||
|
Si erreur "Class 'FPDF' not found" :
|
||||||
|
```bash
|
||||||
|
composer dump-autoload
|
||||||
|
```
|
||||||
|
|
||||||
|
Si problème avec le logo :
|
||||||
|
- Vérifier que `/docs/_logo_recu.png` existe
|
||||||
|
- Vérifier les permissions : `chmod 644 docs/_logo_recu.png`
|
||||||
|
|
||||||
|
Si caractères accentués mal affichés :
|
||||||
|
- FPDF utilise ISO-8859-1, la conversion est automatique dans ReceiptPDFGenerator
|
||||||
622
api/docs/PLANNING-STRIPE-API.md
Normal file
622
api/docs/PLANNING-STRIPE-API.md
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
# PLANNING STRIPE - DÉVELOPPEUR BACKEND PHP
|
||||||
|
## API PHP 8.3 - Intégration Stripe Connect + Terminal
|
||||||
|
### Période : 25/08/2024 - 05/09/2024
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 LUNDI 25/08 - Setup et architecture (8h)
|
||||||
|
|
||||||
|
### 🌅 Matin (4h)
|
||||||
|
```bash
|
||||||
|
# Installation Stripe PHP SDK
|
||||||
|
cd api
|
||||||
|
composer require stripe/stripe-php
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ Configuration environnement
|
||||||
|
- [ ] Créer `config/stripe.php` avec clés TEST
|
||||||
|
- [ ] Ajouter variables `.env` :
|
||||||
|
```env
|
||||||
|
STRIPE_PUBLIC_KEY=pk_test_...
|
||||||
|
STRIPE_SECRET_KEY=sk_test_...
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||||
|
STRIPE_API_VERSION=2024-06-20
|
||||||
|
```
|
||||||
|
- [ ] Créer service `StripeService.php` singleton
|
||||||
|
- [ ] Configurer middleware authentification API
|
||||||
|
|
||||||
|
#### ✅ Base de données
|
||||||
|
```sql
|
||||||
|
-- Tables à créer
|
||||||
|
CREATE TABLE stripe_accounts (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
amicale_id INT NOT NULL,
|
||||||
|
stripe_account_id VARCHAR(255) UNIQUE,
|
||||||
|
charges_enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
payouts_enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
onboarding_completed BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (amicale_id) REFERENCES amicales(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE payment_intents (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
stripe_payment_intent_id VARCHAR(255) UNIQUE,
|
||||||
|
amicale_id INT NOT NULL,
|
||||||
|
pompier_id INT NOT NULL,
|
||||||
|
amount INT NOT NULL, -- en centimes
|
||||||
|
currency VARCHAR(3) DEFAULT 'eur',
|
||||||
|
status VARCHAR(50),
|
||||||
|
application_fee INT,
|
||||||
|
metadata JSON,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (amicale_id) REFERENCES amicales(id),
|
||||||
|
FOREIGN KEY (pompier_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE terminal_readers (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
stripe_reader_id VARCHAR(255) UNIQUE,
|
||||||
|
amicale_id INT NOT NULL,
|
||||||
|
label VARCHAR(255),
|
||||||
|
location VARCHAR(255),
|
||||||
|
status VARCHAR(50),
|
||||||
|
device_type VARCHAR(50),
|
||||||
|
last_seen_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (amicale_id) REFERENCES amicales(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE android_certified_devices (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
manufacturer VARCHAR(100),
|
||||||
|
model VARCHAR(200),
|
||||||
|
model_identifier VARCHAR(200),
|
||||||
|
tap_to_pay_certified BOOLEAN DEFAULT FALSE,
|
||||||
|
certification_date DATE,
|
||||||
|
min_android_version INT,
|
||||||
|
country VARCHAR(2) DEFAULT 'FR',
|
||||||
|
last_verified TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_manufacturer_model (manufacturer, model)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌆 Après-midi (4h)
|
||||||
|
|
||||||
|
#### ✅ Endpoints Connect - Onboarding
|
||||||
|
```php
|
||||||
|
// POST /api/amicales/{id}/stripe-account
|
||||||
|
public function createStripeAccount($amicaleId) {
|
||||||
|
$amicale = Amicale::find($amicaleId);
|
||||||
|
|
||||||
|
$account = \Stripe\Account::create([
|
||||||
|
'type' => 'express',
|
||||||
|
'country' => 'FR',
|
||||||
|
'email' => $amicale->email,
|
||||||
|
'capabilities' => [
|
||||||
|
'card_payments' => ['requested' => true],
|
||||||
|
'transfers' => ['requested' => true],
|
||||||
|
],
|
||||||
|
'business_type' => 'non_profit',
|
||||||
|
'business_profile' => [
|
||||||
|
'name' => $amicale->name,
|
||||||
|
'product_description' => 'Vente de calendriers des pompiers',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sauvegarder stripe_account_id
|
||||||
|
return $account;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/amicales/{id}/onboarding-link
|
||||||
|
public function getOnboardingLink($amicaleId) {
|
||||||
|
$accountLink = \Stripe\AccountLink::create([
|
||||||
|
'account' => $amicale->stripe_account_id,
|
||||||
|
'refresh_url' => config('app.url') . '/stripe/refresh',
|
||||||
|
'return_url' => config('app.url') . '/stripe/success',
|
||||||
|
'type' => 'account_onboarding',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ['url' => $accountLink->url];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 MARDI 26/08 - Webhooks et Terminal (8h)
|
||||||
|
|
||||||
|
### 🌅 Matin (4h)
|
||||||
|
|
||||||
|
#### ✅ Webhooks handler
|
||||||
|
```php
|
||||||
|
// POST /api/webhooks/stripe
|
||||||
|
public function handleWebhook(Request $request) {
|
||||||
|
$payload = $request->getContent();
|
||||||
|
$sig_header = $request->header('Stripe-Signature');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$event = \Stripe\Webhook::constructEvent(
|
||||||
|
$payload, $sig_header, config('stripe.webhook_secret')
|
||||||
|
);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
return response('Invalid signature', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($event->type) {
|
||||||
|
case 'account.updated':
|
||||||
|
$this->handleAccountUpdated($event->data->object);
|
||||||
|
break;
|
||||||
|
case 'account.application.authorized':
|
||||||
|
$this->handleAccountAuthorized($event->data->object);
|
||||||
|
break;
|
||||||
|
case 'payment_intent.succeeded':
|
||||||
|
$this->handlePaymentSucceeded($event->data->object);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response('Webhook handled', 200);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ Terminal Connection Token
|
||||||
|
```php
|
||||||
|
// POST /api/terminal/connection-token
|
||||||
|
public function createConnectionToken(Request $request) {
|
||||||
|
$pompier = Auth::user();
|
||||||
|
$amicale = $pompier->amicale;
|
||||||
|
|
||||||
|
$connectionToken = \Stripe\Terminal\ConnectionToken::create([
|
||||||
|
'location' => $amicale->stripe_location_id,
|
||||||
|
], [
|
||||||
|
'stripe_account' => $amicale->stripe_account_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ['secret' => $connectionToken->secret];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌆 Après-midi (4h)
|
||||||
|
|
||||||
|
#### ✅ Gestion des Locations
|
||||||
|
```php
|
||||||
|
// POST /api/amicales/{id}/create-location
|
||||||
|
public function createLocation($amicaleId) {
|
||||||
|
$amicale = Amicale::find($amicaleId);
|
||||||
|
|
||||||
|
$location = \Stripe\Terminal\Location::create([
|
||||||
|
'display_name' => $amicale->name,
|
||||||
|
'address' => [
|
||||||
|
'line1' => $amicale->address,
|
||||||
|
'city' => $amicale->city,
|
||||||
|
'postal_code' => $amicale->postal_code,
|
||||||
|
'country' => 'FR',
|
||||||
|
],
|
||||||
|
], [
|
||||||
|
'stripe_account' => $amicale->stripe_account_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$amicale->update(['stripe_location_id' => $location->id]);
|
||||||
|
return $location;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 MERCREDI 27/08 - Paiements et fees (8h)
|
||||||
|
|
||||||
|
### 🌅 Matin (4h)
|
||||||
|
|
||||||
|
#### ✅ Création PaymentIntent avec commission
|
||||||
|
```php
|
||||||
|
// POST /api/payments/create-intent
|
||||||
|
public function createPaymentIntent(Request $request) {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'amount' => 'required|integer|min:100', // en centimes
|
||||||
|
'amicale_id' => 'required|exists:amicales,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pompier = Auth::user();
|
||||||
|
$amicale = Amicale::find($validated['amicale_id']);
|
||||||
|
|
||||||
|
// Calculer la commission (2.5% ou 50 centimes minimum)
|
||||||
|
$applicationFee = max(
|
||||||
|
50, // 0.50€ minimum
|
||||||
|
round($validated['amount'] * 0.025) // 2.5%
|
||||||
|
);
|
||||||
|
|
||||||
|
$paymentIntent = \Stripe\PaymentIntent::create([
|
||||||
|
'amount' => $validated['amount'],
|
||||||
|
'currency' => 'eur',
|
||||||
|
'payment_method_types' => ['card_present'],
|
||||||
|
'capture_method' => 'automatic',
|
||||||
|
'application_fee_amount' => $applicationFee,
|
||||||
|
'transfer_data' => [
|
||||||
|
'destination' => $amicale->stripe_account_id,
|
||||||
|
],
|
||||||
|
'metadata' => [
|
||||||
|
'pompier_id' => $pompier->id,
|
||||||
|
'pompier_name' => $pompier->name,
|
||||||
|
'amicale_id' => $amicale->id,
|
||||||
|
'calendrier_annee' => date('Y'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sauvegarder en DB
|
||||||
|
PaymentIntent::create([
|
||||||
|
'stripe_payment_intent_id' => $paymentIntent->id,
|
||||||
|
'amicale_id' => $amicale->id,
|
||||||
|
'pompier_id' => $pompier->id,
|
||||||
|
'amount' => $validated['amount'],
|
||||||
|
'application_fee' => $applicationFee,
|
||||||
|
'status' => $paymentIntent->status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'client_secret' => $paymentIntent->client_secret,
|
||||||
|
'payment_intent_id' => $paymentIntent->id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌆 Après-midi (4h)
|
||||||
|
|
||||||
|
#### ✅ Capture et confirmation
|
||||||
|
```php
|
||||||
|
// POST /api/payments/{id}/capture
|
||||||
|
public function capturePayment($paymentIntentId) {
|
||||||
|
$localPayment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||||
|
|
||||||
|
$paymentIntent = \Stripe\PaymentIntent::retrieve($paymentIntentId);
|
||||||
|
|
||||||
|
if ($paymentIntent->status === 'requires_capture') {
|
||||||
|
$paymentIntent->capture();
|
||||||
|
}
|
||||||
|
|
||||||
|
$localPayment->update(['status' => $paymentIntent->status]);
|
||||||
|
|
||||||
|
// Si succès, envoyer email reçu
|
||||||
|
if ($paymentIntent->status === 'succeeded') {
|
||||||
|
$this->sendReceipt($localPayment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paymentIntent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/payments/{id}/status
|
||||||
|
public function getPaymentStatus($paymentIntentId) {
|
||||||
|
$payment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||||
|
return [
|
||||||
|
'status' => $payment->status,
|
||||||
|
'amount' => $payment->amount,
|
||||||
|
'created_at' => $payment->created_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 JEUDI 28/08 - Reporting et Android compatibility (8h)
|
||||||
|
|
||||||
|
### 🌅 Matin (4h)
|
||||||
|
|
||||||
|
#### ✅ Gestion appareils Android certifiés
|
||||||
|
```php
|
||||||
|
// POST /api/devices/check-tap-to-pay
|
||||||
|
public function checkTapToPayCapability(Request $request) {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'platform' => 'required|in:ios,android',
|
||||||
|
'manufacturer' => 'required_if:platform,android',
|
||||||
|
'model' => 'required_if:platform,android',
|
||||||
|
'os_version' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validated['platform'] === 'ios') {
|
||||||
|
// iPhone XS et ultérieurs avec iOS 15.4+
|
||||||
|
$supportedModels = ['iPhone11,', 'iPhone12,', 'iPhone13,', 'iPhone14,', 'iPhone15,', 'iPhone16,'];
|
||||||
|
$modelSupported = false;
|
||||||
|
|
||||||
|
foreach ($supportedModels as $prefix) {
|
||||||
|
if (str_starts_with($validated['model'], $prefix)) {
|
||||||
|
$modelSupported = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$osVersion = explode('.', $validated['os_version']);
|
||||||
|
$osSupported = $osVersion[0] > 15 ||
|
||||||
|
($osVersion[0] == 15 && isset($osVersion[1]) && $osVersion[1] >= 4);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tap_to_pay_supported' => $modelSupported && $osSupported,
|
||||||
|
'message' => $modelSupported && $osSupported ?
|
||||||
|
'Tap to Pay disponible' :
|
||||||
|
'iPhone XS ou ultérieur avec iOS 15.4+ requis'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android - vérifier dans la base de données
|
||||||
|
$device = DB::table('android_certified_devices')
|
||||||
|
->where('manufacturer', $validated['manufacturer'])
|
||||||
|
->where('model', $validated['model'])
|
||||||
|
->where('tap_to_pay_certified', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tap_to_pay_supported' => $device !== null,
|
||||||
|
'message' => $device ?
|
||||||
|
'Tap to Pay disponible sur cet appareil' :
|
||||||
|
'Appareil non certifié pour Tap to Pay en France',
|
||||||
|
'alternative' => !$device ? 'Utilisez un iPhone compatible' : null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/devices/certified-android
|
||||||
|
public function getCertifiedAndroidDevices() {
|
||||||
|
return DB::table('android_certified_devices')
|
||||||
|
->where('tap_to_pay_certified', true)
|
||||||
|
->where('country', 'FR')
|
||||||
|
->orderBy('manufacturer')
|
||||||
|
->orderBy('model')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ Seeder pour appareils certifiés
|
||||||
|
```php
|
||||||
|
// database/seeders/AndroidCertifiedDevicesSeeder.php
|
||||||
|
public function run() {
|
||||||
|
$devices = [
|
||||||
|
// Samsung
|
||||||
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21', 'model_identifier' => 'SM-G991B', 'min_android_version' => 11],
|
||||||
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21+', 'model_identifier' => 'SM-G996B', 'min_android_version' => 11],
|
||||||
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21 Ultra', 'model_identifier' => 'SM-G998B', 'min_android_version' => 11],
|
||||||
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S22', 'model_identifier' => 'SM-S901B', 'min_android_version' => 12],
|
||||||
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S23', 'model_identifier' => 'SM-S911B', 'min_android_version' => 13],
|
||||||
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S24', 'model_identifier' => 'SM-S921B', 'min_android_version' => 14],
|
||||||
|
// Google Pixel
|
||||||
|
['manufacturer' => 'Google', 'model' => 'Pixel 6', 'model_identifier' => 'oriole', 'min_android_version' => 12],
|
||||||
|
['manufacturer' => 'Google', 'model' => 'Pixel 6 Pro', 'model_identifier' => 'raven', 'min_android_version' => 12],
|
||||||
|
['manufacturer' => 'Google', 'model' => 'Pixel 7', 'model_identifier' => 'panther', 'min_android_version' => 13],
|
||||||
|
['manufacturer' => 'Google', 'model' => 'Pixel 8', 'model_identifier' => 'shiba', 'min_android_version' => 14],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($devices as $device) {
|
||||||
|
DB::table('android_certified_devices')->insert([
|
||||||
|
'manufacturer' => $device['manufacturer'],
|
||||||
|
'model' => $device['model'],
|
||||||
|
'model_identifier' => $device['model_identifier'],
|
||||||
|
'tap_to_pay_certified' => true,
|
||||||
|
'certification_date' => now(),
|
||||||
|
'min_android_version' => $device['min_android_version'],
|
||||||
|
'country' => 'FR',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ Endpoints statistiques
|
||||||
|
```php
|
||||||
|
// GET /api/amicales/{id}/stats
|
||||||
|
public function getAmicaleStats($amicaleId) {
|
||||||
|
$stats = DB::table('payment_intents')
|
||||||
|
->where('amicale_id', $amicaleId)
|
||||||
|
->where('status', 'succeeded')
|
||||||
|
->selectRaw('
|
||||||
|
COUNT(*) as total_ventes,
|
||||||
|
SUM(amount) as total_montant,
|
||||||
|
SUM(application_fee) as total_commissions,
|
||||||
|
DATE(created_at) as date
|
||||||
|
')
|
||||||
|
->groupBy('date')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/pompiers/{id}/ventes
|
||||||
|
public function getPompierVentes($pompierId) {
|
||||||
|
return PaymentIntent::where('pompier_id', $pompierId)
|
||||||
|
->where('status', 'succeeded')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->paginate(20);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌆 Après-midi (4h)
|
||||||
|
|
||||||
|
#### ✅ Gestion des remboursements
|
||||||
|
```php
|
||||||
|
// POST /api/payments/{id}/refund
|
||||||
|
public function refundPayment($paymentIntentId, Request $request) {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'amount' => 'integer|min:100', // optionnel, remboursement partiel
|
||||||
|
'reason' => 'string|in:duplicate,fraudulent,requested_by_customer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$payment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||||
|
|
||||||
|
$refund = \Stripe\Refund::create([
|
||||||
|
'payment_intent' => $paymentIntentId,
|
||||||
|
'amount' => $validated['amount'] ?? null, // null = remboursement total
|
||||||
|
'reason' => $validated['reason'] ?? 'requested_by_customer',
|
||||||
|
'reverse_transfer' => true, // Important pour Connect
|
||||||
|
'refund_application_fee' => true, // Rembourser aussi la commission
|
||||||
|
]);
|
||||||
|
|
||||||
|
$payment->update(['status' => 'refunded']);
|
||||||
|
|
||||||
|
return $refund;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 VENDREDI 29/08 - Mode offline et sync (8h)
|
||||||
|
|
||||||
|
### 🌅 Matin (4h)
|
||||||
|
|
||||||
|
#### ✅ Queue de synchronisation
|
||||||
|
```php
|
||||||
|
// POST /api/payments/batch-sync
|
||||||
|
public function batchSync(Request $request) {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'transactions' => 'required|array',
|
||||||
|
'transactions.*.local_id' => 'required|string',
|
||||||
|
'transactions.*.amount' => 'required|integer',
|
||||||
|
'transactions.*.created_at' => 'required|date',
|
||||||
|
'transactions.*.payment_method' => 'required|in:card,cash',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($validated['transactions'] as $transaction) {
|
||||||
|
if ($transaction['payment_method'] === 'cash') {
|
||||||
|
// Enregistrer paiement cash uniquement en DB
|
||||||
|
$results[] = $this->recordCashPayment($transaction);
|
||||||
|
} else {
|
||||||
|
// Créer PaymentIntent a posteriori (si possible)
|
||||||
|
$results[] = $this->createOfflinePayment($transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['synced' => $results];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌆 Après-midi (4h)
|
||||||
|
|
||||||
|
#### ✅ Tests unitaires critiques
|
||||||
|
```php
|
||||||
|
class StripePaymentTest extends TestCase {
|
||||||
|
public function test_create_payment_intent_with_fees() {
|
||||||
|
// Test création PaymentIntent avec commission
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_webhook_signature_validation() {
|
||||||
|
// Test sécurité webhook
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_refund_reverses_transfer() {
|
||||||
|
// Test remboursement avec annulation virement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 LUNDI 01/09 - Sécurité et optimisations (8h)
|
||||||
|
|
||||||
|
### 🌅 Matin (4h)
|
||||||
|
|
||||||
|
#### ✅ Rate limiting et sécurité
|
||||||
|
```php
|
||||||
|
// Middleware RateLimiter pour endpoints sensibles
|
||||||
|
Route::middleware(['throttle:10,1'])->group(function () {
|
||||||
|
Route::post('/payments/create-intent', 'PaymentController@createIntent');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validation des montants
|
||||||
|
public function validateAmount($amount) {
|
||||||
|
if ($amount < 100 || $amount > 50000) { // 1€ - 500€
|
||||||
|
throw new ValidationException('Montant invalide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌆 Après-midi (4h)
|
||||||
|
|
||||||
|
#### ✅ Logs et monitoring
|
||||||
|
```php
|
||||||
|
// Logger tous les événements Stripe
|
||||||
|
Log::channel('stripe')->info('Payment created', [
|
||||||
|
'payment_intent_id' => $paymentIntent->id,
|
||||||
|
'amount' => $paymentIntent->amount,
|
||||||
|
'pompier_id' => $pompier->id,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 MARDI 02/09 - Documentation API (4h)
|
||||||
|
|
||||||
|
#### ✅ Documentation OpenAPI/Swagger
|
||||||
|
```yaml
|
||||||
|
/api/payments/create-intent:
|
||||||
|
post:
|
||||||
|
summary: Créer une intention de paiement
|
||||||
|
parameters:
|
||||||
|
- name: amount
|
||||||
|
type: integer
|
||||||
|
required: true
|
||||||
|
description: Montant en centimes
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: PaymentIntent créé
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 MERCREDI 03/09 - Tests d'intégration (8h)
|
||||||
|
|
||||||
|
#### ✅ Tests end-to-end
|
||||||
|
- [ ] Parcours complet onboarding amicale
|
||||||
|
- [ ] Création paiement → capture → confirmation
|
||||||
|
- [ ] Test remboursement complet et partiel
|
||||||
|
- [ ] Test webhooks avec ngrok
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 JEUDI 04/09 - Mise en production (8h)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 VENDREDI 05/09 - Support et livraison finale (8h)
|
||||||
|
|
||||||
|
### 🌅 Matin (4h)
|
||||||
|
|
||||||
|
#### ✅ Déploiement final
|
||||||
|
- [ ] Migration DB production
|
||||||
|
- [ ] Variables environnement LIVE
|
||||||
|
- [ ] Smoke tests production
|
||||||
|
- [ ] Vérification des webhooks en production
|
||||||
|
|
||||||
|
### 🌆 Après-midi (4h)
|
||||||
|
|
||||||
|
#### ✅ Support et monitoring
|
||||||
|
- [ ] Monitoring des premiers paiements réels
|
||||||
|
- [ ] Support hotline pour équipes terrain
|
||||||
|
- [ ] Documentation de passation
|
||||||
|
- [ ] Réunion de clôture et retour d'expérience
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 RÉCAPITULATIF
|
||||||
|
|
||||||
|
- **Total heures** : 72h sur 10 jours
|
||||||
|
- **Endpoints créés** : 15
|
||||||
|
- **Tables DB** : 3
|
||||||
|
- **Tests** : 20+
|
||||||
|
|
||||||
|
## 🔧 DÉPENDANCES
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3",
|
||||||
|
"stripe/stripe-php": "^13.0",
|
||||||
|
"laravel/framework": "^10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ CHECKLIST SÉCURITÉ
|
||||||
|
|
||||||
|
- [ ] ❌ JAMAIS logger les clés secrètes
|
||||||
|
- [ ] ✅ TOUJOURS valider signature webhooks
|
||||||
|
- [ ] ✅ TOUJOURS utiliser HTTPS
|
||||||
|
- [ ] ✅ Rate limiting sur endpoints paiement
|
||||||
|
- [ ] ✅ Logs détaillés pour audit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document créé le 24/08/2024 - À mettre à jour quotidiennement*
|
||||||
237
api/docs/PREPA_PROD.md
Normal file
237
api/docs/PREPA_PROD.md
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# PRÉPARATION PRODUCTION - Process Email Queue + Permissions Suppression Passages
|
||||||
|
|
||||||
|
## 📅 Date de mise en production prévue : _____________
|
||||||
|
|
||||||
|
## 🎯 Objectif
|
||||||
|
1. Mettre en place le système de traitement automatique de la queue d'emails pour l'envoi des reçus fiscaux de dons.
|
||||||
|
2. Ajouter le champ de permission pour autoriser les membres à supprimer des passages.
|
||||||
|
|
||||||
|
## ✅ Prérequis
|
||||||
|
- [ ] Backup de la base de données effectué
|
||||||
|
- [ ] Accès SSH au serveur PROD
|
||||||
|
- [ ] Accès à la base de données PROD
|
||||||
|
- [ ] Droits pour éditer le crontab
|
||||||
|
|
||||||
|
## 📝 Fichiers à déployer
|
||||||
|
Les fichiers suivants doivent être présents sur le serveur PROD :
|
||||||
|
- `/scripts/cron/process_email_queue.php`
|
||||||
|
- `/scripts/cron/process_email_queue_with_daily_log.sh`
|
||||||
|
- `/scripts/cron/test_email_queue.php`
|
||||||
|
- `/src/Services/ReceiptPDFGenerator.php` (nouveau)
|
||||||
|
- `/src/Services/ReceiptService.php` (mis à jour)
|
||||||
|
- `/src/Core/MonitoredDatabase.php` (mis à jour)
|
||||||
|
- `/src/Controllers/EntiteController.php` (mis à jour)
|
||||||
|
- `/src/Controllers/LoginController.php` (mis à jour)
|
||||||
|
- `/scripts/sql/add_chk_user_delete_pass.sql` (nouveau)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 ÉTAPES DE MISE EN PRODUCTION
|
||||||
|
|
||||||
|
### 1️⃣ Mise à jour de la base de données
|
||||||
|
|
||||||
|
Se connecter à la base de données PROD et exécuter :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Vérifier d'abord la structure actuelle de email_queue
|
||||||
|
DESCRIBE email_queue;
|
||||||
|
|
||||||
|
-- Ajouter les champs manquants pour email_queue si nécessaire
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD COLUMN IF NOT EXISTS `sent_at` TIMESTAMP NULL DEFAULT NULL
|
||||||
|
COMMENT 'Date/heure d\'envoi effectif de l\'email'
|
||||||
|
AFTER `status`;
|
||||||
|
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD COLUMN IF NOT EXISTS `error_message` TEXT NULL DEFAULT NULL
|
||||||
|
COMMENT 'Message d\'erreur en cas d\'échec'
|
||||||
|
AFTER `attempts`;
|
||||||
|
|
||||||
|
-- Ajouter les index pour optimiser les performances
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_status_attempts` (`status`, `attempts`);
|
||||||
|
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_sent_at` (`sent_at`);
|
||||||
|
|
||||||
|
-- Vérifier les modifications email_queue
|
||||||
|
DESCRIBE email_queue;
|
||||||
|
|
||||||
|
-- ⚠️ IMPORTANT : Ajouter le nouveau champ chk_user_delete_pass dans entites
|
||||||
|
source /var/www/geosector/api/scripts/sql/add_chk_user_delete_pass.sql;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ Test du script avant mise en production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Se connecter au serveur PROD
|
||||||
|
ssh user@prod-server
|
||||||
|
|
||||||
|
# Aller dans le répertoire de l'API
|
||||||
|
cd /var/www/geosector/api
|
||||||
|
|
||||||
|
# Rendre les scripts exécutables
|
||||||
|
chmod +x scripts/cron/process_email_queue.php
|
||||||
|
chmod +x scripts/cron/test_email_queue.php
|
||||||
|
|
||||||
|
# Tester l'état de la queue (lecture seule)
|
||||||
|
php scripts/cron/test_email_queue.php
|
||||||
|
|
||||||
|
# Si tout est OK, faire un test d'envoi sur 1 email
|
||||||
|
# (modifier temporairement BATCH_SIZE à 1 dans le script si nécessaire)
|
||||||
|
php scripts/cron/process_email_queue.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ Configuration du CRON avec logs journaliers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rendre le script wrapper exécutable
|
||||||
|
chmod +x /var/www/geosector/api/scripts/cron/process_email_queue_with_daily_log.sh
|
||||||
|
|
||||||
|
# Éditer le crontab
|
||||||
|
crontab -e
|
||||||
|
|
||||||
|
# Ajouter cette ligne pour exécution toutes les 5 minutes avec logs journaliers
|
||||||
|
*/5 * * * * /var/www/geosector/api/scripts/cron/process_email_queue_with_daily_log.sh
|
||||||
|
|
||||||
|
# Sauvegarder et quitter (:wq sous vi/vim)
|
||||||
|
|
||||||
|
# Vérifier que le cron est bien enregistré
|
||||||
|
crontab -l | grep email_queue
|
||||||
|
|
||||||
|
# Vérifier que le service cron est actif
|
||||||
|
systemctl status cron
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note** : Les logs seront créés automatiquement dans `/var/www/geosector/api/logs/` avec le format : `email_queue_20250820.log`, `email_queue_20250821.log`, etc. Les logs de plus de 30 jours sont supprimés automatiquement.
|
||||||
|
|
||||||
|
### 4️⃣ Surveillance post-déploiement
|
||||||
|
|
||||||
|
Pendant les premières heures après la mise en production :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Surveiller les logs en temps réel (fichier du jour)
|
||||||
|
tail -f /var/www/geosector/api/logs/email_queue_$(date +%Y%m%d).log
|
||||||
|
|
||||||
|
# Vérifier le statut de la queue
|
||||||
|
php scripts/cron/test_email_queue.php
|
||||||
|
|
||||||
|
# Compter les emails traités
|
||||||
|
mysql -u geo_app_user_prod -p geo_app -e "
|
||||||
|
SELECT status, COUNT(*) as count
|
||||||
|
FROM email_queue
|
||||||
|
WHERE DATE(created_at) = CURDATE()
|
||||||
|
GROUP BY status;"
|
||||||
|
|
||||||
|
# Vérifier les erreurs éventuelles
|
||||||
|
mysql -u geo_app_user_prod -p geo_app -e "
|
||||||
|
SELECT id, to_email, subject, attempts, error_message
|
||||||
|
FROM email_queue
|
||||||
|
WHERE status='failed'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 ROLLBACK (si nécessaire)
|
||||||
|
|
||||||
|
En cas de problème, voici comment revenir en arrière :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Stopper le cron
|
||||||
|
crontab -e
|
||||||
|
# Commenter la ligne du process_email_queue
|
||||||
|
|
||||||
|
# 2. Marquer les emails en attente pour traitement manuel
|
||||||
|
mysql -u geo_app_user_prod -p geo_app -e "
|
||||||
|
UPDATE email_queue
|
||||||
|
SET status='pending', attempts=0
|
||||||
|
WHERE status='failed' AND DATE(created_at) = CURDATE();"
|
||||||
|
|
||||||
|
# 3. Informer l'équipe pour traitement manuel si nécessaire
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 VALIDATION POST-DÉPLOIEMENT
|
||||||
|
|
||||||
|
### Critères de succès :
|
||||||
|
- [ ] Aucune erreur dans les logs
|
||||||
|
- [ ] Les emails sont envoyés dans les 5 minutes
|
||||||
|
- [ ] Les reçus PDF sont correctement attachés
|
||||||
|
- [ ] Le champ `date_sent_recu` est mis à jour dans `ope_pass`
|
||||||
|
- [ ] Pas d'accumulation d'emails en status 'pending'
|
||||||
|
|
||||||
|
### Commandes de vérification :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Statistiques générales
|
||||||
|
mysql -u geo_app_user_prod -p geo_app -e "
|
||||||
|
SELECT
|
||||||
|
status,
|
||||||
|
COUNT(*) as count,
|
||||||
|
MIN(created_at) as oldest,
|
||||||
|
MAX(created_at) as newest
|
||||||
|
FROM email_queue
|
||||||
|
GROUP BY status;"
|
||||||
|
|
||||||
|
# Vérifier les passages avec reçus envoyés aujourd'hui
|
||||||
|
mysql -u geo_app_user_prod -p geo_app -e "
|
||||||
|
SELECT COUNT(*) as recus_envoyes_aujourdhui
|
||||||
|
FROM ope_pass
|
||||||
|
WHERE DATE(date_sent_recu) = CURDATE();"
|
||||||
|
|
||||||
|
# Performance du cron (dernières exécutions du jour)
|
||||||
|
tail -20 /var/www/geosector/api/logs/email_queue_$(date +%Y%m%d).log | grep "Traitement terminé"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 CONTACTS EN CAS DE PROBLÈME
|
||||||
|
|
||||||
|
- **Responsable technique** : _____________
|
||||||
|
- **DBA** : _____________
|
||||||
|
- **Support O2Switch** : support@o2switch.fr
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 NOTES IMPORTANTES
|
||||||
|
|
||||||
|
1. **Limite d'envoi** : 1500 emails/heure max (limite O2Switch)
|
||||||
|
2. **Batch size** : 50 emails par exécution (toutes les 5 min = 600/heure max)
|
||||||
|
3. **Lock file** : `/tmp/process_email_queue.lock` empêche l'exécution simultanée
|
||||||
|
4. **Nettoyage auto** : Les emails envoyés > 30 jours sont supprimés automatiquement
|
||||||
|
|
||||||
|
## 🔒 SÉCURITÉ
|
||||||
|
|
||||||
|
- Les mots de passe SMTP ne sont jamais loggués
|
||||||
|
- Les emails en erreur conservent le message d'erreur pour diagnostic
|
||||||
|
- Le PDF est envoyé en pièce jointe encodée en base64
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ CHECKLIST FINALE
|
||||||
|
|
||||||
|
### Email Queue :
|
||||||
|
- [ ] Table email_queue mise à jour (sent_at, error_message, index)
|
||||||
|
- [ ] Scripts cron testés avec succès
|
||||||
|
- [ ] Cron configuré et actif
|
||||||
|
- [ ] Logs accessibles et fonctionnels
|
||||||
|
- [ ] Premier batch d'emails envoyé avec succès
|
||||||
|
|
||||||
|
### Permissions Suppression Passages :
|
||||||
|
- [ ] Champ chk_user_delete_pass ajouté dans la table entites
|
||||||
|
- [ ] EntiteController.php mis à jour pour gérer le nouveau champ
|
||||||
|
- [ ] LoginController.php mis à jour pour retourner le champ dans amicale
|
||||||
|
- [ ] Test de modification de permissions via l'interface admin
|
||||||
|
|
||||||
|
### Général :
|
||||||
|
- [ ] Documentation mise à jour
|
||||||
|
- [ ] Équipe informée de la mise en production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Date de mise en production** : _______________
|
||||||
|
**Validé par** : _______________
|
||||||
|
**Signature** : _______________
|
||||||
149
api/docs/SETUP_EMAIL_QUEUE_CRON.md
Normal file
149
api/docs/SETUP_EMAIL_QUEUE_CRON.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Instructions de mise en place du CRON pour la queue d'emails
|
||||||
|
|
||||||
|
## Problème résolu
|
||||||
|
Les emails de reçus étaient insérés dans la table `email_queue` mais n'étaient jamais envoyés car il manquait le script de traitement.
|
||||||
|
|
||||||
|
## Fichiers créés
|
||||||
|
1. `/scripts/cron/process_email_queue.php` - Script principal de traitement
|
||||||
|
2. `/scripts/cron/test_email_queue.php` - Script de test/diagnostic
|
||||||
|
3. `/scripts/sql/add_email_queue_fields.sql` - Migration SQL pour les champs manquants
|
||||||
|
|
||||||
|
## Installation sur les serveurs (DVA, REC, PROD)
|
||||||
|
|
||||||
|
### 1. Appliquer la migration SQL
|
||||||
|
|
||||||
|
Se connecter à la base de données et exécuter :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysql -u [user] -p [database] < /path/to/api/scripts/sql/add_email_queue_fields.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou directement dans MySQL :
|
||||||
|
```sql
|
||||||
|
-- Ajouter les champs manquants
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD COLUMN IF NOT EXISTS `sent_at` TIMESTAMP NULL DEFAULT NULL AFTER `status`;
|
||||||
|
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD COLUMN IF NOT EXISTS `error_message` TEXT NULL DEFAULT NULL AFTER `attempts`;
|
||||||
|
|
||||||
|
-- Ajouter les index pour les performances
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_status_attempts` (`status`, `attempts`);
|
||||||
|
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_sent_at` (`sent_at`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Tester le script
|
||||||
|
|
||||||
|
Avant de mettre en place le cron, tester que tout fonctionne :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier l'état de la queue
|
||||||
|
php /path/to/api/scripts/cron/test_email_queue.php
|
||||||
|
|
||||||
|
# Tester l'envoi (traite jusqu'à 50 emails)
|
||||||
|
php /path/to/api/scripts/cron/process_email_queue.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configurer le CRON
|
||||||
|
|
||||||
|
Ajouter la ligne suivante dans le crontab du serveur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Éditer le crontab
|
||||||
|
crontab -e
|
||||||
|
|
||||||
|
# Ajouter cette ligne (exécution toutes les 5 minutes)
|
||||||
|
*/5 * * * * /usr/bin/php /path/to/api/scripts/cron/process_email_queue.php >> /var/log/email_queue.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options de fréquence :**
|
||||||
|
- `*/5 * * * *` - Toutes les 5 minutes (recommandé)
|
||||||
|
- `*/10 * * * *` - Toutes les 10 minutes
|
||||||
|
- `*/2 * * * *` - Toutes les 2 minutes (si volume important)
|
||||||
|
|
||||||
|
### 4. Monitoring
|
||||||
|
|
||||||
|
Le script génère des logs via `LogService`. Vérifier les logs dans :
|
||||||
|
- `/path/to/api/logs/` (selon la configuration)
|
||||||
|
|
||||||
|
Points à surveiller :
|
||||||
|
- Nombre d'emails traités
|
||||||
|
- Emails en échec après 3 tentatives
|
||||||
|
- Erreurs de connexion SMTP
|
||||||
|
|
||||||
|
### 5. Configuration SMTP
|
||||||
|
|
||||||
|
Vérifier que la configuration SMTP est correcte dans `AppConfig` :
|
||||||
|
- Host SMTP
|
||||||
|
- Port (587 pour TLS, 465 pour SSL)
|
||||||
|
- Username/Password
|
||||||
|
- Encryption (tls ou ssl)
|
||||||
|
- From Email/Name
|
||||||
|
|
||||||
|
## Fonctionnement du script
|
||||||
|
|
||||||
|
### Caractéristiques
|
||||||
|
- **Batch size** : 50 emails par exécution
|
||||||
|
- **Max tentatives** : 3 essais par email
|
||||||
|
- **Lock file** : Empêche l'exécution simultanée
|
||||||
|
- **Nettoyage** : Supprime les emails envoyés > 30 jours
|
||||||
|
- **Pause** : 0.5s entre chaque email (anti-spam)
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
1. Récupère les emails avec `status = 'pending'` et `attempts < 3`
|
||||||
|
2. Pour chaque email :
|
||||||
|
- Incrémente le compteur de tentatives
|
||||||
|
- Envoie via PHPMailer avec la config SMTP
|
||||||
|
- Si succès : `status = 'sent'` + mise à jour du passage
|
||||||
|
- Si échec : réessai à la prochaine exécution
|
||||||
|
- Après 3 échecs : `status = 'failed'`
|
||||||
|
|
||||||
|
### Tables mises à jour
|
||||||
|
- `email_queue` : status, attempts, sent_at, error_message
|
||||||
|
- `ope_pass` : date_sent_recu, chk_email_sent
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voir les emails en attente
|
||||||
|
mysql -e "SELECT COUNT(*) FROM email_queue WHERE status='pending'" [database]
|
||||||
|
|
||||||
|
# Voir les emails échoués
|
||||||
|
mysql -e "SELECT * FROM email_queue WHERE status='failed' ORDER BY created_at DESC LIMIT 10" [database]
|
||||||
|
|
||||||
|
# Réinitialiser un email échoué pour réessai
|
||||||
|
mysql -e "UPDATE email_queue SET status='pending', attempts=0 WHERE id=[ID]" [database]
|
||||||
|
|
||||||
|
# Voir les logs du cron
|
||||||
|
tail -f /var/log/email_queue.log
|
||||||
|
|
||||||
|
# Vérifier que le cron est actif
|
||||||
|
crontab -l | grep process_email_queue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Le cron ne s'exécute pas
|
||||||
|
- Vérifier les permissions : `chmod +x process_email_queue.php`
|
||||||
|
- Vérifier le chemin PHP : `which php`
|
||||||
|
- Vérifier les logs système : `/var/log/syslog` ou `/var/log/cron`
|
||||||
|
|
||||||
|
### Emails en échec
|
||||||
|
- Vérifier la config SMTP avec `test_email_queue.php`
|
||||||
|
- Vérifier les logs pour les messages d'erreur
|
||||||
|
- Tester la connexion SMTP : `telnet [smtp_host] [port]`
|
||||||
|
|
||||||
|
### Lock bloqué
|
||||||
|
Si le message "Le processus est déjà en cours" persiste :
|
||||||
|
```bash
|
||||||
|
rm /tmp/process_email_queue.lock
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contact support
|
||||||
|
En cas de problème, vérifier :
|
||||||
|
1. Les logs de l'application
|
||||||
|
2. La table `email_queue` pour les messages d'erreur
|
||||||
|
3. La configuration SMTP dans AppConfig
|
||||||
@@ -8,8 +8,9 @@
|
|||||||
4. [Architecture des composants](#architecture-des-composants)
|
4. [Architecture des composants](#architecture-des-composants)
|
||||||
5. [Base de données](#base-de-données)
|
5. [Base de données](#base-de-données)
|
||||||
6. [Sécurité](#sécurité)
|
6. [Sécurité](#sécurité)
|
||||||
7. [Endpoints API](#endpoints-api)
|
7. [Gestion des mots de passe (NIST SP 800-63B)](#gestion-des-mots-de-passe-nist-sp-800-63b)
|
||||||
8. [Changements récents](#changements-récents)
|
8. [Endpoints API](#endpoints-api)
|
||||||
|
9. [Changements récents](#changements-récents)
|
||||||
|
|
||||||
## Structure du projet
|
## Structure du projet
|
||||||
|
|
||||||
@@ -189,6 +190,211 @@ ADD INDEX `idx_encrypted_user_name` (`encrypted_user_name`);
|
|||||||
- Chiffrement AES-256 des données sensibles
|
- Chiffrement AES-256 des données sensibles
|
||||||
- Envoi séparé des identifiants par email
|
- Envoi séparé des identifiants par email
|
||||||
|
|
||||||
|
## Gestion des mots de passe (NIST SP 800-63B)
|
||||||
|
|
||||||
|
### Vue d'ensemble
|
||||||
|
|
||||||
|
L'API implémente un système de gestion des mots de passe conforme aux recommandations NIST SP 800-63B, avec quelques adaptations spécifiques demandées par le client.
|
||||||
|
|
||||||
|
### Service PasswordSecurityService
|
||||||
|
|
||||||
|
Le service `PasswordSecurityService` (`src/Services/PasswordSecurityService.php`) gère :
|
||||||
|
- Validation des mots de passe selon NIST
|
||||||
|
- Vérification contre les bases de données de mots de passe compromis (HIBP)
|
||||||
|
- Génération de mots de passe sécurisés
|
||||||
|
- Estimation de la force des mots de passe
|
||||||
|
|
||||||
|
### Conformités NIST respectées
|
||||||
|
|
||||||
|
| Recommandation NIST | Notre Implémentation | Status |
|
||||||
|
|-------------------|---------------------|--------|
|
||||||
|
| **Longueur minimale : 8 caractères** | ✅ MIN = 8 caractères | ✅ CONFORME |
|
||||||
|
| **Longueur maximale : 64 caractères minimum** | ✅ MAX = 64 caractères | ✅ CONFORME |
|
||||||
|
| **Accepter TOUS les caractères ASCII imprimables** | ✅ Aucune restriction sur les caractères | ✅ CONFORME |
|
||||||
|
| **Accepter les espaces** | ✅ Espaces acceptés (début, milieu, fin) | ✅ CONFORME |
|
||||||
|
| **Accepter Unicode (émojis, accents, etc.)** | ✅ Support UTF-8 avec `mb_strlen()` | ✅ CONFORME |
|
||||||
|
| **Vérifier contre les mots de passe compromis** | ✅ API Have I Been Pwned avec k-anonymity | ✅ CONFORME |
|
||||||
|
| **Pas d'obligation de composition** | ✅ Pas d'erreur si manque majuscules/chiffres/spéciaux | ✅ CONFORME |
|
||||||
|
| **Pas de changement périodique forcé** | ✅ Aucune expiration automatique | ✅ CONFORME |
|
||||||
|
| **Permettre les phrases de passe** | ✅ "Mon chat Félix a 3 ans!" accepté | ✅ CONFORME |
|
||||||
|
|
||||||
|
### Déviations par choix du client
|
||||||
|
|
||||||
|
| Recommandation NIST | Notre Implémentation | Raison |
|
||||||
|
|-------------------|---------------------|--------|
|
||||||
|
| **Email unique par compte** | ❌ Plusieurs comptes par email autorisés | Demande client |
|
||||||
|
| **Mot de passe ≠ identifiant** | ❌ Mot de passe = identifiant autorisé | Demande client |
|
||||||
|
| **Vérifier contexte utilisateur** | ❌ Pas de vérification nom/email dans mdp | Demande client |
|
||||||
|
|
||||||
|
### Vérification contre les mots de passe compromis
|
||||||
|
|
||||||
|
#### Have I Been Pwned (HIBP) API
|
||||||
|
|
||||||
|
L'implémentation utilise l'API HIBP avec la technique **k-anonymity** pour préserver la confidentialité :
|
||||||
|
|
||||||
|
1. **Hash SHA-1** du mot de passe
|
||||||
|
2. **Envoi des 5 premiers caractères** du hash à l'API
|
||||||
|
3. **Comparaison locale** avec les suffixes retournés
|
||||||
|
4. **Aucun mot de passe en clair** n'est transmis
|
||||||
|
|
||||||
|
#### Mode "Fail Open"
|
||||||
|
|
||||||
|
En cas d'erreur de l'API HIBP :
|
||||||
|
- Le système laisse passer le mot de passe
|
||||||
|
- Un avertissement est enregistré dans les logs
|
||||||
|
- L'utilisateur n'est pas bloqué
|
||||||
|
|
||||||
|
### Exemples de mots de passe
|
||||||
|
|
||||||
|
#### Acceptés (conformes NIST)
|
||||||
|
- `monmotdepasse` → Accepté (≥8 caractères, pas compromis)
|
||||||
|
- `12345678` → Accepté SI pas dans HIBP
|
||||||
|
- `Mon chat s'appelle Félix!` → Accepté (phrase de passe)
|
||||||
|
- ` ` → Accepté si ≥8 espaces
|
||||||
|
- `😀🎉🎈🎁🎂🍰🎊🎀` → Accepté (8 émojis)
|
||||||
|
- `jean.dupont` → Accepté même si = username
|
||||||
|
|
||||||
|
#### Refusés
|
||||||
|
- `pass123` → Refusé (< 8 caractères)
|
||||||
|
- `password` → Refusé (compromis dans HIBP)
|
||||||
|
- `123456789` → Refusé (compromis dans HIBP)
|
||||||
|
- Mot de passe > 64 caractères → Refusé
|
||||||
|
|
||||||
|
### Force des mots de passe
|
||||||
|
|
||||||
|
Le système privilégie la **LONGUEUR** sur la complexité (conforme NIST) :
|
||||||
|
|
||||||
|
| Longueur | Force | Score |
|
||||||
|
|----------|-------|-------|
|
||||||
|
| < 8 car. | Trop court | 0-10 |
|
||||||
|
| 8-11 car. | Acceptable | 20-40 |
|
||||||
|
| 12-15 car. | Bon | 40-60 |
|
||||||
|
| 16-19 car. | Fort | 60-80 |
|
||||||
|
| ≥20 car. | Très fort | 80-100 |
|
||||||
|
| Compromis | Compromis | ≤10 |
|
||||||
|
|
||||||
|
### Génération automatique
|
||||||
|
|
||||||
|
Pour la génération automatique, le système reste **strict** pour garantir des mots de passe forts :
|
||||||
|
- Longueur : 12-16 caractères
|
||||||
|
- Contient : majuscules + minuscules + chiffres + spéciaux
|
||||||
|
- Vérifié contre HIBP (10 tentatives max)
|
||||||
|
- Exemple : `Xk9#mP2$nL5!`
|
||||||
|
|
||||||
|
### Gestion des comptes multiples par email
|
||||||
|
|
||||||
|
Depuis janvier 2025, le système permet plusieurs comptes avec le même email :
|
||||||
|
|
||||||
|
#### Fonction `lostPassword` adaptée
|
||||||
|
- Recherche **TOUS** les comptes avec l'email fourni
|
||||||
|
- Génère **UN SEUL** mot de passe pour tous ces comptes
|
||||||
|
- Met à jour **TOUS** les comptes en une requête
|
||||||
|
- Envoie **UN SEUL** email avec la liste des usernames concernés
|
||||||
|
|
||||||
|
#### Exemple de comportement
|
||||||
|
Si 3 comptes partagent l'email `contact@amicale.fr` :
|
||||||
|
- `jean.dupont`
|
||||||
|
- `marie.martin`
|
||||||
|
- `paul.durand`
|
||||||
|
|
||||||
|
L'email contiendra :
|
||||||
|
```
|
||||||
|
Bonjour,
|
||||||
|
Voici votre nouveau mot de passe pour les comptes : jean.dupont, marie.martin, paul.durand
|
||||||
|
Mot de passe : XyZ123!@#
|
||||||
|
```
|
||||||
|
|
||||||
|
### Endpoints API dédiés aux mots de passe
|
||||||
|
|
||||||
|
#### Vérification de force (public)
|
||||||
|
```http
|
||||||
|
POST /api/password/check
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"password": "monmotdepasse",
|
||||||
|
"check_compromised": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"valid": false,
|
||||||
|
"errors": [
|
||||||
|
"Ce mot de passe a été trouvé 23 547 fois dans des fuites de données."
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"Suggestion : Évitez les séquences communes pour plus de sécurité"
|
||||||
|
],
|
||||||
|
"strength": {
|
||||||
|
"score": 20,
|
||||||
|
"strength": "Faible",
|
||||||
|
"feedback": ["Ce mot de passe a été compromis"],
|
||||||
|
"length": 13,
|
||||||
|
"diversity": 1
|
||||||
|
},
|
||||||
|
"compromised": {
|
||||||
|
"compromised": true,
|
||||||
|
"occurrences": 23547,
|
||||||
|
"message": "Ce mot de passe a été trouvé 23 547 fois dans des fuites de données"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Vérification de compromission uniquement (public)
|
||||||
|
```http
|
||||||
|
POST /api/password/compromised
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"password": "monmotdepasse"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Génération automatique (authentifié)
|
||||||
|
```http
|
||||||
|
GET /api/password/generate?length=14
|
||||||
|
Authorization: Bearer {session_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"password": "Xk9#mP2$nL5!qR",
|
||||||
|
"length": 14,
|
||||||
|
"strength": {
|
||||||
|
"score": 85,
|
||||||
|
"strength": "Très fort",
|
||||||
|
"feedback": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration et sécurité
|
||||||
|
|
||||||
|
#### Paramètres de sécurité
|
||||||
|
- **Timeout API HIBP** : 5 secondes
|
||||||
|
- **Cache** : 15 minutes pour les vérifications répétées
|
||||||
|
- **Logging** : Aucun mot de passe en clair dans les logs
|
||||||
|
- **K-anonymity** : Seuls 5 caractères du hash SHA-1 envoyés
|
||||||
|
|
||||||
|
#### Points d'intégration
|
||||||
|
- `LoginController::register` : Validation lors de l'inscription
|
||||||
|
- `LoginController::lostPassword` : Génération sécurisée
|
||||||
|
- `UserController::createUser` : Validation si mot de passe manuel
|
||||||
|
- `UserController::updateUser` : Validation lors du changement
|
||||||
|
- `ApiService::generateSecurePassword` : Génération avec vérification HIBP
|
||||||
|
|
||||||
|
### Résumé
|
||||||
|
|
||||||
|
✅ **100% CONFORME NIST** pour les aspects techniques
|
||||||
|
✅ **Adapté aux besoins du client** (emails multiples, mdp=username)
|
||||||
|
✅ **Sécurité maximale** avec vérification HIBP
|
||||||
|
✅ **Expérience utilisateur optimale** (souple mais sécurisé)
|
||||||
|
|
||||||
## Endpoints API
|
## Endpoints API
|
||||||
|
|
||||||
### Routes Publiques vs Privées
|
### Routes Publiques vs Privées
|
||||||
@@ -573,7 +779,25 @@ fetch('/api/endpoint', {
|
|||||||
|
|
||||||
## Changements récents
|
## Changements récents
|
||||||
|
|
||||||
### Version 3.0.6 (Janvier 2025)
|
### Version 3.0.7 (Août 2025)
|
||||||
|
|
||||||
|
#### 1. Implémentation complète de la norme NIST SP 800-63B pour les mots de passe
|
||||||
|
- **Nouveau service :** `PasswordSecurityService` pour la gestion sécurisée des mots de passe
|
||||||
|
- **Vérification HIBP :** Intégration de l'API Have I Been Pwned avec k-anonymity
|
||||||
|
- **Validation souple :** Suppression des obligations de composition (majuscules, chiffres, spéciaux)
|
||||||
|
- **Support Unicode :** Acceptation de tous les caractères, incluant émojis et espaces
|
||||||
|
- **Nouveaux endpoints :** `/api/password/check`, `/api/password/compromised`, `/api/password/generate`
|
||||||
|
|
||||||
|
#### 2. Autorisation des emails multiples
|
||||||
|
- **Suppression de l'unicité :** Un même email peut être utilisé pour plusieurs comptes
|
||||||
|
- **Adaptation de `lostPassword` :** Mise à jour de tous les comptes partageant l'email
|
||||||
|
- **Un seul mot de passe :** Tous les comptes avec le même email reçoivent le même nouveau mot de passe
|
||||||
|
|
||||||
|
#### 3. Autorisation mot de passe = identifiant
|
||||||
|
- **Choix client :** Permet d'avoir un mot de passe identique au nom d'utilisateur
|
||||||
|
- **Pas de vérification contextuelle :** Aucune vérification nom/email dans le mot de passe
|
||||||
|
|
||||||
|
### Version 3.0.6 (Août 2025)
|
||||||
|
|
||||||
#### 1. Correction des rôles administrateurs
|
#### 1. Correction des rôles administrateurs
|
||||||
- **Avant :** Les administrateurs d'amicale devaient avoir `fk_role > 2`
|
- **Avant :** Les administrateurs d'amicale devaient avoir `fk_role > 2`
|
||||||
@@ -612,3 +836,28 @@ fetch('/api/endpoint', {
|
|||||||
- **Format d'envoi des images :** Base64 data URL pour compatibilité multiplateforme
|
- **Format d'envoi des images :** Base64 data URL pour compatibilité multiplateforme
|
||||||
- **Structure de réponse enrichie :** Le logo est inclus dans l'objet `amicale` lors du login
|
- **Structure de réponse enrichie :** Le logo est inclus dans l'objet `amicale` lors du login
|
||||||
- **Optimisation :** Pas de requête HTTP supplémentaire nécessaire pour afficher le logo
|
- **Optimisation :** Pas de requête HTTP supplémentaire nécessaire pour afficher le logo
|
||||||
|
|
||||||
|
### Version 3.0.8 (Janvier 2025)
|
||||||
|
|
||||||
|
#### 1. Système de génération automatique de reçus fiscaux pour les dons
|
||||||
|
- **Nouveau service :** `ReceiptService` pour la génération automatique de reçus PDF
|
||||||
|
- **Déclencheurs automatiques :**
|
||||||
|
- Création d'un passage avec `fk_type=1` (don) et email valide
|
||||||
|
- Mise à jour d'un passage en don si `nom_recu` est vide/null
|
||||||
|
- **Caractéristiques techniques :**
|
||||||
|
- PDF ultra-légers (< 5KB) générés en format natif sans librairie externe
|
||||||
|
- Support des caractères accentués avec conversion automatique
|
||||||
|
- Stockage structuré : `/uploads/entites/{entite_id}/recus/{operation_id}/`
|
||||||
|
- Enregistrement dans la table `medias` avec catégorie `recu`
|
||||||
|
- **Queue d'envoi email :**
|
||||||
|
- Envoi automatique par email avec pièce jointe PDF
|
||||||
|
- Format MIME multipart pour compatibilité maximale
|
||||||
|
- Gestion dans la table `email_queue` avec statut de suivi
|
||||||
|
- **Nouvelle route API :**
|
||||||
|
- `GET /api/passages/{id}/receipt` : Récupération du PDF d'un reçu
|
||||||
|
- Retourne le PDF en base64 ou téléchargement direct selon Accept header
|
||||||
|
- **Champs base de données utilisés :**
|
||||||
|
- `nom_recu` : Nom du fichier PDF généré
|
||||||
|
- `date_creat_recu` : Date de génération du reçu
|
||||||
|
- `date_sent_recu` : Date d'envoi par email
|
||||||
|
- `chk_email_sent` : Indicateur d'envoi réussi
|
||||||
|
|||||||
155
api/docs/UPLOAD-MIGRATION-RECAP.md
Normal file
155
api/docs/UPLOAD-MIGRATION-RECAP.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# 📋 RÉCAPITULATIF - Migration Arborescence Uploads
|
||||||
|
|
||||||
|
## ✅ Modifications effectuées
|
||||||
|
|
||||||
|
### 1. **EntiteController.php** (ligne 736)
|
||||||
|
```php
|
||||||
|
// Avant : "/entites/{$entiteId}/logo"
|
||||||
|
// Après : "/{$entiteId}/logo"
|
||||||
|
```
|
||||||
|
✅ Les logos sont maintenant stockés dans : `uploads/{entite_id}/logo/`
|
||||||
|
|
||||||
|
### 2. **ReceiptService.php** (ligne 95)
|
||||||
|
```php
|
||||||
|
// Avant : "/entites/{$entiteId}/recus/{$operationId}"
|
||||||
|
// Après : "/{$entiteId}/recus/{$operationId}"
|
||||||
|
```
|
||||||
|
✅ Les reçus PDF sont maintenant stockés dans : `uploads/{entite_id}/recus/{operation_id}/`
|
||||||
|
|
||||||
|
### 3. **ExportService.php** (lignes 40 et 141)
|
||||||
|
```php
|
||||||
|
// Avant Excel : "/{$entiteId}/operations/{$operationId}/exports/excel"
|
||||||
|
// Après Excel : "/{$entiteId}/operations/{$operationId}"
|
||||||
|
|
||||||
|
// Avant JSON : "/{$entiteId}/operations/{$operationId}/exports/json"
|
||||||
|
// Après JSON : "/{$entiteId}/operations/{$operationId}"
|
||||||
|
```
|
||||||
|
✅ Les exports sont maintenant stockés directement dans : `uploads/{entite_id}/operations/{operation_id}/`
|
||||||
|
|
||||||
|
## 📂 Nouvelle structure complète
|
||||||
|
|
||||||
|
```
|
||||||
|
uploads/
|
||||||
|
└── {entite_id}/ # Ex: 5, 1230, etc.
|
||||||
|
├── logo/ # Logo de l'entité
|
||||||
|
│ └── logo_{entite_id}_{timestamp}.{jpg|png}
|
||||||
|
├── operations/ # Exports d'opérations
|
||||||
|
│ └── {operation_id}/ # Ex: 1525, 3124
|
||||||
|
│ ├── geosector-export-{operation_id}-{timestamp}.xlsx
|
||||||
|
│ └── backup-{operation_id}-{timestamp}.json.enc
|
||||||
|
└── recus/ # Reçus fiscaux
|
||||||
|
└── {operation_id}/ # Ex: 3124
|
||||||
|
└── recu_{passage_id}.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Script de migration
|
||||||
|
|
||||||
|
Un script a été créé pour migrer les fichiers existants :
|
||||||
|
|
||||||
|
**Fichier :** `/scripts/migrate_uploads_structure.php`
|
||||||
|
|
||||||
|
**Usage :**
|
||||||
|
```bash
|
||||||
|
# Mode simulation (voir ce qui sera fait sans modifier)
|
||||||
|
php scripts/migrate_uploads_structure.php --dry-run
|
||||||
|
|
||||||
|
# Mode réel (effectue la migration)
|
||||||
|
php scripts/migrate_uploads_structure.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ce que fait le script :**
|
||||||
|
1. Déplace tout le contenu de `uploads/entites/*` vers `uploads/*`
|
||||||
|
2. Fusionne les dossiers si nécessaire
|
||||||
|
3. Simplifie la structure des exports (supprime `/documents/exports/excel/`)
|
||||||
|
4. Applique les bonnes permissions (nginx:nobody 775/664)
|
||||||
|
5. Crée un log détaillé dans `/logs/migration_uploads_YYYYMMDD_HHMMSS.log`
|
||||||
|
|
||||||
|
## 🚀 Procédure de déploiement
|
||||||
|
|
||||||
|
### Sur DEV (déjà fait)
|
||||||
|
✅ Code modifié
|
||||||
|
✅ Script de migration créé
|
||||||
|
|
||||||
|
### Sur REC
|
||||||
|
```bash
|
||||||
|
# 1. Déployer le nouveau code
|
||||||
|
./livre-api.sh rec
|
||||||
|
|
||||||
|
# 2. Faire un backup des uploads actuels
|
||||||
|
cd /var/www/geosector/api
|
||||||
|
tar -czf uploads_backup_$(date +%Y%m%d).tar.gz uploads/
|
||||||
|
|
||||||
|
# 3. Tester en mode dry-run
|
||||||
|
php scripts/migrate_uploads_structure.php --dry-run
|
||||||
|
|
||||||
|
# 4. Si OK, lancer la migration
|
||||||
|
php scripts/migrate_uploads_structure.php
|
||||||
|
|
||||||
|
# 5. Vérifier la nouvelle structure
|
||||||
|
ls -la uploads/
|
||||||
|
ls -la uploads/*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sur PROD
|
||||||
|
Même procédure que REC après validation
|
||||||
|
|
||||||
|
## ⚠️ Points d'attention
|
||||||
|
|
||||||
|
1. **Backup obligatoire** avant migration
|
||||||
|
2. **Vérifier l'espace disque** disponible
|
||||||
|
3. **Tester d'abord en dry-run**
|
||||||
|
4. **Surveiller les logs** après migration
|
||||||
|
5. **Tester** upload logo, génération reçu, et export Excel
|
||||||
|
|
||||||
|
## 📊 Gains obtenus
|
||||||
|
|
||||||
|
| Aspect | Avant | Après |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| **Profondeur max** | 8 niveaux | 4 niveaux |
|
||||||
|
| **Complexité** | 2 structures parallèles | 1 structure unique |
|
||||||
|
| **Clarté** | Confus (entites + racine) | Simple et logique |
|
||||||
|
| **Navigation** | Difficile | Intuitive |
|
||||||
|
|
||||||
|
## 🔍 Vérification post-migration
|
||||||
|
|
||||||
|
Après la migration, vérifier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Structure attendue pour l'entité 5
|
||||||
|
tree uploads/5/
|
||||||
|
# Devrait afficher :
|
||||||
|
# uploads/5/
|
||||||
|
# ├── logo/
|
||||||
|
# │ └── logo_5_*.png
|
||||||
|
# ├── operations/
|
||||||
|
# │ ├── 1525/
|
||||||
|
# │ │ └── *.xlsx
|
||||||
|
# │ └── 3124/
|
||||||
|
# │ └── *.xlsx
|
||||||
|
# └── recus/
|
||||||
|
# └── 3124/
|
||||||
|
# └── recu_*.pdf
|
||||||
|
|
||||||
|
# Vérifier les permissions
|
||||||
|
ls -la uploads/*/
|
||||||
|
# Devrait montrer : nginx:nobody avec 775 pour dossiers, 664 pour fichiers
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Checklist finale
|
||||||
|
|
||||||
|
- [ ] Code modifié et testé en DEV
|
||||||
|
- [ ] Script de migration créé
|
||||||
|
- [ ] Documentation mise à jour
|
||||||
|
- [ ] Backup effectué sur REC
|
||||||
|
- [ ] Migration testée en dry-run sur REC
|
||||||
|
- [ ] Migration exécutée sur REC
|
||||||
|
- [ ] Tests fonctionnels sur REC
|
||||||
|
- [ ] Backup effectué sur PROD
|
||||||
|
- [ ] Migration exécutée sur PROD
|
||||||
|
- [ ] Tests fonctionnels sur PROD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Date de création :** 20/08/2025
|
||||||
|
**Auteur :** Assistant Claude
|
||||||
|
**Status :** Prêt pour déploiement
|
||||||
93
api/docs/UPLOAD-REORGANIZATION.md
Normal file
93
api/docs/UPLOAD-REORGANIZATION.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Réorganisation de l'arborescence des uploads
|
||||||
|
|
||||||
|
## 📅 Date : 20/08/2025
|
||||||
|
|
||||||
|
## 🎯 Objectif
|
||||||
|
Uniformiser et simplifier l'arborescence des fichiers uploads pour une meilleure organisation et maintenance.
|
||||||
|
|
||||||
|
## 📂 Arborescence actuelle (PROBLÈME)
|
||||||
|
```
|
||||||
|
uploads/
|
||||||
|
├── entites/
|
||||||
|
│ └── 5/
|
||||||
|
│ ├── logo/
|
||||||
|
│ ├── operations/
|
||||||
|
│ │ └── 1525/
|
||||||
|
│ │ └── documents/
|
||||||
|
│ │ └── exports/
|
||||||
|
│ │ └── excel/
|
||||||
|
│ │ └── geosector-export-*.xlsx
|
||||||
|
│ └── recus/
|
||||||
|
│ └── 3124/
|
||||||
|
│ └── recu_*.pdf
|
||||||
|
└── 5/
|
||||||
|
└── operations/
|
||||||
|
├── 1525/
|
||||||
|
└── 2021/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problèmes identifiés :**
|
||||||
|
- Duplication des structures (dossier `5` à la racine ET dans `entites/`)
|
||||||
|
- Chemins trop profonds pour les exports Excel (6 niveaux)
|
||||||
|
- Incohérence dans les chemins
|
||||||
|
|
||||||
|
## ✅ Nouvelle arborescence (SOLUTION)
|
||||||
|
```
|
||||||
|
uploads/
|
||||||
|
└── {entite_id}/ # Un seul dossier par entité à la racine
|
||||||
|
├── logo/ # Logo de l'entité
|
||||||
|
│ └── logo_*.{jpg,png}
|
||||||
|
├── operations/ # Exports par opération
|
||||||
|
│ └── {operation_id}/
|
||||||
|
│ └── *.xlsx # Exports Excel directement ici
|
||||||
|
└── recus/ # Reçus par opération
|
||||||
|
└── {operation_id}/
|
||||||
|
└── recu_*.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Fichiers à modifier
|
||||||
|
|
||||||
|
### 1. EntiteController.php (Upload logo)
|
||||||
|
**Actuel :** `/entites/{$entiteId}/logo`
|
||||||
|
**Nouveau :** `/{$entiteId}/logo`
|
||||||
|
|
||||||
|
### 2. ReceiptService.php (Stockage reçus PDF)
|
||||||
|
**Actuel :** `/entites/{$entiteId}/recus/{$operationId}`
|
||||||
|
**Nouveau :** `/{$entiteId}/recus/{$operationId}`
|
||||||
|
|
||||||
|
### 3. ExportService.php (Export Excel)
|
||||||
|
**Actuel :** `/{$entiteId}/operations/{$operationId}/exports/excel`
|
||||||
|
**Nouveau :** `/{$entiteId}/operations/{$operationId}`
|
||||||
|
|
||||||
|
### 4. ExportService.php (Export JSON)
|
||||||
|
**Actuel :** `/{$entiteId}/operations/{$operationId}/exports/json`
|
||||||
|
**Nouveau :** `/{$entiteId}/operations/{$operationId}` (ou supprimer si non utilisé)
|
||||||
|
|
||||||
|
## 🔄 Plan de migration
|
||||||
|
|
||||||
|
### Étape 1 : Modifier le code
|
||||||
|
1. Mettre à jour tous les chemins dans les contrôleurs et services
|
||||||
|
2. Tester en environnement DEV
|
||||||
|
|
||||||
|
### Étape 2 : Script de migration des fichiers existants
|
||||||
|
Créer un script PHP pour :
|
||||||
|
1. Lister tous les fichiers existants
|
||||||
|
2. Les déplacer vers la nouvelle structure
|
||||||
|
3. Supprimer les anciens dossiers vides
|
||||||
|
|
||||||
|
### Étape 3 : Déploiement
|
||||||
|
1. Exécuter le script de migration sur REC
|
||||||
|
2. Vérifier le bon fonctionnement
|
||||||
|
3. Exécuter sur PROD
|
||||||
|
|
||||||
|
## 🚀 Avantages de la nouvelle structure
|
||||||
|
- **Plus simple** : Chemins plus courts et plus logiques
|
||||||
|
- **Plus cohérent** : Une seule structure pour toutes les entités
|
||||||
|
- **Plus maintenable** : Facile de naviguer et comprendre
|
||||||
|
- **Performance** : Moins de niveaux de dossiers à parcourir
|
||||||
|
|
||||||
|
## ⚠️ Points d'attention
|
||||||
|
- Vérifier les permissions (nginx:nobody 775/664)
|
||||||
|
- S'assurer que les anciens fichiers sont bien migrés
|
||||||
|
- Mettre à jour la documentation
|
||||||
|
- Informer l'équipe du changement
|
||||||
135
api/docs/USERNAME_VALIDATION_CHANGES.md
Normal file
135
api/docs/USERNAME_VALIDATION_CHANGES.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Changements de validation des usernames - Version ultra-souple
|
||||||
|
|
||||||
|
## Date : 17 janvier 2025
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
Suite aux problèmes d'erreurs 400 et au besoin d'avoir une approche plus moderne et inclusive, les règles de validation des usernames ont été assouplies pour accepter tous les caractères UTF-8, similaire à l'approche NIST pour les mots de passe.
|
||||||
|
|
||||||
|
## Anciennes règles (trop restrictives)
|
||||||
|
- ❌ 10-30 caractères
|
||||||
|
- ❌ Doit commencer par une lettre minuscule
|
||||||
|
- ❌ Seulement : a-z, 0-9, ., -, _
|
||||||
|
- ❌ Pas d'espaces
|
||||||
|
- ❌ Pas de majuscules
|
||||||
|
- ❌ Pas d'accents ou caractères spéciaux
|
||||||
|
|
||||||
|
## Nouvelles règles (ultra-souples)
|
||||||
|
- ✅ **8-30 caractères UTF-8**
|
||||||
|
- ✅ **Tous caractères acceptés** :
|
||||||
|
- Lettres (majuscules/minuscules)
|
||||||
|
- Chiffres
|
||||||
|
- Espaces
|
||||||
|
- Caractères spéciaux (!@#$%^&*()_+-=[]{}|;:'"<>,.?/)
|
||||||
|
- Accents (é, è, à, ñ, ü, etc.)
|
||||||
|
- Émojis (😀, 🎉, ❤️, etc.)
|
||||||
|
- Caractères non-latins (中文, العربية, Русский, etc.)
|
||||||
|
- ✅ **Sensible à la casse** (Jean ≠ jean)
|
||||||
|
- ✅ **Trim automatique** des espaces début/fin
|
||||||
|
- ✅ **Unicité vérifiée** dans toute la base
|
||||||
|
|
||||||
|
## Exemples de usernames valides
|
||||||
|
|
||||||
|
### Noms classiques
|
||||||
|
- `Jean-Pierre`
|
||||||
|
- `Marie Claire` (avec espace)
|
||||||
|
- `O'Connor`
|
||||||
|
- `José García`
|
||||||
|
|
||||||
|
### Avec chiffres et caractères spéciaux
|
||||||
|
- `admin2024`
|
||||||
|
- `user@company`
|
||||||
|
- `test_user#1`
|
||||||
|
- `Marie*123!`
|
||||||
|
|
||||||
|
### International
|
||||||
|
- `李明` (chinois)
|
||||||
|
- `محمد` (arabe)
|
||||||
|
- `Владимир` (russe)
|
||||||
|
- `さくら` (japonais)
|
||||||
|
- `Παύλος` (grec)
|
||||||
|
|
||||||
|
### Modernes/Fun
|
||||||
|
- `🦄Unicorn`
|
||||||
|
- `Player_1 🎮`
|
||||||
|
- `☕Coffee.Lover`
|
||||||
|
- `2024_User`
|
||||||
|
|
||||||
|
## Exemples de usernames invalides
|
||||||
|
- `short` ❌ (moins de 8 caractères)
|
||||||
|
- ` ` ❌ (espaces seulement)
|
||||||
|
- `very_long_username_that_exceeds_thirty_chars` ❌ (plus de 30 caractères)
|
||||||
|
|
||||||
|
## Modifications techniques
|
||||||
|
|
||||||
|
### 1. Code PHP (UserController.php)
|
||||||
|
```php
|
||||||
|
// Avant (restrictif)
|
||||||
|
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username))
|
||||||
|
|
||||||
|
// Après (ultra-souple)
|
||||||
|
$username = trim($data['username']);
|
||||||
|
$usernameLength = mb_strlen($username, 'UTF-8');
|
||||||
|
|
||||||
|
if ($usernameLength < 8) {
|
||||||
|
// Erreur : trop court
|
||||||
|
}
|
||||||
|
if ($usernameLength > 30) {
|
||||||
|
// Erreur : trop long
|
||||||
|
}
|
||||||
|
// C'est tout ! Pas d'autre validation
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Base de données
|
||||||
|
```sql
|
||||||
|
-- Script à exécuter : scripts/sql/migration_username_utf8_support.sql
|
||||||
|
ALTER TABLE `users`
|
||||||
|
MODIFY COLUMN `encrypted_user_name` varchar(255) DEFAULT '';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Messages d'erreur simplifiés
|
||||||
|
- Avant : "Format du nom d'utilisateur invalide (10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _)"
|
||||||
|
- Après :
|
||||||
|
- "Identifiant trop court" + "Minimum 8 caractères"
|
||||||
|
- "Identifiant trop long" + "Maximum 30 caractères"
|
||||||
|
- "Identifiant déjà utilisé"
|
||||||
|
|
||||||
|
## Impact sur l'expérience utilisateur
|
||||||
|
|
||||||
|
### Avantages
|
||||||
|
1. **Inclusivité** : Support de toutes les langues et cultures
|
||||||
|
2. **Modernité** : Permet les émojis et caractères spéciaux
|
||||||
|
3. **Simplicité** : Règles faciles à comprendre (juste la longueur)
|
||||||
|
4. **Flexibilité** : Les utilisateurs peuvent choisir l'identifiant qu'ils veulent
|
||||||
|
5. **Moins d'erreurs** : Moins de rejets pour format invalide
|
||||||
|
|
||||||
|
### Points d'attention
|
||||||
|
1. **Support client** : Former le support aux nouveaux formats possibles
|
||||||
|
2. **Affichage** : S'assurer que l'UI supporte bien l'UTF-8
|
||||||
|
3. **Recherche** : La recherche d'utilisateurs doit gérer la casse et l'UTF-8
|
||||||
|
4. **Export** : Vérifier que les exports CSV/Excel gèrent bien l'UTF-8
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Pas d'impact sur la sécurité
|
||||||
|
- ✅ Les usernames sont toujours chiffrés en base (AES-256-CBC)
|
||||||
|
- ✅ L'unicité est toujours vérifiée
|
||||||
|
- ✅ Les injections SQL sont impossibles (prepared statements)
|
||||||
|
- ✅ Le trim empêche les espaces invisibles
|
||||||
|
|
||||||
|
### Recommandations
|
||||||
|
- Continuer à générer automatiquement des usernames simples (ASCII) pour éviter les problèmes
|
||||||
|
- Mais permettre la saisie manuelle de tout format
|
||||||
|
- Logger les usernames "exotiques" pour détecter d'éventuels abus
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- Script de test disponible : `/tests/test_username_validation.php`
|
||||||
|
- Teste tous les cas limites et formats internationaux
|
||||||
|
|
||||||
|
## Rollback si nécessaire
|
||||||
|
Si besoin de revenir en arrière :
|
||||||
|
1. Restaurer l'ancienne validation dans UserController
|
||||||
|
2. Les usernames UTF-8 existants continueront de fonctionner
|
||||||
|
3. Seuls les nouveaux seront restreints
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
Cette approche ultra-souple aligne les usernames sur les standards modernes d'inclusivité et d'accessibilité, tout en maintenant la sécurité grâce au chiffrement et à la validation de l'unicité.
|
||||||
BIN
api/docs/_logo_recu.png
Normal file
BIN
api/docs/_logo_recu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
BIN
api/docs/_recu_template.pdf
Normal file
BIN
api/docs/_recu_template.pdf
Normal file
Binary file not shown.
@@ -18,168 +18,39 @@
|
|||||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE `chat_anonymous_users` (
|
-- Tables préfixées "chat_"
|
||||||
`id` varchar(50) NOT NULL,
|
CREATE TABLE chat_rooms (
|
||||||
`device_id` varchar(100) NOT NULL,
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
`name` varchar(100) DEFAULT NULL,
|
title VARCHAR(255),
|
||||||
`email` varchar(100) DEFAULT NULL,
|
type ENUM('private', 'group', 'broadcast'),
|
||||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
created_at TIMESTAMP,
|
||||||
`converted_to_user_id` int(10) unsigned DEFAULT NULL,
|
created_by INT
|
||||||
`metadata` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),
|
);
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_device_id` (`device_id`),
|
|
||||||
KEY `idx_converted_user` (`converted_to_user_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_attachments` (
|
CREATE TABLE chat_messages (
|
||||||
`id` varchar(50) NOT NULL,
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
`fk_message` varchar(50) NOT NULL,
|
room_id VARCHAR(36),
|
||||||
`file_name` varchar(255) NOT NULL,
|
content TEXT,
|
||||||
`file_path` varchar(500) NOT NULL,
|
sender_id INT,
|
||||||
`file_type` varchar(100) NOT NULL,
|
sent_at TIMESTAMP,
|
||||||
`file_size` int(10) unsigned NOT NULL,
|
FOREIGN KEY (room_id) REFERENCES chat_rooms(id)
|
||||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
);
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_message` (`fk_message`),
|
|
||||||
CONSTRAINT `fk_chat_attachments_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_audience_targets` (
|
CREATE TABLE chat_participants (
|
||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
room_id VARCHAR(36),
|
||||||
`fk_room` varchar(50) NOT NULL,
|
user_id INT,
|
||||||
`target_type` enum('role','entity','all','combined') NOT NULL DEFAULT 'all',
|
role INT,
|
||||||
`target_id` varchar(50) DEFAULT NULL,
|
entite_id INT,
|
||||||
`role_filter` varchar(20) DEFAULT NULL,
|
joined_at TIMESTAMP,
|
||||||
`entity_filter` varchar(50) DEFAULT NULL,
|
PRIMARY KEY (room_id, user_id)
|
||||||
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
);
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_room` (`fk_room`),
|
|
||||||
KEY `idx_type` (`target_type`),
|
|
||||||
CONSTRAINT `fk_chat_audience_targets_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_broadcast_lists` (
|
CREATE TABLE chat_read_receipts (
|
||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
message_id VARCHAR(36),
|
||||||
`fk_room` varchar(50) NOT NULL,
|
user_id INT,
|
||||||
`name` varchar(100) NOT NULL,
|
read_at TIMESTAMP,
|
||||||
`description` text DEFAULT NULL,
|
PRIMARY KEY (message_id, user_id)
|
||||||
`fk_user_creator` int(10) unsigned NOT NULL,
|
);
|
||||||
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_room` (`fk_room`),
|
|
||||||
KEY `idx_user_creator` (`fk_user_creator`),
|
|
||||||
CONSTRAINT `fk_chat_broadcast_lists_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE `chat_messages` (
|
|
||||||
`id` varchar(50) NOT NULL,
|
|
||||||
`fk_room` varchar(50) NOT NULL,
|
|
||||||
`fk_user` int(10) unsigned DEFAULT NULL,
|
|
||||||
`sender_type` enum('user','anonymous','system') NOT NULL DEFAULT 'user',
|
|
||||||
`content` text DEFAULT NULL,
|
|
||||||
`content_type` enum('text','image','file') NOT NULL DEFAULT 'text',
|
|
||||||
`date_sent` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`date_delivered` timestamp NULL DEFAULT NULL,
|
|
||||||
`date_read` timestamp NULL DEFAULT NULL,
|
|
||||||
`statut` enum('envoye','livre','lu','error') NOT NULL DEFAULT 'envoye',
|
|
||||||
`is_announcement` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_room` (`fk_room`),
|
|
||||||
KEY `idx_user` (`fk_user`),
|
|
||||||
KEY `idx_date` (`date_sent`),
|
|
||||||
KEY `idx_status` (`statut`),
|
|
||||||
KEY `idx_messages_unread` (`fk_room`,`statut`),
|
|
||||||
CONSTRAINT `fk_chat_messages_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_notifications` (
|
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
`fk_user` int(10) unsigned NOT NULL,
|
|
||||||
`fk_message` varchar(50) DEFAULT NULL,
|
|
||||||
`fk_room` varchar(50) DEFAULT NULL,
|
|
||||||
`type` varchar(50) NOT NULL,
|
|
||||||
`contenu` text DEFAULT NULL,
|
|
||||||
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`date_lecture` timestamp NULL DEFAULT NULL,
|
|
||||||
`statut` enum('non_lue','lue') NOT NULL DEFAULT 'non_lue',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_user` (`fk_user`),
|
|
||||||
KEY `idx_message` (`fk_message`),
|
|
||||||
KEY `idx_room` (`fk_room`),
|
|
||||||
KEY `idx_statut` (`statut`),
|
|
||||||
KEY `idx_notifications_unread` (`fk_user`,`statut`),
|
|
||||||
CONSTRAINT `fk_chat_notifications_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE SET NULL,
|
|
||||||
CONSTRAINT `fk_chat_notifications_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE SET NULL
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_offline_queue` (
|
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
`user_id` int(10) unsigned NOT NULL,
|
|
||||||
`operation_type` varchar(50) NOT NULL,
|
|
||||||
`operation_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`operation_data`)),
|
|
||||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`processed_at` timestamp NULL DEFAULT NULL,
|
|
||||||
`status` enum('pending','processing','completed','failed') NOT NULL DEFAULT 'pending',
|
|
||||||
`error_message` text DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_user_id` (`user_id`),
|
|
||||||
KEY `idx_status` (`status`),
|
|
||||||
KEY `idx_created_at` (`created_at`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_participants` (
|
|
||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
`id_room` varchar(50) NOT NULL,
|
|
||||||
`id_user` int(10) unsigned DEFAULT NULL,
|
|
||||||
`anonymous_id` varchar(50) DEFAULT NULL,
|
|
||||||
`role` enum('administrateur','participant','en_lecture_seule') NOT NULL DEFAULT 'participant',
|
|
||||||
`date_ajout` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`notification_activee` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
|
||||||
`last_read_message_id` varchar(50) DEFAULT NULL,
|
|
||||||
`via_target` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
|
||||||
`can_reply` tinyint(1) unsigned DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `uc_room_user` (`id_room`,`id_user`),
|
|
||||||
KEY `idx_room` (`id_room`),
|
|
||||||
KEY `idx_user` (`id_user`),
|
|
||||||
KEY `idx_anonymous_id` (`anonymous_id`),
|
|
||||||
KEY `idx_participants_active` (`id_room`,`id_user`,`notification_activee`),
|
|
||||||
CONSTRAINT `fk_chat_participants_room` FOREIGN KEY (`id_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_read_messages` (
|
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
`fk_message` varchar(50) NOT NULL,
|
|
||||||
`fk_user` int(10) unsigned NOT NULL,
|
|
||||||
`date_read` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `uc_message_user` (`fk_message`,`fk_user`),
|
|
||||||
KEY `idx_message` (`fk_message`),
|
|
||||||
KEY `idx_user` (`fk_user`),
|
|
||||||
CONSTRAINT `fk_chat_read_messages_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_rooms` (
|
|
||||||
`id` varchar(50) NOT NULL,
|
|
||||||
`type` enum('privee','groupe','liste_diffusion','broadcast','announcement') NOT NULL,
|
|
||||||
`title` varchar(100) DEFAULT NULL,
|
|
||||||
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`fk_user` int(10) unsigned NOT NULL,
|
|
||||||
`fk_entite` int(10) unsigned DEFAULT NULL,
|
|
||||||
`statut` enum('active','archive') NOT NULL DEFAULT 'active',
|
|
||||||
`description` text DEFAULT NULL,
|
|
||||||
`reply_permission` enum('all','admins_only','sender_only','none') NOT NULL DEFAULT 'all',
|
|
||||||
`is_pinned` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
|
||||||
`expiry_date` timestamp NULL DEFAULT NULL,
|
|
||||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_user` (`fk_user`),
|
|
||||||
KEY `idx_entite` (`fk_entite`),
|
|
||||||
KEY `idx_type` (`type`),
|
|
||||||
KEY `idx_statut` (`statut`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `email_counter` (
|
CREATE TABLE `email_counter` (
|
||||||
`id` int(10) unsigned NOT NULL DEFAULT 1,
|
`id` int(10) unsigned NOT NULL DEFAULT 1,
|
||||||
|
|||||||
19
api/docs/logrotate_email_queue.conf
Normal file
19
api/docs/logrotate_email_queue.conf
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Configuration logrotate pour email_queue.log
|
||||||
|
# À placer dans /etc/logrotate.d/geosector-email-queue
|
||||||
|
|
||||||
|
/var/www/geosector/api/logs/email_queue.log {
|
||||||
|
daily # Rotation journalière
|
||||||
|
rotate 30 # Garder 30 jours d'historique
|
||||||
|
compress # Compresser les anciens logs
|
||||||
|
delaycompress # Compresser le jour suivant
|
||||||
|
missingok # Pas d'erreur si le fichier n'existe pas
|
||||||
|
notifempty # Ne pas tourner si vide
|
||||||
|
create 664 www-data www-data # Créer nouveau fichier avec permissions
|
||||||
|
dateext # Ajouter la date au nom du fichier
|
||||||
|
dateformat -%Y%m%d # Format de date YYYYMMDD
|
||||||
|
maxsize 100M # Rotation si dépasse 100MB même avant la fin du jour
|
||||||
|
postrotate
|
||||||
|
# Optionnel : envoyer un signal au process si nécessaire
|
||||||
|
# /usr/bin/killall -SIGUSR1 php 2>/dev/null || true
|
||||||
|
endscript
|
||||||
|
}
|
||||||
93
api/docs/recu_19500582.pdf
Normal file
93
api/docs/recu_19500582.pdf
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
%PDF-1.4
|
||||||
|
%âãÏÓ
|
||||||
|
1 0 obj
|
||||||
|
<< /Type /Catalog /Pages 2 0 R >>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<< /Length 599 >>
|
||||||
|
stream
|
||||||
|
BT
|
||||||
|
/F1 14 Tf
|
||||||
|
217 792 Td
|
||||||
|
(AMICALE TEST DEV PIERRE) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 11 Tf
|
||||||
|
281 770 Td
|
||||||
|
(RENNES) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 14 Tf
|
||||||
|
213.5 726 Td
|
||||||
|
(RECU FISCAL N 19500582) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 9 Tf
|
||||||
|
263.75 704 Td
|
||||||
|
(Article 200 CGI) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 12 Tf
|
||||||
|
50 657 Td
|
||||||
|
(Dugues) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 11 Tf
|
||||||
|
50 637 Td
|
||||||
|
(8 le Petit Monthelon Acigne) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 16 Tf
|
||||||
|
257.5 598 Td
|
||||||
|
(8,00 euros) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 12 Tf
|
||||||
|
267.5 559 Td
|
||||||
|
(20/08/2025) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 10 Tf
|
||||||
|
277.5 529 Td
|
||||||
|
(OPE 2025) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 9 Tf
|
||||||
|
198.5 476 Td
|
||||||
|
(Don ouvrant droit a reduction d'impot de 66%) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 10 Tf
|
||||||
|
50 419 Td
|
||||||
|
(Le 20/08/2025) Tj
|
||||||
|
ET
|
||||||
|
BT
|
||||||
|
/F1 10 Tf
|
||||||
|
50 401 Td
|
||||||
|
(Le President) Tj
|
||||||
|
ET
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 6
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000019 00000 n
|
||||||
|
0000000068 00000 n
|
||||||
|
0000000125 00000 n
|
||||||
|
0000000251 00000 n
|
||||||
|
0000000353 00000 n
|
||||||
|
trailer
|
||||||
|
<< /Size 6 /Root 1 0 R >>
|
||||||
|
startxref
|
||||||
|
1003
|
||||||
|
%%EOF
|
||||||
75
api/docs/recu_19500586.pdf
Normal file
75
api/docs/recu_19500586.pdf
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
%PDF-1.3
|
||||||
|
1 0 obj
|
||||||
|
<< /Type /Catalog /Pages 2 0 R >>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<< /Length 767 >>
|
||||||
|
stream
|
||||||
|
BT
|
||||||
|
/F1 12 Tf
|
||||||
|
50 750 Td
|
||||||
|
(AMICALE TEST DEV PIERRE) Tj
|
||||||
|
0 -20 Td
|
||||||
|
(17 place hoche 35000 RENNES) Tj
|
||||||
|
/F1 16 Tf
|
||||||
|
0 -40 Td
|
||||||
|
(RECU DE DON N° 19500586) Tj
|
||||||
|
/F1 10 Tf
|
||||||
|
0 -15 Td
|
||||||
|
(Article 200 du Code General des Impots) Tj
|
||||||
|
/F1 12 Tf
|
||||||
|
0 -45 Td
|
||||||
|
(DONATEUR) Tj
|
||||||
|
/F1 11 Tf
|
||||||
|
0 -20 Td
|
||||||
|
(Nom : M. Hermann) Tj
|
||||||
|
0 -15 Td
|
||||||
|
(Adresse : 12 le Petit Monthelon Acigne) Tj
|
||||||
|
0 -15 Td
|
||||||
|
(Email : pierre.vaissaire@gmail.com) Tj
|
||||||
|
0 -30 Td
|
||||||
|
/F1 12 Tf
|
||||||
|
(DETAILS DU DON) Tj
|
||||||
|
/F1 11 Tf
|
||||||
|
0 -20 Td
|
||||||
|
(Date : 19/08/2025) Tj
|
||||||
|
0 -15 Td
|
||||||
|
(Montant : 12,00 EUR) Tj
|
||||||
|
0 -15 Td
|
||||||
|
(Mode de reglement : Espece) Tj
|
||||||
|
0 -15 Td
|
||||||
|
(Campagne : OPE 2025) Tj
|
||||||
|
/F1 9 Tf
|
||||||
|
0 -40 Td
|
||||||
|
(Reduction d'impot egale a 66% du montant verse dans la limite de 20% du revenu imposable) Tj
|
||||||
|
/F1 11 Tf
|
||||||
|
0 -30 Td
|
||||||
|
(Fait a RENNES, le 19/08/2025) Tj
|
||||||
|
0 -20 Td
|
||||||
|
(Le President) Tj
|
||||||
|
ET
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 6
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000009 00000 n
|
||||||
|
0000000058 00000 n
|
||||||
|
0000000115 00000 n
|
||||||
|
0000000241 00000 n
|
||||||
|
0000000311 00000 n
|
||||||
|
trailer
|
||||||
|
<< /Size 6 /Root 1 0 R >>
|
||||||
|
startxref
|
||||||
|
1129
|
||||||
|
%%EOF
|
||||||
BIN
api/docs/recu_537254062.pdf
Normal file
BIN
api/docs/recu_537254062.pdf
Normal file
Binary file not shown.
BIN
api/docs/recu_972506460.pdf
Normal file
BIN
api/docs/recu_972506460.pdf
Normal file
Binary file not shown.
135
api/index.php
135
api/index.php
@@ -15,6 +15,12 @@ require_once __DIR__ . '/src/Core/Response.php';
|
|||||||
require_once __DIR__ . '/src/Utils/ClientDetector.php';
|
require_once __DIR__ . '/src/Utils/ClientDetector.php';
|
||||||
require_once __DIR__ . '/src/Services/LogService.php';
|
require_once __DIR__ . '/src/Services/LogService.php';
|
||||||
|
|
||||||
|
// Chargement des services de sécurité
|
||||||
|
require_once __DIR__ . '/src/Services/Security/PerformanceMonitor.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/IPBlocker.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/SecurityMonitor.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/AlertService.php';
|
||||||
|
|
||||||
// Chargement des contrôleurs
|
// Chargement des contrôleurs
|
||||||
require_once __DIR__ . '/src/Controllers/LogController.php';
|
require_once __DIR__ . '/src/Controllers/LogController.php';
|
||||||
require_once __DIR__ . '/src/Controllers/LoginController.php';
|
require_once __DIR__ . '/src/Controllers/LoginController.php';
|
||||||
@@ -25,6 +31,9 @@ require_once __DIR__ . '/src/Controllers/PassageController.php';
|
|||||||
require_once __DIR__ . '/src/Controllers/VilleController.php';
|
require_once __DIR__ . '/src/Controllers/VilleController.php';
|
||||||
require_once __DIR__ . '/src/Controllers/FileController.php';
|
require_once __DIR__ . '/src/Controllers/FileController.php';
|
||||||
require_once __DIR__ . '/src/Controllers/SectorController.php';
|
require_once __DIR__ . '/src/Controllers/SectorController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/PasswordController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/ChatController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/SecurityController.php';
|
||||||
|
|
||||||
// Initialiser la configuration
|
// Initialiser la configuration
|
||||||
$appConfig = AppConfig::getInstance();
|
$appConfig = AppConfig::getInstance();
|
||||||
@@ -56,8 +65,132 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|||||||
// Initialiser la session
|
// Initialiser la session
|
||||||
Session::start();
|
Session::start();
|
||||||
|
|
||||||
|
// ===== DÉBUT DU MONITORING DE SÉCURITÉ =====
|
||||||
|
use App\Services\Security\PerformanceMonitor;
|
||||||
|
use App\Services\Security\IPBlocker;
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
use App\Services\Security\AlertService;
|
||||||
|
|
||||||
|
// Obtenir l'IP du client
|
||||||
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
|
||||||
|
// Vérifier si l'IP est bloquée
|
||||||
|
if (IPBlocker::isBlocked($clientIp)) {
|
||||||
|
http_response_code(403);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Access denied. Your IP has been blocked.',
|
||||||
|
'error_code' => 'IP_BLOCKED'
|
||||||
|
], 403);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le rate limiting
|
||||||
|
if (!SecurityMonitor::checkRateLimit($clientIp)) {
|
||||||
|
http_response_code(429);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Too many requests. Please try again later.',
|
||||||
|
'error_code' => 'RATE_LIMIT_EXCEEDED'
|
||||||
|
], 429);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Démarrer le monitoring de performance
|
||||||
|
PerformanceMonitor::startRequest();
|
||||||
|
|
||||||
|
// Capturer le endpoint pour le monitoring
|
||||||
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||||
|
|
||||||
|
// Vérifier les patterns de scan
|
||||||
|
if (!SecurityMonitor::checkScanPattern($requestUri)) {
|
||||||
|
// Pattern suspect détecté, bloquer l'IP temporairement
|
||||||
|
IPBlocker::block($clientIp, 3600, 'Suspicious scan pattern detected');
|
||||||
|
http_response_code(404);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Not found'
|
||||||
|
], 404);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les paramètres pour injection SQL
|
||||||
|
$allParams = array_merge($_GET, $_POST, json_decode(file_get_contents('php://input'), true) ?? []);
|
||||||
|
if (!empty($allParams) && !SecurityMonitor::checkRequestParameters($allParams)) {
|
||||||
|
// Injection SQL détectée, bloquer l'IP définitivement
|
||||||
|
IPBlocker::blockPermanent($clientIp, 'SQL injection attempt');
|
||||||
|
http_response_code(400);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Bad request'
|
||||||
|
], 400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// Créer l'instance de routeur
|
// Créer l'instance de routeur
|
||||||
$router = new Router();
|
$router = new Router();
|
||||||
|
|
||||||
|
// Enregistrer une fonction de shutdown pour capturer les métriques
|
||||||
|
register_shutdown_function(function() use ($requestUri, $requestMethod) {
|
||||||
|
$statusCode = http_response_code();
|
||||||
|
|
||||||
|
// Terminer le monitoring de performance
|
||||||
|
PerformanceMonitor::endRequest($requestUri, $requestMethod, $statusCode);
|
||||||
|
|
||||||
|
// Vérifier les patterns 404
|
||||||
|
if ($statusCode === 404) {
|
||||||
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
SecurityMonitor::check404Pattern($clientIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alerter sur les erreurs 500
|
||||||
|
if ($statusCode >= 500) {
|
||||||
|
$error = error_get_last();
|
||||||
|
AlertService::trigger('HTTP_500', [
|
||||||
|
'endpoint' => $requestUri,
|
||||||
|
'method' => $requestMethod,
|
||||||
|
'error_message' => $error['message'] ?? 'Unknown error',
|
||||||
|
'error_file' => $error['file'] ?? 'Unknown',
|
||||||
|
'error_line' => $error['line'] ?? 0,
|
||||||
|
'message' => "Erreur serveur 500 sur $requestUri"
|
||||||
|
], 'ERROR');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer périodiquement les IPs expirées (1% de chance)
|
||||||
|
if (rand(1, 100) === 1) {
|
||||||
|
IPBlocker::cleanupExpired();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gérer les erreurs non capturées
|
||||||
|
set_exception_handler(function($exception) use ($requestUri, $requestMethod) {
|
||||||
|
// Logger l'erreur
|
||||||
|
error_log("Uncaught exception: " . $exception->getMessage());
|
||||||
|
|
||||||
|
// Créer une alerte
|
||||||
|
AlertService::trigger('UNCAUGHT_EXCEPTION', [
|
||||||
|
'endpoint' => $requestUri,
|
||||||
|
'method' => $requestMethod,
|
||||||
|
'exception' => get_class($exception),
|
||||||
|
'message' => $exception->getMessage(),
|
||||||
|
'file' => $exception->getFile(),
|
||||||
|
'line' => $exception->getLine(),
|
||||||
|
'trace' => substr($exception->getTraceAsString(), 0, 1000)
|
||||||
|
], 'ERROR');
|
||||||
|
|
||||||
|
// Retourner une erreur 500
|
||||||
|
http_response_code(500);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Internal server error'
|
||||||
|
], 500);
|
||||||
|
});
|
||||||
|
|
||||||
// Gérer la requête
|
// Gérer la requête
|
||||||
$router->handle();
|
try {
|
||||||
|
$router->handle();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Les exceptions sont gérées par le handler ci-dessus
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|||||||
@@ -142,6 +142,15 @@ fi
|
|||||||
|
|
||||||
echo "✅ Propriétaire et permissions appliqués avec succès"
|
echo "✅ Propriétaire et permissions appliqués avec succès"
|
||||||
|
|
||||||
|
# Mise à jour des dépendances Composer
|
||||||
|
echo "📦 Mise à jour des dépendances Composer sur $DEST_CONTAINER..."
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- bash -c 'cd $API_PATH && composer update --no-dev --optimize-autoloader'" > /dev/null 2>&1
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✅ Dépendances Composer mises à jour avec succès"
|
||||||
|
else
|
||||||
|
echo "⚠️ Composer non disponible ou échec, poursuite sans mise à jour des dépendances"
|
||||||
|
fi
|
||||||
|
|
||||||
# Vérifier la copie
|
# Vérifier la copie
|
||||||
echo "✅ Vérification de la copie..."
|
echo "✅ Vérification de la copie..."
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $API_PATH"
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $API_PATH"
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
322
api/scripts/cron/process_email_queue.php
Executable file
322
api/scripts/cron/process_email_queue.php
Executable file
@@ -0,0 +1,322 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script CRON pour traiter la queue d'emails
|
||||||
|
* Envoie les emails en attente dans la table email_queue
|
||||||
|
*
|
||||||
|
* À exécuter toutes les 5 minutes via crontab :
|
||||||
|
* Exemple: [asterisk]/5 [asterisk] [asterisk] [asterisk] [asterisk] /usr/bin/php /path/to/api/scripts/cron/process_email_queue.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
define('MAX_ATTEMPTS', 3);
|
||||||
|
define('BATCH_SIZE', 50);
|
||||||
|
define('LOCK_FILE', '/tmp/process_email_queue.lock');
|
||||||
|
|
||||||
|
// Empêcher l'exécution multiple simultanée
|
||||||
|
if (file_exists(LOCK_FILE)) {
|
||||||
|
$lockTime = filemtime(LOCK_FILE);
|
||||||
|
// Si le lock a plus de 30 minutes, on le supprime (processus probablement bloqué)
|
||||||
|
if (time() - $lockTime > 1800) {
|
||||||
|
unlink(LOCK_FILE);
|
||||||
|
} else {
|
||||||
|
die("Le processus est déjà en cours d'exécution\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le fichier de lock
|
||||||
|
file_put_contents(LOCK_FILE, getmypid());
|
||||||
|
|
||||||
|
// Enregistrer un handler pour supprimer le lock en cas d'arrêt
|
||||||
|
register_shutdown_function(function() {
|
||||||
|
if (file_exists(LOCK_FILE)) {
|
||||||
|
unlink(LOCK_FILE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simuler l'environnement web pour AppConfig en CLI
|
||||||
|
if (php_sapi_name() === 'cli') {
|
||||||
|
// Détecter l'environnement basé sur le hostname ou un paramètre
|
||||||
|
$hostname = gethostname();
|
||||||
|
if (strpos($hostname, 'prod') !== false) {
|
||||||
|
$_SERVER['SERVER_NAME'] = 'app.geosector.fr';
|
||||||
|
} elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rapp') !== false) {
|
||||||
|
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
|
||||||
|
} else {
|
||||||
|
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DVA par défaut
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||||
|
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||||
|
|
||||||
|
// Définir getallheaders si elle n'existe pas (CLI)
|
||||||
|
if (!function_exists('getallheaders')) {
|
||||||
|
function getallheaders() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chargement de l'environnement
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||||
|
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||||
|
require_once __DIR__ . '/../../src/Services/LogService.php';
|
||||||
|
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use PHPMailer\PHPMailer\SMTP;
|
||||||
|
use PHPMailer\PHPMailer\Exception;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialisation de la configuration
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
$dbConfig = $appConfig->getDatabaseConfig();
|
||||||
|
|
||||||
|
// Initialiser la base de données avec la configuration
|
||||||
|
Database::init($dbConfig);
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Log uniquement si mode debug activé
|
||||||
|
// LogService::log('Démarrage du processeur de queue d\'emails', [
|
||||||
|
// 'level' => 'info',
|
||||||
|
// 'script' => 'process_email_queue.php'
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// Récupérer les emails en attente
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT id, fk_pass, to_email, subject, body, headers, attempts
|
||||||
|
FROM email_queue
|
||||||
|
WHERE status = ? AND attempts < ?
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
LIMIT ?
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['pending', MAX_ATTEMPTS, BATCH_SIZE]);
|
||||||
|
$emails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($emails)) {
|
||||||
|
// Ne pas logger quand il n'y a rien à faire (toutes les 5 minutes)
|
||||||
|
// LogService::log('Aucun email en attente dans la queue', [
|
||||||
|
// 'level' => 'debug'
|
||||||
|
// ]);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogService::log('Emails à traiter', [
|
||||||
|
'level' => 'info',
|
||||||
|
'count' => count($emails)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Configuration SMTP
|
||||||
|
$smtpConfig = $appConfig->getSmtpConfig();
|
||||||
|
$emailConfig = $appConfig->getEmailConfig();
|
||||||
|
|
||||||
|
$successCount = 0;
|
||||||
|
$failureCount = 0;
|
||||||
|
|
||||||
|
// Traiter chaque email
|
||||||
|
foreach ($emails as $emailData) {
|
||||||
|
$emailId = $emailData['id'];
|
||||||
|
$passageId = $emailData['fk_pass'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Incrémenter le compteur de tentatives
|
||||||
|
$stmt = $db->prepare('UPDATE email_queue SET attempts = attempts + 1 WHERE id = ?');
|
||||||
|
$stmt->execute([$emailId]);
|
||||||
|
|
||||||
|
// Créer l'instance PHPMailer
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
|
||||||
|
// Configuration du serveur SMTP
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $smtpConfig['host'];
|
||||||
|
$mail->SMTPAuth = $smtpConfig['auth'] ?? true;
|
||||||
|
$mail->Username = $smtpConfig['user'];
|
||||||
|
$mail->Password = $smtpConfig['pass'];
|
||||||
|
$mail->SMTPSecure = $smtpConfig['secure'];
|
||||||
|
$mail->Port = $smtpConfig['port'];
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
|
||||||
|
// Configuration de l'expéditeur
|
||||||
|
$fromName = 'Amicale Sapeurs-Pompiers'; // Nom par défaut
|
||||||
|
$mail->setFrom($emailConfig['from'], $fromName);
|
||||||
|
|
||||||
|
// Destinataire
|
||||||
|
$mail->addAddress($emailData['to_email']);
|
||||||
|
|
||||||
|
// Sujet
|
||||||
|
$mail->Subject = $emailData['subject'];
|
||||||
|
|
||||||
|
// Headers personnalisés si présents
|
||||||
|
if (!empty($emailData['headers'])) {
|
||||||
|
// Les headers contiennent déjà les informations MIME pour la pièce jointe
|
||||||
|
// On doit extraire le boundary et reconstruire le message
|
||||||
|
if (preg_match('/boundary="([^"]+)"/', $emailData['headers'], $matches)) {
|
||||||
|
$boundary = $matches[1];
|
||||||
|
|
||||||
|
// Le body contient déjà le message complet avec pièce jointe
|
||||||
|
$mail->isHTML(false);
|
||||||
|
$mail->Body = $emailData['body'];
|
||||||
|
|
||||||
|
// Extraire le contenu HTML et la pièce jointe
|
||||||
|
$parts = explode("--$boundary", $emailData['body']);
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (strpos($part, 'Content-Type: text/html') !== false) {
|
||||||
|
// Extraire le contenu HTML
|
||||||
|
$htmlContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4));
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->Body = $htmlContent;
|
||||||
|
} elseif (strpos($part, 'Content-Type: application/pdf') !== false) {
|
||||||
|
// Extraire le PDF encodé en base64
|
||||||
|
if (preg_match('/filename="([^"]+)"/', $part, $fileMatches)) {
|
||||||
|
$filename = $fileMatches[1];
|
||||||
|
$pdfContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4));
|
||||||
|
// Supprimer les retours à la ligne du base64
|
||||||
|
$pdfContent = str_replace(["\r", "\n"], '', $pdfContent);
|
||||||
|
|
||||||
|
// Ajouter la pièce jointe
|
||||||
|
$mail->addStringAttachment(
|
||||||
|
base64_decode($pdfContent),
|
||||||
|
$filename,
|
||||||
|
'base64',
|
||||||
|
'application/pdf'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Email simple sans pièce jointe
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->Body = $emailData['body'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter une copie si configuré
|
||||||
|
if (!empty($emailConfig['contact'])) {
|
||||||
|
$mail->addBCC($emailConfig['contact']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envoyer l'email
|
||||||
|
if ($mail->send()) {
|
||||||
|
// Marquer comme envoyé
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
UPDATE email_queue
|
||||||
|
SET status = ?, sent_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute(['sent', $emailId]);
|
||||||
|
|
||||||
|
// Mettre à jour le passage si nécessaire
|
||||||
|
if ($passageId > 0) {
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
UPDATE ope_pass
|
||||||
|
SET date_sent_recu = NOW(), chk_email_sent = 1
|
||||||
|
WHERE id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$passageId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$successCount++;
|
||||||
|
|
||||||
|
LogService::log('Email envoyé avec succès', [
|
||||||
|
'level' => 'info',
|
||||||
|
'emailId' => $emailId,
|
||||||
|
'passageId' => $passageId,
|
||||||
|
'to' => $emailData['to_email']
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
throw new Exception('Échec de l\'envoi');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$failureCount++;
|
||||||
|
|
||||||
|
LogService::log('Erreur lors de l\'envoi de l\'email', [
|
||||||
|
'level' => 'error',
|
||||||
|
'emailId' => $emailId,
|
||||||
|
'passageId' => $passageId,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'attempts' => $emailData['attempts'] + 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Si on a atteint le nombre max de tentatives, marquer comme échoué
|
||||||
|
if ($emailData['attempts'] + 1 >= MAX_ATTEMPTS) {
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
UPDATE email_queue
|
||||||
|
SET status = ?, error_message = ?
|
||||||
|
WHERE id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute(['failed', $e->getMessage(), $emailId]);
|
||||||
|
|
||||||
|
LogService::log('Email marqué comme échoué après ' . MAX_ATTEMPTS . ' tentatives', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'emailId' => $emailId,
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause courte entre chaque email pour éviter la surcharge
|
||||||
|
usleep(500000); // 0.5 seconde
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger uniquement s'il y avait des emails à traiter
|
||||||
|
if (count($emails) > 0) {
|
||||||
|
LogService::log('Traitement de la queue terminé', [
|
||||||
|
'level' => 'info',
|
||||||
|
'success' => $successCount,
|
||||||
|
'failures' => $failureCount,
|
||||||
|
'total' => count($emails)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur fatale dans le processeur de queue', [
|
||||||
|
'level' => 'critical',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Supprimer le lock en cas d'erreur
|
||||||
|
if (file_exists(LOCK_FILE)) {
|
||||||
|
unlink(LOCK_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer les vieux emails traités (optionnel)
|
||||||
|
try {
|
||||||
|
// Supprimer les emails envoyés de plus de 30 jours
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
DELETE FROM email_queue
|
||||||
|
WHERE status = ? AND sent_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||||
|
');
|
||||||
|
$stmt->execute(['sent']);
|
||||||
|
|
||||||
|
$deleted = $stmt->rowCount();
|
||||||
|
if ($deleted > 0) {
|
||||||
|
LogService::log('Nettoyage des anciens emails', [
|
||||||
|
'level' => 'info',
|
||||||
|
'deleted' => $deleted
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors du nettoyage des anciens emails', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer le lock
|
||||||
|
if (file_exists(LOCK_FILE)) {
|
||||||
|
unlink(LOCK_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Traitement terminé : $successCount envoyés, $failureCount échecs\n";
|
||||||
|
exit(0);
|
||||||
31
api/scripts/cron/process_email_queue_with_daily_log.sh
Normal file
31
api/scripts/cron/process_email_queue_with_daily_log.sh
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script wrapper pour process_email_queue avec logs journaliers
|
||||||
|
# Crée automatiquement un nouveau fichier log chaque jour
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
LOG_DIR="/var/www/geosector/api/logs"
|
||||||
|
LOG_FILE="$LOG_DIR/email_queue_$(date +%Y%m%d).log"
|
||||||
|
PHP_SCRIPT="/var/www/geosector/api/scripts/cron/process_email_queue.php"
|
||||||
|
|
||||||
|
# Créer le répertoire de logs s'il n'existe pas
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
# Ajouter un timestamp au début de l'exécution
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Démarrage du processeur de queue d'emails" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Exécuter le script PHP
|
||||||
|
/usr/bin/php "$PHP_SCRIPT" >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
# Ajouter le statut de sortie
|
||||||
|
EXIT_CODE=$?
|
||||||
|
if [ $EXIT_CODE -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (succès)" >> "$LOG_FILE"
|
||||||
|
else
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (erreur: $EXIT_CODE)" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Nettoyer les logs de plus de 30 jours
|
||||||
|
find "$LOG_DIR" -name "email_queue_*.log" -type f -mtime +30 -delete 2>/dev/null
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
||||||
186
api/scripts/cron/test_email_queue.php
Executable file
186
api/scripts/cron/test_email_queue.php
Executable file
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script de test pour vérifier le processeur de queue d'emails
|
||||||
|
* Affiche les emails en attente sans les envoyer
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// Simuler l'environnement web pour AppConfig en CLI
|
||||||
|
if (php_sapi_name() === 'cli') {
|
||||||
|
$_SERVER['SERVER_NAME'] = $_SERVER['SERVER_NAME'] ?? 'dapp.geosector.fr'; // DVA par défaut
|
||||||
|
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||||
|
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||||
|
|
||||||
|
// Définir getallheaders si elle n'existe pas (CLI)
|
||||||
|
if (!function_exists('getallheaders')) {
|
||||||
|
function getallheaders() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||||
|
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialiser la configuration
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
$dbConfig = $appConfig->getDatabaseConfig();
|
||||||
|
|
||||||
|
// Initialiser la base de données avec la configuration
|
||||||
|
Database::init($dbConfig);
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
echo "=== TEST DE LA QUEUE D'EMAILS ===\n\n";
|
||||||
|
|
||||||
|
// Statistiques générales
|
||||||
|
$stmt = $db->query('
|
||||||
|
SELECT
|
||||||
|
status,
|
||||||
|
COUNT(*) as count,
|
||||||
|
MIN(created_at) as oldest,
|
||||||
|
MAX(created_at) as newest
|
||||||
|
FROM email_queue
|
||||||
|
GROUP BY status
|
||||||
|
');
|
||||||
|
|
||||||
|
$stats = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo "STATISTIQUES:\n";
|
||||||
|
echo "-------------\n";
|
||||||
|
foreach ($stats as $stat) {
|
||||||
|
echo sprintf(
|
||||||
|
"Status: %s - Nombre: %d (Plus ancien: %s, Plus récent: %s)\n",
|
||||||
|
$stat['status'],
|
||||||
|
$stat['count'],
|
||||||
|
$stat['oldest'] ?? 'N/A',
|
||||||
|
$stat['newest'] ?? 'N/A'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Emails en attente
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT
|
||||||
|
eq.id,
|
||||||
|
eq.fk_pass,
|
||||||
|
eq.to_email,
|
||||||
|
eq.subject,
|
||||||
|
eq.created_at,
|
||||||
|
eq.attempts,
|
||||||
|
eq.status,
|
||||||
|
p.fk_type,
|
||||||
|
p.montant,
|
||||||
|
p.nom_recu
|
||||||
|
FROM email_queue eq
|
||||||
|
LEFT JOIN ope_pass p ON eq.fk_pass = p.id
|
||||||
|
WHERE eq.status = ?
|
||||||
|
ORDER BY eq.created_at DESC
|
||||||
|
LIMIT 10
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['pending']);
|
||||||
|
$pendingEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($pendingEmails)) {
|
||||||
|
echo "Aucun email en attente.\n";
|
||||||
|
} else {
|
||||||
|
echo "EMAILS EN ATTENTE (10 plus récents):\n";
|
||||||
|
echo "------------------------------------\n";
|
||||||
|
foreach ($pendingEmails as $email) {
|
||||||
|
echo sprintf(
|
||||||
|
"ID: %d | Passage: %d | Destinataire: %s\n",
|
||||||
|
$email['id'],
|
||||||
|
$email['fk_pass'],
|
||||||
|
$email['to_email']
|
||||||
|
);
|
||||||
|
echo sprintf(
|
||||||
|
" Sujet: %s\n",
|
||||||
|
$email['subject']
|
||||||
|
);
|
||||||
|
echo sprintf(
|
||||||
|
" Créé le: %s | Tentatives: %d\n",
|
||||||
|
$email['created_at'],
|
||||||
|
$email['attempts']
|
||||||
|
);
|
||||||
|
if ($email['fk_pass'] > 0) {
|
||||||
|
echo sprintf(
|
||||||
|
" Passage - Type: %s | Montant: %.2f€ | Reçu: %s\n",
|
||||||
|
$email['fk_type'] == 1 ? 'DON' : 'Autre',
|
||||||
|
$email['montant'] ?? 0,
|
||||||
|
$email['nom_recu'] ?? 'Non généré'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
echo "---\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emails échoués
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
fk_pass,
|
||||||
|
to_email,
|
||||||
|
subject,
|
||||||
|
created_at,
|
||||||
|
attempts,
|
||||||
|
error_message
|
||||||
|
FROM email_queue
|
||||||
|
WHERE status = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 5
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['failed']);
|
||||||
|
$failedEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!empty($failedEmails)) {
|
||||||
|
echo "\nEMAILS ÉCHOUÉS (5 plus récents):\n";
|
||||||
|
echo "--------------------------------\n";
|
||||||
|
foreach ($failedEmails as $email) {
|
||||||
|
echo sprintf(
|
||||||
|
"ID: %d | Passage: %d | Destinataire: %s\n",
|
||||||
|
$email['id'],
|
||||||
|
$email['fk_pass'],
|
||||||
|
$email['to_email']
|
||||||
|
);
|
||||||
|
echo sprintf(
|
||||||
|
" Sujet: %s\n",
|
||||||
|
$email['subject']
|
||||||
|
);
|
||||||
|
echo sprintf(
|
||||||
|
" Tentatives: %d | Erreur: %s\n",
|
||||||
|
$email['attempts'],
|
||||||
|
$email['error_message'] ?? 'Non spécifiée'
|
||||||
|
);
|
||||||
|
echo "---\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier la configuration SMTP
|
||||||
|
echo "\nCONFIGURATION SMTP:\n";
|
||||||
|
echo "-------------------\n";
|
||||||
|
|
||||||
|
$smtpConfig = $appConfig->getSmtpConfig();
|
||||||
|
$emailConfig = $appConfig->getEmailConfig();
|
||||||
|
|
||||||
|
echo "Host: " . ($smtpConfig['host'] ?? 'Non configuré') . "\n";
|
||||||
|
echo "Port: " . ($smtpConfig['port'] ?? 'Non configuré') . "\n";
|
||||||
|
echo "Username: " . ($smtpConfig['user'] ?? 'Non configuré') . "\n";
|
||||||
|
echo "Password: " . (isset($smtpConfig['pass']) ? '***' : 'Non configuré') . "\n";
|
||||||
|
echo "Encryption: " . ($smtpConfig['secure'] ?? 'Non configuré') . "\n";
|
||||||
|
echo "From Email: " . ($emailConfig['from'] ?? 'Non configuré') . "\n";
|
||||||
|
echo "Contact Email: " . ($emailConfig['contact'] ?? 'Non configuré') . "\n";
|
||||||
|
|
||||||
|
echo "\n=== FIN DU TEST ===\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "ERREUR: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
298
api/scripts/migrate_uploads_structure.php
Normal file
298
api/scripts/migrate_uploads_structure.php
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script de migration de l'arborescence des uploads
|
||||||
|
* Réorganise les fichiers existants vers la nouvelle structure simplifiée
|
||||||
|
*
|
||||||
|
* Ancienne structure : uploads/entites/{id}/* et uploads/{id}/*
|
||||||
|
* Nouvelle structure : uploads/{id}/*
|
||||||
|
*
|
||||||
|
* Usage: php scripts/migrate_uploads_structure.php [--dry-run]
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// Chemin de base des uploads
|
||||||
|
const BASE_PATH = '/var/www/geosector/api/uploads';
|
||||||
|
const LOG_FILE = '/var/www/geosector/api/logs/migration_uploads_' . date('Ymd_His') . '.log';
|
||||||
|
|
||||||
|
// Mode dry-run (simulation sans modification)
|
||||||
|
$dryRun = in_array('--dry-run', $argv);
|
||||||
|
|
||||||
|
// Fonction pour logger
|
||||||
|
function logMessage(string $message, string $level = 'INFO'): void {
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$log = "[$timestamp] [$level] $message" . PHP_EOL;
|
||||||
|
echo $log;
|
||||||
|
if (!$GLOBALS['dryRun']) {
|
||||||
|
file_put_contents(LOG_FILE, $log, FILE_APPEND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour déplacer un fichier ou dossier
|
||||||
|
function moveItem(string $source, string $destination): bool {
|
||||||
|
global $dryRun;
|
||||||
|
|
||||||
|
if (!file_exists($source)) {
|
||||||
|
logMessage("Source n'existe pas: $source", 'WARNING');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le dossier de destination si nécessaire
|
||||||
|
$destDir = dirname($destination);
|
||||||
|
if (!is_dir($destDir)) {
|
||||||
|
logMessage("Création du dossier: $destDir");
|
||||||
|
if (!$dryRun) {
|
||||||
|
mkdir($destDir, 0775, true);
|
||||||
|
chown($destDir, 'nginx');
|
||||||
|
chgrp($destDir, 'nobody');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déplacer l'élément
|
||||||
|
logMessage("Déplacement: $source -> $destination");
|
||||||
|
if (!$dryRun) {
|
||||||
|
if (is_dir($source)) {
|
||||||
|
// Pour un dossier, utiliser rename
|
||||||
|
return rename($source, $destination);
|
||||||
|
} else {
|
||||||
|
// Pour un fichier
|
||||||
|
return rename($source, $destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour copier récursivement un dossier
|
||||||
|
function copyDirectory(string $source, string $dest): bool {
|
||||||
|
global $dryRun;
|
||||||
|
|
||||||
|
if (!is_dir($source)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
if (!is_dir($dest)) {
|
||||||
|
mkdir($dest, 0775, true);
|
||||||
|
chown($dest, 'nginx');
|
||||||
|
chgrp($dest, 'nobody');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir = opendir($source);
|
||||||
|
while (($file = readdir($dir)) !== false) {
|
||||||
|
if ($file === '.' || $file === '..') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$srcPath = "$source/$file";
|
||||||
|
$destPath = "$dest/$file";
|
||||||
|
|
||||||
|
if (is_dir($srcPath)) {
|
||||||
|
copyDirectory($srcPath, $destPath);
|
||||||
|
} else {
|
||||||
|
logMessage("Copie: $srcPath -> $destPath");
|
||||||
|
if (!$dryRun) {
|
||||||
|
copy($srcPath, $destPath);
|
||||||
|
chmod($destPath, 0664);
|
||||||
|
chown($destPath, 'nginx');
|
||||||
|
chgrp($destPath, 'nobody');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir($dir);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction principale de migration
|
||||||
|
function migrateUploads(): void {
|
||||||
|
global $dryRun;
|
||||||
|
|
||||||
|
logMessage("=== Début de la migration des uploads ===");
|
||||||
|
logMessage($dryRun ? "MODE DRY-RUN (simulation)" : "MODE RÉEL (modifications effectives)");
|
||||||
|
|
||||||
|
// 1. Migrer uploads/entites/* vers uploads/*
|
||||||
|
$entitesPath = BASE_PATH . '/entites';
|
||||||
|
if (is_dir($entitesPath)) {
|
||||||
|
logMessage("Traitement du dossier entites/");
|
||||||
|
|
||||||
|
$entites = scandir($entitesPath);
|
||||||
|
foreach ($entites as $entiteId) {
|
||||||
|
if ($entiteId === '.' || $entiteId === '..') continue;
|
||||||
|
|
||||||
|
$oldPath = "$entitesPath/$entiteId";
|
||||||
|
$newPath = BASE_PATH . "/$entiteId";
|
||||||
|
|
||||||
|
if (!is_dir($oldPath)) continue;
|
||||||
|
|
||||||
|
logMessage("Migration entité $entiteId");
|
||||||
|
|
||||||
|
// Si le dossier destination existe déjà, fusionner
|
||||||
|
if (is_dir($newPath)) {
|
||||||
|
logMessage("Le dossier $entiteId existe déjà à la racine, fusion nécessaire", 'INFO');
|
||||||
|
|
||||||
|
// Migrer les sous-dossiers
|
||||||
|
$subDirs = scandir($oldPath);
|
||||||
|
foreach ($subDirs as $subDir) {
|
||||||
|
if ($subDir === '.' || $subDir === '..') continue;
|
||||||
|
|
||||||
|
$oldSubPath = "$oldPath/$subDir";
|
||||||
|
$newSubPath = "$newPath/$subDir";
|
||||||
|
|
||||||
|
if ($subDir === 'operations') {
|
||||||
|
// Traiter spécialement le dossier operations
|
||||||
|
migrateOperations($oldSubPath, $newSubPath);
|
||||||
|
} else {
|
||||||
|
// Pour logo et recus, déplacer directement
|
||||||
|
if (!is_dir($newSubPath)) {
|
||||||
|
moveItem($oldSubPath, $newSubPath);
|
||||||
|
} else {
|
||||||
|
logMessage("Le dossier $newSubPath existe déjà, fusion du contenu");
|
||||||
|
copyDirectory($oldSubPath, $newSubPath);
|
||||||
|
if (!$dryRun) {
|
||||||
|
// Supprimer l'ancien après copie
|
||||||
|
exec("rm -rf " . escapeshellarg($oldSubPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Déplacer simplement le dossier entier
|
||||||
|
moveItem($oldPath, $newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer le dossier entites vide
|
||||||
|
if (!$dryRun) {
|
||||||
|
if (count(scandir($entitesPath)) === 2) { // Seulement . et ..
|
||||||
|
rmdir($entitesPath);
|
||||||
|
logMessage("Suppression du dossier entites/ vide");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Nettoyer la structure des dossiers operations
|
||||||
|
logMessage("Nettoyage de la structure des dossiers operations");
|
||||||
|
cleanupOperationsStructure();
|
||||||
|
|
||||||
|
logMessage("=== Migration terminée ===");
|
||||||
|
if (!$dryRun) {
|
||||||
|
logMessage("Logs sauvegardés dans: " . LOG_FILE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour migrer le dossier operations avec simplification
|
||||||
|
function migrateOperations(string $oldPath, string $newPath): void {
|
||||||
|
global $dryRun;
|
||||||
|
|
||||||
|
if (!is_dir($oldPath)) return;
|
||||||
|
|
||||||
|
logMessage("Migration du dossier operations: $oldPath");
|
||||||
|
|
||||||
|
if (!$dryRun && !is_dir($newPath)) {
|
||||||
|
mkdir($newPath, 0775, true);
|
||||||
|
chown($newPath, 'nginx');
|
||||||
|
chgrp($newPath, 'nobody');
|
||||||
|
}
|
||||||
|
|
||||||
|
$operations = scandir($oldPath);
|
||||||
|
foreach ($operations as $opId) {
|
||||||
|
if ($opId === '.' || $opId === '..') continue;
|
||||||
|
|
||||||
|
$oldOpPath = "$oldPath/$opId";
|
||||||
|
$newOpPath = "$newPath/$opId";
|
||||||
|
|
||||||
|
// Simplifier la structure: déplacer les xlsx directement dans operations/{id}/
|
||||||
|
if (is_dir("$oldOpPath/documents/exports/excel")) {
|
||||||
|
$excelPath = "$oldOpPath/documents/exports/excel";
|
||||||
|
$files = scandir($excelPath);
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file === '.' || $file === '..' || !str_ends_with($file, '.xlsx')) continue;
|
||||||
|
|
||||||
|
$oldFilePath = "$excelPath/$file";
|
||||||
|
$newFilePath = "$newOpPath/$file";
|
||||||
|
|
||||||
|
logMessage("Déplacement Excel: $oldFilePath -> $newFilePath");
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
if (!is_dir($newOpPath)) {
|
||||||
|
mkdir($newOpPath, 0775, true);
|
||||||
|
chown($newOpPath, 'nginx');
|
||||||
|
chgrp($newOpPath, 'nobody');
|
||||||
|
}
|
||||||
|
rename($oldFilePath, $newFilePath);
|
||||||
|
chmod($newFilePath, 0664);
|
||||||
|
chown($newFilePath, 'nginx');
|
||||||
|
chgrp($newFilePath, 'nobody');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour nettoyer la structure après migration
|
||||||
|
function cleanupOperationsStructure(): void {
|
||||||
|
global $dryRun;
|
||||||
|
|
||||||
|
$uploadsDir = BASE_PATH;
|
||||||
|
$entites = scandir($uploadsDir);
|
||||||
|
|
||||||
|
foreach ($entites as $entiteId) {
|
||||||
|
if ($entiteId === '.' || $entiteId === '..' || $entiteId === 'entites') continue;
|
||||||
|
|
||||||
|
$operationsPath = "$uploadsDir/$entiteId/operations";
|
||||||
|
if (!is_dir($operationsPath)) continue;
|
||||||
|
|
||||||
|
$operations = scandir($operationsPath);
|
||||||
|
foreach ($operations as $opId) {
|
||||||
|
if ($opId === '.' || $opId === '..') continue;
|
||||||
|
|
||||||
|
$opPath = "$operationsPath/$opId";
|
||||||
|
|
||||||
|
// Supprimer l'ancienne structure documents/exports/excel si elle est vide
|
||||||
|
$oldStructure = "$opPath/documents";
|
||||||
|
if (is_dir($oldStructure)) {
|
||||||
|
logMessage("Suppression de l'ancienne structure: $oldStructure");
|
||||||
|
if (!$dryRun) {
|
||||||
|
exec("rm -rf " . escapeshellarg($oldStructure));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les permissions
|
||||||
|
if (!is_dir(BASE_PATH)) {
|
||||||
|
die("ERREUR: Le dossier " . BASE_PATH . " n'existe pas\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_writable(BASE_PATH) && !$dryRun) {
|
||||||
|
die("ERREUR: Le dossier " . BASE_PATH . " n'est pas accessible en écriture\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lancer la migration
|
||||||
|
try {
|
||||||
|
migrateUploads();
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
echo "\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
echo "SIMULATION TERMINÉE\n";
|
||||||
|
echo "Pour exécuter réellement la migration:\n";
|
||||||
|
echo "php " . $argv[0] . "\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
} else {
|
||||||
|
echo "\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
echo "MIGRATION TERMINÉE AVEC SUCCÈS\n";
|
||||||
|
echo "Vérifiez les logs: " . LOG_FILE . "\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logMessage("ERREUR FATALE: " . $e->getMessage(), 'ERROR');
|
||||||
|
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);
|
||||||
|
}
|
||||||
22
api/scripts/sql/add_chk_user_delete_pass.sql
Normal file
22
api/scripts/sql/add_chk_user_delete_pass.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Script de migration pour ajouter le champ chk_user_delete_pass
|
||||||
|
-- Ce champ permet aux administrateurs d'autoriser ou non leurs membres à supprimer des passages
|
||||||
|
-- Date : 2025-08-20
|
||||||
|
-- À exécuter sur DVA, REC et PROD
|
||||||
|
|
||||||
|
-- Ajouter le champ chk_user_delete_pass s'il n'existe pas
|
||||||
|
ALTER TABLE `entites`
|
||||||
|
ADD COLUMN IF NOT EXISTS `chk_user_delete_pass` tinyint(1) unsigned NOT NULL DEFAULT 0
|
||||||
|
COMMENT 'Autoriser les membres à supprimer des passages (1) ou non (0)'
|
||||||
|
AFTER `chk_username_manuel`;
|
||||||
|
|
||||||
|
-- Vérifier l'ajout
|
||||||
|
SELECT
|
||||||
|
COLUMN_NAME,
|
||||||
|
DATA_TYPE,
|
||||||
|
COLUMN_DEFAULT,
|
||||||
|
IS_NULLABLE,
|
||||||
|
COLUMN_COMMENT
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'entites'
|
||||||
|
AND COLUMN_NAME = 'chk_user_delete_pass';
|
||||||
22
api/scripts/sql/add_email_queue_fields.sql
Normal file
22
api/scripts/sql/add_email_queue_fields.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Migration pour ajouter les champs manquants à la table email_queue
|
||||||
|
-- À exécuter sur DVA, REC et PROD
|
||||||
|
|
||||||
|
-- Ajouter le champ sent_at s'il n'existe pas
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD COLUMN IF NOT EXISTS `sent_at` TIMESTAMP NULL DEFAULT NULL
|
||||||
|
COMMENT 'Date/heure d\'envoi effectif de l\'email'
|
||||||
|
AFTER `status`;
|
||||||
|
|
||||||
|
-- Ajouter le champ error_message s'il n'existe pas
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD COLUMN IF NOT EXISTS `error_message` TEXT NULL DEFAULT NULL
|
||||||
|
COMMENT 'Message d\'erreur en cas d\'échec'
|
||||||
|
AFTER `attempts`;
|
||||||
|
|
||||||
|
-- Ajouter un index sur le status pour optimiser les requêtes
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_status_attempts` (`status`, `attempts`);
|
||||||
|
|
||||||
|
-- Ajouter un index sur sent_at pour le nettoyage automatique
|
||||||
|
ALTER TABLE `email_queue`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_sent_at` (`sent_at`);
|
||||||
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
|
||||||
1559
api/src/Controllers/ChatController.php
Normal file
1559
api/src/Controllers/ChatController.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,8 +57,9 @@ class EntiteController {
|
|||||||
ville,
|
ville,
|
||||||
fk_type,
|
fk_type,
|
||||||
created_at,
|
created_at,
|
||||||
chk_active
|
chk_active,
|
||||||
) VALUES (?, ?, ?, 1, NOW(), 1)
|
chk_user_delete_pass
|
||||||
|
) VALUES (?, ?, ?, 1, NOW(), 1, 0)
|
||||||
');
|
');
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
@@ -109,7 +110,7 @@ class EntiteController {
|
|||||||
public function getEntiteById(int $id): array|false {
|
public function getEntiteById(int $id): array|false {
|
||||||
try {
|
try {
|
||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
SELECT id, encrypted_name, code_postal, ville, fk_type, chk_active
|
SELECT id, encrypted_name, code_postal, ville, fk_type, chk_active, chk_user_delete_pass
|
||||||
FROM entites
|
FROM entites
|
||||||
WHERE id = ? AND chk_active = 1
|
WHERE id = ? AND chk_active = 1
|
||||||
');
|
');
|
||||||
@@ -146,7 +147,7 @@ class EntiteController {
|
|||||||
public function getEntiteByPostalCode(string $postalCode): array|false {
|
public function getEntiteByPostalCode(string $postalCode): array|false {
|
||||||
try {
|
try {
|
||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
SELECT id, encrypted_name, code_postal, ville, fk_type, chk_active
|
SELECT id, encrypted_name, code_postal, ville, fk_type, chk_active, chk_user_delete_pass
|
||||||
FROM entites
|
FROM entites
|
||||||
WHERE code_postal = ? AND chk_active = 1
|
WHERE code_postal = ? AND chk_active = 1
|
||||||
');
|
');
|
||||||
@@ -247,7 +248,7 @@ class EntiteController {
|
|||||||
public function getEntites(): void {
|
public function getEntites(): void {
|
||||||
try {
|
try {
|
||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
SELECT id, encrypted_name, code_postal, ville, fk_type, chk_active
|
SELECT id, encrypted_name, code_postal, ville, fk_type, chk_active, chk_user_delete_pass
|
||||||
FROM entites
|
FROM entites
|
||||||
WHERE chk_active = 1
|
WHERE chk_active = 1
|
||||||
ORDER BY code_postal ASC
|
ORDER BY code_postal ASC
|
||||||
@@ -587,6 +588,11 @@ class EntiteController {
|
|||||||
$updateFields[] = 'chk_username_manuel = ?';
|
$updateFields[] = 'chk_username_manuel = ?';
|
||||||
$params[] = $data['chk_username_manuel'] ? 1 : 0;
|
$params[] = $data['chk_username_manuel'] ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($data['chk_user_delete_pass'])) {
|
||||||
|
$updateFields[] = 'chk_user_delete_pass = ?';
|
||||||
|
$params[] = $data['chk_user_delete_pass'] ? 1 : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si aucun champ à mettre à jour, retourner une erreur
|
// Si aucun champ à mettre à jour, retourner une erreur
|
||||||
@@ -728,7 +734,7 @@ class EntiteController {
|
|||||||
// Créer le dossier de destination
|
// Créer le dossier de destination
|
||||||
require_once __DIR__ . '/../Services/FileService.php';
|
require_once __DIR__ . '/../Services/FileService.php';
|
||||||
$fileService = new \FileService();
|
$fileService = new \FileService();
|
||||||
$uploadPath = "/entites/{$entiteId}/logo";
|
$uploadPath = "/{$entiteId}/logo";
|
||||||
$fullPath = $fileService->createDirectory($entiteId, $uploadPath);
|
$fullPath = $fileService->createDirectory($entiteId, $uploadPath);
|
||||||
|
|
||||||
// Nom du fichier final
|
// Nom du fichier final
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ use ApiService;
|
|||||||
require_once __DIR__ . '/../Services/LogService.php';
|
require_once __DIR__ . '/../Services/LogService.php';
|
||||||
require_once __DIR__ . '/../Services/ApiService.php';
|
require_once __DIR__ . '/../Services/ApiService.php';
|
||||||
require_once __DIR__ . '/EntiteController.php';
|
require_once __DIR__ . '/EntiteController.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/SecurityMonitor.php';
|
||||||
|
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
|
||||||
class LoginController {
|
class LoginController {
|
||||||
private PDO $db;
|
private PDO $db;
|
||||||
@@ -76,6 +79,11 @@ class LoginController {
|
|||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
// Enregistrer la tentative échouée
|
||||||
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
SecurityMonitor::recordFailedLogin($clientIp, $username, 'user_not_found', $userAgent);
|
||||||
|
|
||||||
LogService::log('Tentative de connexion GeoSector échouée : utilisateur non trouvé', [
|
LogService::log('Tentative de connexion GeoSector échouée : utilisateur non trouvé', [
|
||||||
'level' => 'warning',
|
'level' => 'warning',
|
||||||
'username' => $username
|
'username' => $username
|
||||||
@@ -88,6 +96,11 @@ class LoginController {
|
|||||||
$passwordValid = password_verify($data['password'], $user['user_pass_hash']);
|
$passwordValid = password_verify($data['password'], $user['user_pass_hash']);
|
||||||
|
|
||||||
if (!$passwordValid) {
|
if (!$passwordValid) {
|
||||||
|
// Enregistrer la tentative échouée
|
||||||
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
SecurityMonitor::recordFailedLogin($clientIp, $username, 'invalid_password', $userAgent);
|
||||||
|
|
||||||
LogService::log('Tentative de connexion GeoSector échouée : mot de passe incorrect', [
|
LogService::log('Tentative de connexion GeoSector échouée : mot de passe incorrect', [
|
||||||
'level' => 'warning',
|
'level' => 'warning',
|
||||||
'username' => $username
|
'username' => $username
|
||||||
@@ -520,7 +533,7 @@ class LoginController {
|
|||||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
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_email as email, e.gps_lat, e.gps_lng,
|
||||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
|
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||||
FROM entites e
|
FROM entites e
|
||||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||||
WHERE e.id = ? AND e.chk_active = 1'
|
WHERE e.id = ? AND e.chk_active = 1'
|
||||||
@@ -534,7 +547,7 @@ class LoginController {
|
|||||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
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_email as email, e.gps_lat, e.gps_lng,
|
||||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
|
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||||
FROM entites e
|
FROM entites e
|
||||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||||
WHERE e.id != 1 AND e.chk_active = 1'
|
WHERE e.id != 1 AND e.chk_active = 1'
|
||||||
@@ -587,7 +600,7 @@ class LoginController {
|
|||||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
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_email as email, e.gps_lat, e.gps_lng,
|
||||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
|
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||||
FROM entites e
|
FROM entites e
|
||||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||||
WHERE e.fk_type = 1 AND e.chk_active = 1'
|
WHERE e.fk_type = 1 AND e.chk_active = 1'
|
||||||
@@ -769,6 +782,88 @@ class LoginController {
|
|||||||
$response['regions'] = $regionsData;
|
$response['regions'] = $regionsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ajout des informations du module chat
|
||||||
|
$chatData = [];
|
||||||
|
|
||||||
|
// Récupérer le nombre total de conversations de l'utilisateur
|
||||||
|
$roomCountStmt = $this->db->prepare('
|
||||||
|
SELECT COUNT(DISTINCT r.id) as total_rooms
|
||||||
|
FROM chat_rooms r
|
||||||
|
INNER JOIN chat_participants p ON r.id = p.room_id
|
||||||
|
WHERE p.user_id = :user_id
|
||||||
|
AND p.left_at IS NULL
|
||||||
|
AND r.is_active = 1
|
||||||
|
');
|
||||||
|
$roomCountStmt->execute(['user_id' => $user['id']]);
|
||||||
|
$roomCount = $roomCountStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$chatData['total_rooms'] = (int)($roomCount['total_rooms'] ?? 0);
|
||||||
|
|
||||||
|
// Récupérer le nombre de messages non lus
|
||||||
|
$unreadStmt = $this->db->prepare('
|
||||||
|
SELECT COUNT(*) as unread_count
|
||||||
|
FROM chat_messages m
|
||||||
|
INNER JOIN chat_participants p ON m.room_id = p.room_id
|
||||||
|
WHERE p.user_id = :user_id
|
||||||
|
AND p.left_at IS NULL
|
||||||
|
AND m.sender_id != :sender_id
|
||||||
|
AND m.sent_at > COALESCE(p.last_read_at, p.joined_at)
|
||||||
|
AND m.is_deleted = 0
|
||||||
|
');
|
||||||
|
$unreadStmt->execute([
|
||||||
|
'user_id' => $user['id'],
|
||||||
|
'sender_id' => $user['id']
|
||||||
|
]);
|
||||||
|
$unreadResult = $unreadStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$chatData['unread_messages'] = (int)($unreadResult['unread_count'] ?? 0);
|
||||||
|
|
||||||
|
// Récupérer la dernière conversation active (optionnel, pour affichage rapide)
|
||||||
|
$lastRoomStmt = $this->db->prepare('
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.title,
|
||||||
|
r.type,
|
||||||
|
(SELECT m.content
|
||||||
|
FROM chat_messages m
|
||||||
|
WHERE m.room_id = r.id
|
||||||
|
AND m.is_deleted = 0
|
||||||
|
ORDER BY m.sent_at DESC
|
||||||
|
LIMIT 1) as last_message,
|
||||||
|
(SELECT m.sent_at
|
||||||
|
FROM chat_messages m
|
||||||
|
WHERE m.room_id = r.id
|
||||||
|
AND m.is_deleted = 0
|
||||||
|
ORDER BY m.sent_at DESC
|
||||||
|
LIMIT 1) as last_message_at
|
||||||
|
FROM chat_rooms r
|
||||||
|
INNER JOIN chat_participants p ON r.id = p.room_id
|
||||||
|
WHERE p.user_id = :user_id
|
||||||
|
AND p.left_at IS NULL
|
||||||
|
AND r.is_active = 1
|
||||||
|
ORDER BY COALESCE(
|
||||||
|
(SELECT MAX(m.sent_at) FROM chat_messages m WHERE m.room_id = r.id),
|
||||||
|
r.created_at
|
||||||
|
) DESC
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
$lastRoomStmt->execute(['user_id' => $user['id']]);
|
||||||
|
$lastRoom = $lastRoomStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($lastRoom) {
|
||||||
|
$chatData['last_active_room'] = [
|
||||||
|
'id' => $lastRoom['id'],
|
||||||
|
'title' => $lastRoom['title'],
|
||||||
|
'type' => $lastRoom['type'],
|
||||||
|
'last_message' => $lastRoom['last_message'],
|
||||||
|
'last_message_at' => $lastRoom['last_message_at']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicateur si le chat est disponible pour cet utilisateur
|
||||||
|
$chatData['chat_enabled'] = true; // Peut être conditionné selon le rôle ou l'entité
|
||||||
|
|
||||||
|
// Ajouter les données du chat à la réponse
|
||||||
|
$response['chat'] = $chatData;
|
||||||
|
|
||||||
// Envoi de la réponse
|
// Envoi de la réponse
|
||||||
Response::json($response);
|
Response::json($response);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
@@ -819,16 +914,16 @@ class LoginController {
|
|||||||
// Chiffrement de l'email pour la recherche
|
// Chiffrement de l'email pour la recherche
|
||||||
$encryptedEmail = ApiService::encryptSearchableData($email);
|
$encryptedEmail = ApiService::encryptSearchableData($email);
|
||||||
|
|
||||||
// Recherche de l'utilisateur
|
// Recherche de TOUS les utilisateurs avec cet email (actifs ou non)
|
||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
SELECT id, encrypted_name, encrypted_user_name, chk_active
|
SELECT id, encrypted_name, encrypted_user_name, chk_active
|
||||||
FROM users
|
FROM users
|
||||||
WHERE encrypted_email = ?
|
WHERE encrypted_email = ?
|
||||||
');
|
');
|
||||||
$stmt->execute([$encryptedEmail]);
|
$stmt->execute([$encryptedEmail]);
|
||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!$user) {
|
if (empty($users)) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Aucun compte trouvé avec cet email'
|
'message' => 'Aucun compte trouvé avec cet email'
|
||||||
@@ -836,54 +931,74 @@ class LoginController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($user['chk_active'] == 0) {
|
// Génération d'un nouveau mot de passe unique pour tous les comptes
|
||||||
Response::json([
|
|
||||||
'status' => 'error',
|
|
||||||
'message' => 'Ce compte est désactivé. Contactez l\'administrateur.'
|
|
||||||
], 403);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Déchiffrement du nom et du username
|
|
||||||
$name = ApiService::decryptData($user['encrypted_name']);
|
|
||||||
$username = ApiService::decryptSearchableData($user['encrypted_user_name']);
|
|
||||||
|
|
||||||
// Génération d'un nouveau mot de passe
|
|
||||||
$newPassword = ApiService::generateSecurePassword();
|
$newPassword = ApiService::generateSecurePassword();
|
||||||
$passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
|
$passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
// Mise à jour du mot de passe
|
// Mise à jour du mot de passe pour TOUS les comptes avec cet email
|
||||||
$updateStmt = $this->db->prepare('
|
$updateStmt = $this->db->prepare('
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET user_pass_hash = ?, updated_at = NOW()
|
SET user_pass_hash = ?, updated_at = NOW()
|
||||||
WHERE id = ?
|
WHERE encrypted_email = ?
|
||||||
');
|
');
|
||||||
$updateStmt->execute([$passwordHash, $user['id']]);
|
$updateStmt->execute([$passwordHash, $encryptedEmail]);
|
||||||
|
|
||||||
|
// Récupération du nombre de comptes mis à jour
|
||||||
|
$updatedCount = $updateStmt->rowCount();
|
||||||
|
|
||||||
|
// Collecte des usernames et du premier nom pour l'email
|
||||||
|
$usernames = [];
|
||||||
|
$firstName = '';
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$username = ApiService::decryptSearchableData($user['encrypted_user_name']);
|
||||||
|
if ($username) {
|
||||||
|
$usernames[] = $username;
|
||||||
|
}
|
||||||
|
// Utiliser le premier nom trouvé pour personnaliser l'email
|
||||||
|
if (empty($firstName) && !empty($user['encrypted_name'])) {
|
||||||
|
$firstName = ApiService::decryptData($user['encrypted_name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si aucun nom n'a été trouvé, utiliser "Utilisateur"
|
||||||
|
if (empty($firstName)) {
|
||||||
|
$firstName = 'Utilisateur';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envoi d'un seul email avec le nouveau mot de passe et la liste des comptes affectés
|
||||||
|
$emailData = [
|
||||||
|
'username' => implode(', ', $usernames), // Liste tous les usernames concernés
|
||||||
|
'password' => $newPassword
|
||||||
|
];
|
||||||
|
|
||||||
// Envoi de l'email avec le nouveau mot de passe
|
|
||||||
$emailSent = ApiService::sendEmail(
|
$emailSent = ApiService::sendEmail(
|
||||||
$email,
|
$email,
|
||||||
$name,
|
$firstName,
|
||||||
'lostpwd',
|
'lostpwd',
|
||||||
['username' => $username, 'password' => $newPassword]
|
$emailData
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($emailSent) {
|
if ($emailSent) {
|
||||||
LogService::log('Réinitialisation mot de passe GeoSector réussie', [
|
LogService::log('Réinitialisation mot de passe GeoSector réussie', [
|
||||||
'level' => 'info',
|
'level' => 'info',
|
||||||
'userId' => $user['id'],
|
'email' => $email,
|
||||||
'email' => $email
|
'comptes_modifies' => $updatedCount,
|
||||||
|
'usernames' => $usernames
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$message = $updatedCount > 1
|
||||||
|
? sprintf('Un nouveau mot de passe a été envoyé pour les %d comptes associés à votre adresse email', $updatedCount)
|
||||||
|
: 'Un nouveau mot de passe a été envoyé à votre adresse email';
|
||||||
|
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Un nouveau mot de passe a été envoyé à votre adresse email'
|
'message' => $message
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
LogService::log('Échec envoi email réinitialisation mot de passe GeoSector', [
|
LogService::log('Échec envoi email réinitialisation mot de passe GeoSector', [
|
||||||
'level' => 'error',
|
'level' => 'error',
|
||||||
'userId' => $user['id'],
|
'email' => $email,
|
||||||
'email' => $email
|
'comptes_modifies' => $updatedCount
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Response::json([
|
Response::json([
|
||||||
@@ -999,7 +1114,9 @@ class LoginController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Vérification de l'existence de l'email
|
// 4. Vérification de l'existence de l'email
|
||||||
|
// DÉSACTIVÉ : Le client souhaite permettre plusieurs comptes avec le même email
|
||||||
$encryptedEmail = ApiService::encryptSearchableData($email);
|
$encryptedEmail = ApiService::encryptSearchableData($email);
|
||||||
|
/*
|
||||||
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_email = ?');
|
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_email = ?');
|
||||||
$checkStmt->execute([$encryptedEmail]);
|
$checkStmt->execute([$encryptedEmail]);
|
||||||
if ($checkStmt->fetch()) {
|
if ($checkStmt->fetch()) {
|
||||||
@@ -1009,6 +1126,7 @@ class LoginController {
|
|||||||
], 409);
|
], 409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// 5. Vérification de l'existence du code postal dans la table entites
|
// 5. Vérification de l'existence du code postal dans la table entites
|
||||||
$checkPostalStmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ?');
|
$checkPostalStmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ?');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace App\Controllers;
|
|||||||
|
|
||||||
require_once __DIR__ . '/../Services/LogService.php';
|
require_once __DIR__ . '/../Services/LogService.php';
|
||||||
require_once __DIR__ . '/../Services/ApiService.php';
|
require_once __DIR__ . '/../Services/ApiService.php';
|
||||||
|
require_once __DIR__ . '/../Services/ReceiptService.php';
|
||||||
|
|
||||||
use PDO;
|
use PDO;
|
||||||
use PDOException;
|
use PDOException;
|
||||||
@@ -551,11 +552,58 @@ class PassageController {
|
|||||||
'operationId' => $operationId
|
'operationId' => $operationId
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Envoyer la réponse immédiatement pour éviter les timeouts
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Passage créé avec succès',
|
'message' => 'Passage créé avec succès',
|
||||||
'passage_id' => $passageId
|
'passage_id' => $passageId,
|
||||||
|
'receipt_generated' => false // On va générer le reçu en arrière-plan
|
||||||
], 201);
|
], 201);
|
||||||
|
|
||||||
|
// Flush la sortie pour s'assurer que la réponse est envoyée
|
||||||
|
if (ob_get_level()) {
|
||||||
|
ob_end_flush();
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
|
||||||
|
// Fermer la connexion HTTP mais continuer le traitement
|
||||||
|
if (function_exists('fastcgi_finish_request')) {
|
||||||
|
fastcgi_finish_request();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer automatiquement un reçu si c'est un don (fk_type = 1) avec email valide
|
||||||
|
if (isset($data['fk_type']) && (int)$data['fk_type'] === 1) {
|
||||||
|
// Vérifier si un email a été fourni
|
||||||
|
$hasEmail = false;
|
||||||
|
if (!empty($data['email'])) {
|
||||||
|
$hasEmail = filter_var($data['email'], FILTER_VALIDATE_EMAIL) !== false;
|
||||||
|
} elseif (!empty($encryptedEmail)) {
|
||||||
|
// L'email a déjà été validé lors du chiffrement
|
||||||
|
$hasEmail = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasEmail) {
|
||||||
|
try {
|
||||||
|
$receiptService = new \App\Services\ReceiptService();
|
||||||
|
$receiptGenerated = $receiptService->generateReceiptForPassage($passageId);
|
||||||
|
|
||||||
|
if ($receiptGenerated) {
|
||||||
|
LogService::log('Reçu généré automatiquement pour le passage', [
|
||||||
|
'level' => 'info',
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la génération automatique du reçu', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return; // Fin de la méthode, éviter d'exécuter le code après
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
LogService::log('Erreur lors de la création du passage', [
|
LogService::log('Erreur lors de la création du passage', [
|
||||||
'level' => 'error',
|
'level' => 'error',
|
||||||
@@ -705,10 +753,65 @@ class PassageController {
|
|||||||
'passageId' => $passageId
|
'passageId' => $passageId
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Envoyer la réponse immédiatement pour éviter les timeouts
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Passage mis à jour avec succès'
|
'message' => 'Passage mis à jour avec succès',
|
||||||
|
'receipt_generated' => false // On va générer le reçu en arrière-plan
|
||||||
], 200);
|
], 200);
|
||||||
|
|
||||||
|
// Flush la sortie pour s'assurer que la réponse est envoyée
|
||||||
|
if (ob_get_level()) {
|
||||||
|
ob_end_flush();
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
|
||||||
|
// Fermer la connexion HTTP mais continuer le traitement
|
||||||
|
if (function_exists('fastcgi_finish_request')) {
|
||||||
|
fastcgi_finish_request();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintenant générer le reçu en arrière-plan après avoir envoyé la réponse
|
||||||
|
try {
|
||||||
|
// Récupérer les données actualisées du passage
|
||||||
|
$stmt = $this->db->prepare('SELECT fk_type, encrypted_email, nom_recu FROM ope_pass WHERE id = ?');
|
||||||
|
$stmt->execute([$passageId]);
|
||||||
|
$updatedPassage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($updatedPassage) {
|
||||||
|
// Générer un reçu si :
|
||||||
|
// - C'est un don (fk_type = 1)
|
||||||
|
// - Il y a un email valide
|
||||||
|
// - Il n'y a pas encore de reçu (nom_recu est vide ou null)
|
||||||
|
if ((int)$updatedPassage['fk_type'] === 1 &&
|
||||||
|
!empty($updatedPassage['encrypted_email']) &&
|
||||||
|
empty($updatedPassage['nom_recu'])) {
|
||||||
|
|
||||||
|
// Vérifier que l'email est valide en le déchiffrant
|
||||||
|
$email = ApiService::decryptSearchableData($updatedPassage['encrypted_email']);
|
||||||
|
|
||||||
|
if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$receiptService = new \App\Services\ReceiptService();
|
||||||
|
$receiptGenerated = $receiptService->generateReceiptForPassage($passageId);
|
||||||
|
|
||||||
|
if ($receiptGenerated) {
|
||||||
|
LogService::log('Reçu généré automatiquement après mise à jour du passage', [
|
||||||
|
'level' => 'info',
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la génération automatique du reçu après mise à jour', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return; // Fin de la méthode, éviter d'exécuter le code après
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
LogService::log('Erreur lors de la mise à jour du passage', [
|
LogService::log('Erreur lors de la mise à jour du passage', [
|
||||||
'level' => 'error',
|
'level' => 'error',
|
||||||
@@ -740,8 +843,47 @@ class PassageController {
|
|||||||
|
|
||||||
$passageId = (int)$id;
|
$passageId = (int)$id;
|
||||||
|
|
||||||
|
// Récupérer le rôle 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userRole = (int)$user['fk_role'];
|
||||||
|
$entiteId = (int)$user['fk_entite'];
|
||||||
|
|
||||||
|
// Si l'utilisateur est un membre (fk_role = 1), vérifier les permissions de l'entité
|
||||||
|
if ($userRole === 1) {
|
||||||
|
$stmt = $this->db->prepare('SELECT chk_user_delete_pass FROM entites WHERE id = ?');
|
||||||
|
$stmt->execute([$entiteId]);
|
||||||
|
$entite = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$entite || (int)$entite['chk_user_delete_pass'] !== 1) {
|
||||||
|
LogService::log('Tentative de suppression de passage non autorisée', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'userId' => $userId,
|
||||||
|
'userRole' => $userRole,
|
||||||
|
'entiteId' => $entiteId,
|
||||||
|
'passageId' => $passageId,
|
||||||
|
'chk_user_delete_pass' => $entite ? $entite['chk_user_delete_pass'] : null
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous n\'avez pas l\'autorisation de supprimer des passages'
|
||||||
|
], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vérifier que le passage existe et appartient à l'entité de l'utilisateur
|
// Vérifier que le passage existe et appartient à l'entité de l'utilisateur
|
||||||
$entiteId = $this->getUserEntiteId($userId);
|
|
||||||
if (!$entiteId) {
|
if (!$entiteId) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
@@ -800,4 +942,150 @@ class PassageController {
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le reçu PDF d'un passage
|
||||||
|
*
|
||||||
|
* @param string $id ID du passage
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function getReceipt(string $id): void {
|
||||||
|
try {
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
if (!$userId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||||
|
], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$passageId = (int)$id;
|
||||||
|
|
||||||
|
// Vérifier que le passage existe et que l'utilisateur y a accès
|
||||||
|
$entiteId = $this->getUserEntiteId($userId);
|
||||||
|
if (!$entiteId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Entité non trouvée pour cet utilisateur'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les informations du passage et du reçu
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT p.id, p.nom_recu, p.date_creat_recu, p.fk_operation, o.fk_entite
|
||||||
|
FROM ope_pass p
|
||||||
|
INNER JOIN operations o ON p.fk_operation = o.id
|
||||||
|
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$passageId, $entiteId]);
|
||||||
|
$passage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$passage) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Passage non trouvé ou accès non autorisé'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($passage['nom_recu'])) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Aucun reçu disponible pour ce passage'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le fichier depuis la table medias
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT file_path, mime_type, file_size, fichier
|
||||||
|
FROM medias
|
||||||
|
WHERE support = ? AND support_id = ? AND file_category = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
$stmt->execute(['passage', $passageId, 'recu']);
|
||||||
|
$media = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$media) {
|
||||||
|
// Si pas trouvé dans medias, essayer de construire le chemin
|
||||||
|
$filePath = __DIR__ . '/../../uploads/entites/' . $passage['fk_entite'] .
|
||||||
|
'/recus/' . $passage['fk_operation'] . '/' . $passage['nom_recu'];
|
||||||
|
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Fichier reçu introuvable'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$media = [
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'fichier' => $passage['nom_recu'],
|
||||||
|
'file_size' => filesize($filePath)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le fichier existe
|
||||||
|
if (!file_exists($media['file_path'])) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Fichier reçu introuvable sur le serveur'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lire le contenu du fichier
|
||||||
|
$pdfContent = file_get_contents($media['file_path']);
|
||||||
|
if ($pdfContent === false) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Impossible de lire le fichier reçu'
|
||||||
|
], 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 1: Retourner le PDF directement (pour téléchargement)
|
||||||
|
if (isset($_GET['download']) && $_GET['download'] === 'true') {
|
||||||
|
header('Content-Type: ' . $media['mime_type']);
|
||||||
|
header('Content-Disposition: attachment; filename="' . $media['fichier'] . '"');
|
||||||
|
header('Content-Length: ' . $media['file_size']);
|
||||||
|
header('Cache-Control: no-cache, must-revalidate');
|
||||||
|
echo $pdfContent;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 2: Retourner le PDF en base64 dans JSON (pour Flutter)
|
||||||
|
$base64 = base64_encode($pdfContent);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'receipt' => [
|
||||||
|
'passage_id' => $passageId,
|
||||||
|
'file_name' => $media['fichier'],
|
||||||
|
'mime_type' => $media['mime_type'],
|
||||||
|
'file_size' => $media['file_size'],
|
||||||
|
'created_at' => $passage['date_creat_recu'],
|
||||||
|
'data_base64' => $base64
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la récupération du reçu', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'passageId' => $id,
|
||||||
|
'userId' => $userId ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la récupération du reçu'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
210
api/src/Controllers/PasswordController.php
Normal file
210
api/src/Controllers/PasswordController.php
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../Services/PasswordSecurityService.php';
|
||||||
|
require_once __DIR__ . '/../Services/LogService.php';
|
||||||
|
|
||||||
|
use Request;
|
||||||
|
use Response;
|
||||||
|
use LogService;
|
||||||
|
use App\Services\PasswordSecurityService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour la gestion de la sécurité des mots de passe
|
||||||
|
* Fournit des endpoints pour vérifier la force et la compromission des mots de passe
|
||||||
|
*/
|
||||||
|
class PasswordController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie la force d'un mot de passe et s'il a été compromis
|
||||||
|
* Endpoint utilisable sans authentification pour le formulaire d'inscription
|
||||||
|
*
|
||||||
|
* POST /api/password/check
|
||||||
|
*/
|
||||||
|
public function checkStrength(): void {
|
||||||
|
try {
|
||||||
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
if (!isset($data['password']) || empty($data['password'])) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Mot de passe requis'
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$password = $data['password'];
|
||||||
|
$checkCompromised = $data['check_compromised'] ?? true;
|
||||||
|
|
||||||
|
// Validation du mot de passe
|
||||||
|
$validation = PasswordSecurityService::validatePassword($password, $checkCompromised);
|
||||||
|
|
||||||
|
// Estimation de la force
|
||||||
|
$strength = PasswordSecurityService::estimatePasswordStrength($password);
|
||||||
|
|
||||||
|
// Vérification spécifique de compromission si demandée
|
||||||
|
$compromisedInfo = null;
|
||||||
|
if ($checkCompromised) {
|
||||||
|
$compromisedCheck = PasswordSecurityService::checkPasswordCompromised($password);
|
||||||
|
if ($compromisedCheck['compromised']) {
|
||||||
|
$compromisedInfo = [
|
||||||
|
'compromised' => true,
|
||||||
|
'occurrences' => $compromisedCheck['occurrences'],
|
||||||
|
'message' => sprintf(
|
||||||
|
'Ce mot de passe a été trouvé %s fois dans des fuites de données',
|
||||||
|
number_format($compromisedCheck['occurrences'], 0, ',', ' ')
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'valid' => $validation['valid'],
|
||||||
|
'errors' => $validation['errors'],
|
||||||
|
'warnings' => $validation['warnings'],
|
||||||
|
'strength' => $strength,
|
||||||
|
'compromised' => $compromisedInfo
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la vérification du mot de passe', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la vérification du mot de passe'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un mot de passe sécurisé aléatoire
|
||||||
|
* Endpoint nécessitant une authentification
|
||||||
|
*
|
||||||
|
* GET /api/password/generate
|
||||||
|
*/
|
||||||
|
public function generate(): void {
|
||||||
|
try {
|
||||||
|
// Vérifier l'authentification
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Authentification requise'
|
||||||
|
], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les paramètres optionnels
|
||||||
|
$length = isset($_GET['length']) ? intval($_GET['length']) : 14;
|
||||||
|
$length = max(12, min(20, $length)); // Limiter entre 12 et 20
|
||||||
|
|
||||||
|
// Générer un mot de passe non compromis
|
||||||
|
$password = PasswordSecurityService::generateSecurePassword($length);
|
||||||
|
|
||||||
|
if ($password === null) {
|
||||||
|
// En cas d'échec, utiliser la méthode classique
|
||||||
|
$password = $this->generateFallbackPassword($length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer la force du mot de passe généré
|
||||||
|
$strength = PasswordSecurityService::estimatePasswordStrength($password);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'password' => $password,
|
||||||
|
'length' => strlen($password),
|
||||||
|
'strength' => $strength
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la génération du mot de passe', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la génération du mot de passe'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie uniquement si un mot de passe est compromis
|
||||||
|
* Endpoint rapide pour vérification en temps réel
|
||||||
|
*
|
||||||
|
* POST /api/password/compromised
|
||||||
|
*/
|
||||||
|
public function checkCompromised(): void {
|
||||||
|
try {
|
||||||
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
if (!isset($data['password']) || empty($data['password'])) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Mot de passe requis'
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$password = $data['password'];
|
||||||
|
|
||||||
|
// Vérification de compromission
|
||||||
|
$compromisedCheck = PasswordSecurityService::checkPasswordCompromised($password);
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'status' => 'success',
|
||||||
|
'compromised' => $compromisedCheck['compromised'],
|
||||||
|
'occurrences' => $compromisedCheck['occurrences']
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($compromisedCheck['compromised']) {
|
||||||
|
$response['message'] = sprintf(
|
||||||
|
'Ce mot de passe a été trouvé %s fois dans des fuites de données',
|
||||||
|
number_format($compromisedCheck['occurrences'], 0, ',', ' ')
|
||||||
|
);
|
||||||
|
$response['recommendation'] = 'Il est fortement recommandé de choisir un autre mot de passe';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($compromisedCheck['error']) {
|
||||||
|
$response['warning'] = 'Impossible de vérifier complètement le mot de passe';
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::json($response);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la vérification de compromission', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la vérification'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un mot de passe de secours si le service principal échoue
|
||||||
|
*
|
||||||
|
* @param int $length Longueur du mot de passe
|
||||||
|
* @return string Le mot de passe généré
|
||||||
|
*/
|
||||||
|
private function generateFallbackPassword(int $length): string {
|
||||||
|
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||||
|
$password = '';
|
||||||
|
|
||||||
|
for ($i = 0; $i < $length; $i++) {
|
||||||
|
$password .= $chars[random_int(0, strlen($chars) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
}
|
||||||
251
api/src/Controllers/SecurityController.php
Normal file
251
api/src/Controllers/SecurityController.php
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../Services/Security/AlertService.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/SecurityMonitor.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/PerformanceMonitor.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/IPBlocker.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/EmailThrottler.php';
|
||||||
|
|
||||||
|
use App\Services\Security\AlertService;
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
use App\Services\Security\PerformanceMonitor;
|
||||||
|
use App\Services\Security\IPBlocker;
|
||||||
|
use App\Services\Security\EmailThrottler;
|
||||||
|
use Database;
|
||||||
|
use Session;
|
||||||
|
use Response;
|
||||||
|
use Request;
|
||||||
|
|
||||||
|
class SecurityController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les métriques de performance
|
||||||
|
* GET /api/admin/metrics
|
||||||
|
*/
|
||||||
|
public function getMetrics(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpoint = Request::getQuery('endpoint');
|
||||||
|
$hours = (int)(Request::getQuery('hours') ?? 24);
|
||||||
|
|
||||||
|
$stats = PerformanceMonitor::getStats($endpoint, $hours);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $stats
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les alertes actives
|
||||||
|
* GET /api/admin/alerts
|
||||||
|
*/
|
||||||
|
public function getAlerts(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = (int)(Request::getQuery('limit') ?? 50);
|
||||||
|
$alerts = AlertService::getActiveAlerts($limit);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $alerts
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résoudre une alerte
|
||||||
|
* POST /api/admin/alerts/:id/resolve
|
||||||
|
*/
|
||||||
|
public function resolveAlert(string $id): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::getJson();
|
||||||
|
$notes = $data['notes'] ?? '';
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
|
||||||
|
$success = AlertService::resolve((int)$id, $userId, $notes);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => $success,
|
||||||
|
'message' => $success ? 'Alerte résolue' : 'Erreur lors de la résolution'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les IPs bloquées
|
||||||
|
* GET /api/admin/blocked-ips
|
||||||
|
*/
|
||||||
|
public function getBlockedIPs(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeOnly = Request::getQuery('active_only') !== 'false';
|
||||||
|
$ips = IPBlocker::getBlockedIPs($activeOnly);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $ips
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Débloquer une IP
|
||||||
|
* POST /api/admin/unblock-ip
|
||||||
|
*/
|
||||||
|
public function unblockIP(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
if (!isset($data['ip'])) {
|
||||||
|
Response::json(['error' => 'IP address required'], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
$success = IPBlocker::unblock($data['ip'], $userId);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => $success,
|
||||||
|
'message' => $success ? 'IP débloquée' : 'Erreur lors du déblocage'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bloquer une IP manuellement
|
||||||
|
* POST /api/admin/block-ip
|
||||||
|
*/
|
||||||
|
public function blockIP(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
if (!isset($data['ip'])) {
|
||||||
|
Response::json(['error' => 'IP address required'], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reason = $data['reason'] ?? 'Blocked by admin';
|
||||||
|
$duration = (int)($data['duration'] ?? 3600);
|
||||||
|
$permanent = $data['permanent'] ?? false;
|
||||||
|
|
||||||
|
if ($permanent) {
|
||||||
|
$success = IPBlocker::blockPermanent($data['ip'], $reason, 'admin_' . Session::getUserId());
|
||||||
|
} else {
|
||||||
|
$success = IPBlocker::block($data['ip'], $duration, $reason, 'admin_' . Session::getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => $success,
|
||||||
|
'message' => $success ? 'IP bloquée' : 'Erreur lors du blocage'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir le rapport de sécurité
|
||||||
|
* GET /api/admin/security-report
|
||||||
|
*/
|
||||||
|
public function getSecurityReport(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compiler le rapport
|
||||||
|
$report = [
|
||||||
|
'security_stats' => SecurityMonitor::getSecurityStats(),
|
||||||
|
'performance_stats' => PerformanceMonitor::getStats(null, 24),
|
||||||
|
'blocked_ips_stats' => IPBlocker::getStats(),
|
||||||
|
'email_throttle_stats' => (new EmailThrottler())->getStats(),
|
||||||
|
'recent_alerts' => AlertService::getActiveAlerts(10)
|
||||||
|
];
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $report,
|
||||||
|
'generated_at' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyer les anciennes données
|
||||||
|
* POST /api/admin/cleanup
|
||||||
|
*/
|
||||||
|
public function cleanup(): void {
|
||||||
|
// Vérifier l'authentification et les droits super admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 9) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::getJson();
|
||||||
|
$daysToKeep = (int)($data['days_to_keep'] ?? 30);
|
||||||
|
|
||||||
|
// Nettoyer les métriques de performance
|
||||||
|
$cleanedMetrics = PerformanceMonitor::cleanup($daysToKeep);
|
||||||
|
|
||||||
|
// Nettoyer les IPs expirées
|
||||||
|
$cleanedIPs = IPBlocker::cleanupExpired();
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Nettoyage effectué',
|
||||||
|
'cleaned' => [
|
||||||
|
'performance_metrics' => $cleanedMetrics,
|
||||||
|
'expired_ips' => $cleanedIPs
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tester les alertes email
|
||||||
|
* POST /api/admin/test-alert
|
||||||
|
*/
|
||||||
|
public function testAlert(): void {
|
||||||
|
// Vérifier l'authentification et les droits super admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 9) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déclencher une alerte de test
|
||||||
|
AlertService::trigger('TEST_ALERT', [
|
||||||
|
'message' => 'Ceci est une alerte de test déclenchée manuellement',
|
||||||
|
'triggered_by' => Session::getUsername(),
|
||||||
|
'timestamp' => date('Y-m-d H:i:s')
|
||||||
|
], 'INFO');
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Alerte de test envoyée'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ namespace App\Controllers;
|
|||||||
|
|
||||||
require_once __DIR__ . '/../Services/LogService.php';
|
require_once __DIR__ . '/../Services/LogService.php';
|
||||||
require_once __DIR__ . '/../Services/ApiService.php';
|
require_once __DIR__ . '/../Services/ApiService.php';
|
||||||
|
require_once __DIR__ . '/../Services/PasswordSecurityService.php';
|
||||||
|
|
||||||
use PDO;
|
use PDO;
|
||||||
use PDOException;
|
use PDOException;
|
||||||
@@ -16,6 +17,7 @@ use Response;
|
|||||||
use Session;
|
use Session;
|
||||||
use LogService;
|
use LogService;
|
||||||
use ApiService;
|
use ApiService;
|
||||||
|
use App\Services\PasswordSecurityService;
|
||||||
|
|
||||||
class UserController {
|
class UserController {
|
||||||
private PDO $db;
|
private PDO $db;
|
||||||
@@ -212,11 +214,41 @@ class UserController {
|
|||||||
$data = Request::getJson();
|
$data = Request::getJson();
|
||||||
$currentUserId = Session::getUserId();
|
$currentUserId = Session::getUserId();
|
||||||
|
|
||||||
|
// Log de début de création avec les données reçues (sans données sensibles)
|
||||||
|
LogService::log('Tentative de création d\'utilisateur', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'fields_received' => array_keys($data ?? []),
|
||||||
|
'has_email' => isset($data['email']),
|
||||||
|
'has_name' => isset($data['name']),
|
||||||
|
'has_username' => isset($data['username']),
|
||||||
|
'has_password' => isset($data['password'])
|
||||||
|
]);
|
||||||
|
|
||||||
// Validation des données requises
|
// Validation des données requises
|
||||||
if (!isset($data['email'], $data['name'])) {
|
if (!isset($data['email']) || empty(trim($data['email']))) {
|
||||||
|
LogService::log('Erreur création utilisateur : Email manquant', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Email et nom requis'
|
'message' => 'Email requis',
|
||||||
|
'field' => 'email'
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($data['name']) || empty(trim($data['name']))) {
|
||||||
|
LogService::log('Erreur création utilisateur : Nom manquant', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $data['email'] ?? 'non fourni'
|
||||||
|
]);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Nom requis',
|
||||||
|
'field' => 'name'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -258,9 +290,16 @@ class UserController {
|
|||||||
|
|
||||||
// Validation de l'email
|
// Validation de l'email
|
||||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
LogService::log('Erreur création utilisateur : Format email invalide', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Format d\'email invalide'
|
'message' => 'Format d\'email invalide',
|
||||||
|
'field' => 'email',
|
||||||
|
'value' => $email
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -270,6 +309,8 @@ class UserController {
|
|||||||
$encryptedName = ApiService::encryptData($name);
|
$encryptedName = ApiService::encryptData($name);
|
||||||
|
|
||||||
// Vérification de l'existence de l'email
|
// Vérification de l'existence de l'email
|
||||||
|
// DÉSACTIVÉ : Le client souhaite permettre plusieurs comptes avec le même email
|
||||||
|
/*
|
||||||
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_email = ?');
|
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_email = ?');
|
||||||
$checkStmt->execute([$encryptedEmail]);
|
$checkStmt->execute([$encryptedEmail]);
|
||||||
if ($checkStmt->fetch()) {
|
if ($checkStmt->fetch()) {
|
||||||
@@ -279,26 +320,63 @@ class UserController {
|
|||||||
], 409);
|
], 409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Gestion du USERNAME selon chk_username_manuel
|
// Gestion du USERNAME selon chk_username_manuel
|
||||||
$encryptedUsername = '';
|
$encryptedUsername = '';
|
||||||
if ($chkUsernameManuel === 1) {
|
if ($chkUsernameManuel === 1) {
|
||||||
// Username manuel obligatoire
|
// Username manuel obligatoire
|
||||||
if (!isset($data['username']) || empty(trim($data['username']))) {
|
if (!isset($data['username']) || empty(trim($data['username']))) {
|
||||||
|
LogService::log('Erreur création utilisateur : Username manuel requis mais non fourni', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'entite_id' => $entiteId,
|
||||||
|
'chk_username_manuel' => $chkUsernameManuel
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Le nom d\'utilisateur est requis pour cette entité'
|
'message' => 'Identifiant requis',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Saisie manuelle obligatoire pour cette entité'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$username = trim(strtolower($data['username']));
|
// Trim du username mais on garde la casse originale (plus de lowercase forcé)
|
||||||
|
$username = trim($data['username']);
|
||||||
|
|
||||||
// Validation du format du username
|
// Validation ultra-souple : seulement la longueur en caractères UTF-8
|
||||||
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
|
$usernameLength = mb_strlen($username, 'UTF-8');
|
||||||
|
|
||||||
|
if ($usernameLength < 8) {
|
||||||
|
LogService::log('Erreur création utilisateur : Username trop court', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'username_length' => $usernameLength
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'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, ., -, _)'
|
'message' => 'Identifiant trop court',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Minimum 8 caractères'
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($usernameLength > 30) {
|
||||||
|
LogService::log('Erreur création utilisateur : Username trop long', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'username_length' => $usernameLength
|
||||||
|
]);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Identifiant trop long',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Maximum 30 caractères'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -311,7 +389,8 @@ class UserController {
|
|||||||
if ($checkUsernameStmt->fetch()) {
|
if ($checkUsernameStmt->fetch()) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Ce nom d\'utilisateur est déjà utilisé dans GeoSector'
|
'message' => 'Identifiant déjà utilisé',
|
||||||
|
'field' => 'username'
|
||||||
], 409);
|
], 409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -333,27 +412,49 @@ class UserController {
|
|||||||
if ($chkMdpManuel === 1) {
|
if ($chkMdpManuel === 1) {
|
||||||
// Mot de passe manuel obligatoire
|
// Mot de passe manuel obligatoire
|
||||||
if (!isset($data['password']) || empty($data['password'])) {
|
if (!isset($data['password']) || empty($data['password'])) {
|
||||||
|
LogService::log('Erreur création utilisateur : Mot de passe manuel requis mais non fourni', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'entite_id' => $entiteId,
|
||||||
|
'chk_mdp_manuel' => $chkMdpManuel
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Le mot de passe est requis pour cette entité'
|
'message' => 'Mot de passe requis',
|
||||||
|
'field' => 'password',
|
||||||
|
'details' => 'Saisie manuelle obligatoire pour cette entité'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$password = $data['password'];
|
$password = $data['password'];
|
||||||
|
|
||||||
// Validation du mot de passe (minimum 8 caractères)
|
// Validation du mot de passe selon NIST SP 800-63B
|
||||||
if (strlen($password) < 8) {
|
$passwordValidation = PasswordSecurityService::validatePassword($password);
|
||||||
|
|
||||||
|
if (!$passwordValidation['valid']) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Le mot de passe doit contenir au moins 8 caractères'
|
'message' => 'Mot de passe invalide',
|
||||||
|
'errors' => $passwordValidation['errors'],
|
||||||
|
'warnings' => $passwordValidation['warnings']
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Si le mot de passe a des avertissements mais est valide, les logger
|
||||||
|
if (!empty($passwordValidation['warnings'])) {
|
||||||
|
LogService::log('Mot de passe manuel avec avertissements accepté lors de la création', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'email' => $email,
|
||||||
|
'warnings' => $passwordValidation['warnings']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
} else {
|
} else {
|
||||||
// Génération automatique du mot de passe
|
// Génération automatique du mot de passe (déjà vérifié contre HIBP)
|
||||||
$password = ApiService::generateSecurePassword();
|
$password = ApiService::generateSecurePassword();
|
||||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
}
|
}
|
||||||
@@ -505,6 +606,9 @@ class UserController {
|
|||||||
$email = trim(strtolower($data['email']));
|
$email = trim(strtolower($data['email']));
|
||||||
$encryptedEmail = ApiService::encryptSearchableData($email);
|
$encryptedEmail = ApiService::encryptSearchableData($email);
|
||||||
|
|
||||||
|
// Vérification de l'unicité de l'email
|
||||||
|
// DÉSACTIVÉ : Le client souhaite permettre plusieurs comptes avec le même email
|
||||||
|
/*
|
||||||
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_email = ? AND id != ?');
|
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_email = ? AND id != ?');
|
||||||
$checkStmt->execute([$encryptedEmail, $id]);
|
$checkStmt->execute([$encryptedEmail, $id]);
|
||||||
if ($checkStmt->fetch()) {
|
if ($checkStmt->fetch()) {
|
||||||
@@ -514,6 +618,7 @@ class UserController {
|
|||||||
], 409);
|
], 409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
$updateFields[] = "encrypted_email = :encrypted_email";
|
$updateFields[] = "encrypted_email = :encrypted_email";
|
||||||
$params['encrypted_email'] = $encryptedEmail;
|
$params['encrypted_email'] = $encryptedEmail;
|
||||||
@@ -556,13 +661,28 @@ class UserController {
|
|||||||
|
|
||||||
// Mise à jour du mot de passe si fourni
|
// Mise à jour du mot de passe si fourni
|
||||||
if (isset($data['password']) && !empty($data['password'])) {
|
if (isset($data['password']) && !empty($data['password'])) {
|
||||||
if (strlen($data['password']) < 8) {
|
// Validation du mot de passe selon NIST SP 800-63B
|
||||||
|
$passwordValidation = PasswordSecurityService::validatePassword($data['password']);
|
||||||
|
|
||||||
|
if (!$passwordValidation['valid']) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Le mot de passe doit contenir au moins 8 caractères'
|
'message' => 'Mot de passe invalide',
|
||||||
|
'errors' => $passwordValidation['errors'],
|
||||||
|
'warnings' => $passwordValidation['warnings']
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Si le mot de passe a des avertissements mais est valide, les logger
|
||||||
|
if (!empty($passwordValidation['warnings'])) {
|
||||||
|
LogService::log('Mot de passe avec avertissements accepté', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'user_id' => $id,
|
||||||
|
'warnings' => $passwordValidation['warnings']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$updateFields[] = "user_pass_hash = :password";
|
$updateFields[] = "user_pass_hash = :password";
|
||||||
$params['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
$params['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||||
}
|
}
|
||||||
@@ -890,22 +1010,60 @@ class UserController {
|
|||||||
try {
|
try {
|
||||||
$data = Request::getJson();
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
// Log de la requête
|
||||||
|
LogService::log('Vérification de disponibilité username', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'checkedBy' => Session::getUserId(),
|
||||||
|
'has_username' => isset($data['username'])
|
||||||
|
]);
|
||||||
|
|
||||||
// Validation de la présence du username
|
// Validation de la présence du username
|
||||||
if (!isset($data['username']) || empty(trim($data['username']))) {
|
if (!isset($data['username']) || empty(trim($data['username']))) {
|
||||||
|
LogService::log('Erreur vérification username : Username manquant', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'checkedBy' => Session::getUserId()
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Username requis pour la vérification'
|
'message' => 'Identifiant requis',
|
||||||
|
'field' => 'username'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$username = trim(strtolower($data['username']));
|
// Trim du username mais on garde la casse originale
|
||||||
|
$username = trim($data['username']);
|
||||||
|
|
||||||
// Validation du format du username
|
// Validation ultra-souple : seulement la longueur en caractères UTF-8
|
||||||
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
|
$usernameLength = mb_strlen($username, 'UTF-8');
|
||||||
|
|
||||||
|
if ($usernameLength < 8) {
|
||||||
|
LogService::log('Erreur vérification username : Username trop court', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'checkedBy' => Session::getUserId(),
|
||||||
|
'username_length' => $usernameLength
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Format invalide : 10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _',
|
'message' => 'Identifiant trop court',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Minimum 8 caractères',
|
||||||
|
'available' => false
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($usernameLength > 30) {
|
||||||
|
LogService::log('Erreur vérification username : Username trop long', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'checkedBy' => Session::getUserId(),
|
||||||
|
'username_length' => $usernameLength
|
||||||
|
]);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Identifiant trop long',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Maximum 30 caractères',
|
||||||
'available' => false
|
'available' => false
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/MonitoredDatabase.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/AlertService.php';
|
||||||
|
|
||||||
|
use App\Services\Security\AlertService;
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
private static ?PDO $instance = null;
|
private static ?PDO $instance = null;
|
||||||
private static array $config;
|
private static array $config;
|
||||||
@@ -23,13 +28,22 @@ class Database {
|
|||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
self::$instance = new PDO(
|
// Utiliser MonitoredDatabase pour le monitoring
|
||||||
|
self::$instance = new MonitoredDatabase(
|
||||||
$dsn,
|
$dsn,
|
||||||
self::$config['username'],
|
self::$config['username'],
|
||||||
self::$config['password'],
|
self::$config['password'],
|
||||||
$options
|
$options
|
||||||
);
|
);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
|
// Créer une alerte pour la connexion échouée
|
||||||
|
AlertService::trigger('DB_CONNECTION', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'host' => self::$config['host'],
|
||||||
|
'database' => self::$config['name'],
|
||||||
|
'message' => 'Échec de connexion à la base de données'
|
||||||
|
], 'CRITICAL');
|
||||||
|
|
||||||
throw new RuntimeException("Database connection failed: " . $e->getMessage());
|
throw new RuntimeException("Database connection failed: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
187
api/src/Core/MonitoredDatabase.php
Normal file
187
api/src/Core/MonitoredDatabase.php
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../Services/Security/PerformanceMonitor.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/SecurityMonitor.php';
|
||||||
|
|
||||||
|
use App\Services\Security\PerformanceMonitor;
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe PDO étendue avec monitoring de sécurité et performance
|
||||||
|
*/
|
||||||
|
class MonitoredDatabase extends PDO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Préparer une requête avec monitoring
|
||||||
|
*/
|
||||||
|
public function prepare($statement, $options = []): PDOStatement|false {
|
||||||
|
// Démarrer le chronométrage
|
||||||
|
PerformanceMonitor::startDbQuery($statement);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = parent::prepare($statement, $options);
|
||||||
|
|
||||||
|
// Retourner un statement monitored
|
||||||
|
if ($stmt !== false) {
|
||||||
|
return new MonitoredStatement($stmt, $statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Terminer le chronométrage
|
||||||
|
PerformanceMonitor::endDbQuery();
|
||||||
|
|
||||||
|
// Analyser l'erreur SQL
|
||||||
|
SecurityMonitor::analyzeSQLError($e, $statement);
|
||||||
|
|
||||||
|
// Re-lancer l'exception
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécuter une requête directement avec monitoring
|
||||||
|
*/
|
||||||
|
public function exec($statement): int|false {
|
||||||
|
// Démarrer le chronométrage
|
||||||
|
PerformanceMonitor::startDbQuery($statement);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = parent::exec($statement);
|
||||||
|
|
||||||
|
// Terminer le chronométrage
|
||||||
|
PerformanceMonitor::endDbQuery();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Terminer le chronométrage
|
||||||
|
PerformanceMonitor::endDbQuery();
|
||||||
|
|
||||||
|
// Analyser l'erreur SQL
|
||||||
|
SecurityMonitor::analyzeSQLError($e, $statement);
|
||||||
|
|
||||||
|
// Re-lancer l'exception
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query avec monitoring
|
||||||
|
*/
|
||||||
|
public function query($statement, $mode = null, ...$args): PDOStatement|false {
|
||||||
|
// Démarrer le chronométrage
|
||||||
|
PerformanceMonitor::startDbQuery($statement);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Si pas de mode spécifié, utiliser query simple
|
||||||
|
if ($mode === null) {
|
||||||
|
$result = parent::query($statement);
|
||||||
|
} else {
|
||||||
|
$result = parent::query($statement, $mode, ...$args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminer le chronométrage
|
||||||
|
PerformanceMonitor::endDbQuery();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Terminer le chronométrage
|
||||||
|
PerformanceMonitor::endDbQuery();
|
||||||
|
|
||||||
|
// Analyser l'erreur SQL
|
||||||
|
SecurityMonitor::analyzeSQLError($e, $statement);
|
||||||
|
|
||||||
|
// Re-lancer l'exception
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PDOStatement étendu avec monitoring
|
||||||
|
*/
|
||||||
|
class MonitoredStatement extends PDOStatement {
|
||||||
|
|
||||||
|
private PDOStatement $stmt;
|
||||||
|
private string $query;
|
||||||
|
|
||||||
|
public function __construct(PDOStatement $stmt, string $query) {
|
||||||
|
$this->stmt = $stmt;
|
||||||
|
$this->query = $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécuter avec monitoring
|
||||||
|
*/
|
||||||
|
public function execute($params = null): bool {
|
||||||
|
// Démarrer le chronométrage (si pas déjà fait dans prepare)
|
||||||
|
PerformanceMonitor::startDbQuery($this->query);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->stmt->execute($params);
|
||||||
|
|
||||||
|
// Terminer le chronométrage
|
||||||
|
PerformanceMonitor::endDbQuery();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Terminer le chronométrage
|
||||||
|
PerformanceMonitor::endDbQuery();
|
||||||
|
|
||||||
|
// Analyser l'erreur SQL
|
||||||
|
SecurityMonitor::analyzeSQLError($e, $this->query);
|
||||||
|
|
||||||
|
// Re-lancer l'exception
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy vers le statement original pour toutes les autres méthodes
|
||||||
|
*/
|
||||||
|
public function __call($method, $args) {
|
||||||
|
return call_user_func_array([$this->stmt, $method], $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetch($mode = PDO::FETCH_DEFAULT, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0): mixed {
|
||||||
|
return $this->stmt->fetch($mode, $cursorOrientation, $cursorOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchAll($mode = PDO::FETCH_DEFAULT, ...$args): array {
|
||||||
|
return $this->stmt->fetchAll($mode, ...$args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchColumn($column = 0): mixed {
|
||||||
|
return $this->stmt->fetchColumn($column);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rowCount(): int {
|
||||||
|
return $this->stmt->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bindParam($param, &$var, $type = PDO::PARAM_STR, $maxLength = null, $driverOptions = null): bool {
|
||||||
|
return $this->stmt->bindParam($param, $var, $type, $maxLength, $driverOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bindValue($param, $value, $type = PDO::PARAM_STR): bool {
|
||||||
|
return $this->stmt->bindValue($param, $value, $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeCursor(): bool {
|
||||||
|
return $this->stmt->closeCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function errorCode(): ?string {
|
||||||
|
return $this->stmt->errorCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function errorInfo(): array {
|
||||||
|
return $this->stmt->errorInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,14 +54,17 @@ class Response {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log de débogage
|
// Log de débogage (désactivé car peut causer des problèmes avec de grandes réponses)
|
||||||
error_log('Envoi de la réponse JSON: ' . $jsonResponse);
|
// error_log('Envoi de la réponse JSON: ' . $jsonResponse);
|
||||||
|
|
||||||
// Envoyer la réponse
|
// Envoyer la réponse
|
||||||
echo $jsonResponse;
|
echo $jsonResponse;
|
||||||
|
|
||||||
// S'assurer que tout est envoyé
|
// S'assurer que tout est envoyé
|
||||||
flush();
|
flush();
|
||||||
|
|
||||||
|
// Terminer l'exécution pour éviter d'envoyer du contenu supplémentaire
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class Router {
|
|||||||
'lostpassword',
|
'lostpassword',
|
||||||
'log',
|
'log',
|
||||||
'villes', // Ajout de la route villes comme endpoint public pour l'autocomplétion du code postal
|
'villes', // Ajout de la route villes comme endpoint public pour l'autocomplétion du code postal
|
||||||
|
'password/check', // Vérification de la force des mots de passe (public pour l'inscription)
|
||||||
|
'password/compromised', // Vérification si un mot de passe est compromis
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
@@ -32,13 +34,14 @@ class Router {
|
|||||||
$this->post('log', ['LogController', 'index']);
|
$this->post('log', ['LogController', 'index']);
|
||||||
|
|
||||||
// Routes privées utilisateurs
|
// Routes privées utilisateurs
|
||||||
|
// IMPORTANT: Les routes spécifiques doivent être déclarées AVANT les routes avec paramètres
|
||||||
|
$this->post('users/check-username', ['UserController', 'checkUsername']); // Déplacé avant les routes avec :id
|
||||||
$this->get('users', ['UserController', 'getUsers']);
|
$this->get('users', ['UserController', 'getUsers']);
|
||||||
$this->get('users/:id', ['UserController', 'getUserById']);
|
$this->get('users/:id', ['UserController', 'getUserById']);
|
||||||
$this->post('users', ['UserController', 'createUser']);
|
$this->post('users', ['UserController', 'createUser']);
|
||||||
$this->put('users/:id', ['UserController', 'updateUser']);
|
$this->put('users/:id', ['UserController', 'updateUser']);
|
||||||
$this->delete('users/:id', ['UserController', 'deleteUser']);
|
$this->delete('users/:id', ['UserController', 'deleteUser']);
|
||||||
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
|
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
|
||||||
$this->post('users/check-username', ['UserController', 'checkUsername']);
|
|
||||||
$this->post('logout', ['LoginController', 'logout']);
|
$this->post('logout', ['LoginController', 'logout']);
|
||||||
|
|
||||||
// Routes entités
|
// Routes entités
|
||||||
@@ -67,6 +70,7 @@ class Router {
|
|||||||
// Routes passages
|
// Routes passages
|
||||||
$this->get('passages', ['PassageController', 'getPassages']);
|
$this->get('passages', ['PassageController', 'getPassages']);
|
||||||
$this->get('passages/:id', ['PassageController', 'getPassageById']);
|
$this->get('passages/:id', ['PassageController', 'getPassageById']);
|
||||||
|
$this->get('passages/:id/receipt', ['PassageController', 'getReceipt']);
|
||||||
$this->get('passages/operation/:operation_id', ['PassageController', 'getPassagesByOperation']);
|
$this->get('passages/operation/:operation_id', ['PassageController', 'getPassagesByOperation']);
|
||||||
$this->post('passages', ['PassageController', 'createPassage']);
|
$this->post('passages', ['PassageController', 'createPassage']);
|
||||||
$this->put('passages/:id', ['PassageController', 'updatePassage']);
|
$this->put('passages/:id', ['PassageController', 'updatePassage']);
|
||||||
@@ -90,6 +94,33 @@ class Router {
|
|||||||
$this->post('sectors', ['SectorController', 'create']);
|
$this->post('sectors', ['SectorController', 'create']);
|
||||||
$this->put('sectors/:id', ['SectorController', 'update']);
|
$this->put('sectors/:id', ['SectorController', 'update']);
|
||||||
$this->delete('sectors/:id', ['SectorController', 'delete']);
|
$this->delete('sectors/:id', ['SectorController', 'delete']);
|
||||||
|
|
||||||
|
// Routes mots de passe
|
||||||
|
$this->post('password/check', ['PasswordController', 'checkStrength']);
|
||||||
|
$this->post('password/compromised', ['PasswordController', 'checkCompromised']);
|
||||||
|
$this->get('password/generate', ['PasswordController', 'generate']);
|
||||||
|
|
||||||
|
// Routes du module Chat
|
||||||
|
$this->get('chat/rooms', ['ChatController', 'getRooms']);
|
||||||
|
$this->post('chat/rooms', ['ChatController', 'createRoom']);
|
||||||
|
$this->put('chat/rooms/:id', ['ChatController', 'updateRoom']);
|
||||||
|
$this->delete('chat/rooms/:id', ['ChatController', 'deleteRoom']);
|
||||||
|
$this->get('chat/rooms/:id/messages', ['ChatController', 'getRoomMessages']);
|
||||||
|
$this->post('chat/rooms/:id/messages', ['ChatController', 'sendMessage']);
|
||||||
|
$this->put('chat/messages/:id', ['ChatController', 'updateMessage']);
|
||||||
|
$this->post('chat/rooms/:id/read', ['ChatController', 'markAsRead']);
|
||||||
|
$this->get('chat/recipients', ['ChatController', 'getRecipients']);
|
||||||
|
|
||||||
|
// Routes du module Sécurité (Admin uniquement)
|
||||||
|
$this->get('admin/metrics', ['SecurityController', 'getMetrics']);
|
||||||
|
$this->get('admin/alerts', ['SecurityController', 'getAlerts']);
|
||||||
|
$this->post('admin/alerts/:id/resolve', ['SecurityController', 'resolveAlert']);
|
||||||
|
$this->get('admin/blocked-ips', ['SecurityController', 'getBlockedIPs']);
|
||||||
|
$this->post('admin/unblock-ip', ['SecurityController', 'unblockIP']);
|
||||||
|
$this->post('admin/block-ip', ['SecurityController', 'blockIP']);
|
||||||
|
$this->get('admin/security-report', ['SecurityController', 'getSecurityReport']);
|
||||||
|
$this->post('admin/cleanup', ['SecurityController', 'cleanup']);
|
||||||
|
$this->post('admin/test-alert', ['SecurityController', 'testAlert']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void {
|
public function handle(): void {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ declare(strict_types=1);
|
|||||||
use PHPMailer\PHPMailer\PHPMailer;
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
use PHPMailer\PHPMailer\SMTP;
|
use PHPMailer\PHPMailer\SMTP;
|
||||||
use PHPMailer\PHPMailer\Exception;
|
use PHPMailer\PHPMailer\Exception;
|
||||||
|
use App\Services\PasswordSecurityService;
|
||||||
|
|
||||||
require_once __DIR__ . '/EmailTemplates.php';
|
require_once __DIR__ . '/EmailTemplates.php';
|
||||||
|
require_once __DIR__ . '/PasswordSecurityService.php';
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
|
|
||||||
@@ -277,34 +279,49 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Génère un mot de passe sécurisé aléatoire
|
* Génère un mot de passe sécurisé aléatoire non compromis
|
||||||
|
* Utilise le service PasswordSecurityService pour vérifier contre HIBP
|
||||||
*
|
*
|
||||||
* @param int $minLength Longueur minimale du mot de passe (par défaut 12)
|
* @param int $minLength Longueur minimale du mot de passe (par défaut 12)
|
||||||
* @param int $maxLength Longueur maximale du mot de passe (par défaut 16)
|
* @param int $maxLength Longueur maximale du mot de passe (par défaut 16)
|
||||||
* @return string Mot de passe généré
|
* @return string Mot de passe généré
|
||||||
*/
|
*/
|
||||||
public static function generateSecurePassword(int $minLength = 12, int $maxLength = 16): string {
|
public static function generateSecurePassword(int $minLength = 12, int $maxLength = 16): string {
|
||||||
|
$length = random_int($minLength, $maxLength);
|
||||||
|
|
||||||
|
// Utiliser le nouveau service pour générer un mot de passe non compromis
|
||||||
|
$password = PasswordSecurityService::generateSecurePassword($length, 10);
|
||||||
|
|
||||||
|
// Si le service échoue (très rare), utiliser l'ancienne méthode
|
||||||
|
if ($password === null) {
|
||||||
|
LogService::log('Fallback vers génération de mot de passe classique', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'reason' => 'PasswordSecurityService a échoué'
|
||||||
|
]);
|
||||||
|
|
||||||
$lowercase = 'abcdefghijklmnopqrstuvwxyz';
|
$lowercase = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
$uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
$uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
$numbers = '0123456789';
|
$numbers = '0123456789';
|
||||||
$special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
$special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||||
|
|
||||||
$length = rand($minLength, $maxLength);
|
|
||||||
$password = '';
|
$password = '';
|
||||||
|
|
||||||
// Au moins un de chaque type
|
// Au moins un de chaque type
|
||||||
$password .= $lowercase[rand(0, strlen($lowercase) - 1)];
|
$password .= $lowercase[random_int(0, strlen($lowercase) - 1)];
|
||||||
$password .= $uppercase[rand(0, strlen($uppercase) - 1)];
|
$password .= $uppercase[random_int(0, strlen($uppercase) - 1)];
|
||||||
$password .= $numbers[rand(0, strlen($numbers) - 1)];
|
$password .= $numbers[random_int(0, strlen($numbers) - 1)];
|
||||||
$password .= $special[rand(0, strlen($special) - 1)];
|
$password .= $special[random_int(0, strlen($special) - 1)];
|
||||||
|
|
||||||
// Compléter avec des caractères aléatoires
|
// Compléter avec des caractères aléatoires
|
||||||
$all = $lowercase . $uppercase . $numbers . $special;
|
$all = $lowercase . $uppercase . $numbers . $special;
|
||||||
for ($i = strlen($password); $i < $length; $i++) {
|
for ($i = strlen($password); $i < $length; $i++) {
|
||||||
$password .= $all[rand(0, strlen($all) - 1)];
|
$password .= $all[random_int(0, strlen($all) - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mélanger le mot de passe
|
// Mélanger le mot de passe
|
||||||
return str_shuffle($password);
|
return str_shuffle($password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Créer le dossier de destination
|
// Créer le dossier de destination
|
||||||
$exportDir = $this->fileService->createDirectory($entiteId, "/{$entiteId}/operations/{$operationId}/exports/excel");
|
$exportDir = $this->fileService->createDirectory($entiteId, "/{$entiteId}/operations/{$operationId}");
|
||||||
|
|
||||||
LogService::log('exportDir', [
|
LogService::log('exportDir', [
|
||||||
'level' => 'warning',
|
'level' => 'warning',
|
||||||
@@ -138,7 +138,7 @@ class ExportService {
|
|||||||
$exportData = $this->collectOperationData($operationId, $entiteId);
|
$exportData = $this->collectOperationData($operationId, $entiteId);
|
||||||
|
|
||||||
// Créer le dossier de destination
|
// Créer le dossier de destination
|
||||||
$exportDir = $this->fileService->createDirectory($entiteId, "/{$entiteId}/operations/{$operationId}/exports/json");
|
$exportDir = $this->fileService->createDirectory($entiteId, "/{$entiteId}/operations/{$operationId}");
|
||||||
|
|
||||||
// Initialiser le service de chiffrement
|
// Initialiser le service de chiffrement
|
||||||
$backupService = new BackupEncryptionService();
|
$backupService = new BackupEncryptionService();
|
||||||
|
|||||||
488
api/src/Services/PDFGenerator.php
Normal file
488
api/src/Services/PDFGenerator.php
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Générateur de PDF avec support des images
|
||||||
|
* Version simplifiée basée sur FPDF
|
||||||
|
*/
|
||||||
|
class PDFGenerator {
|
||||||
|
protected $page = '';
|
||||||
|
protected $n = 2;
|
||||||
|
protected $offsets = [];
|
||||||
|
protected $buffer = '';
|
||||||
|
protected $pages = [];
|
||||||
|
protected $state = 0;
|
||||||
|
protected $compress = true;
|
||||||
|
protected $k;
|
||||||
|
protected $DefOrientation = 'P';
|
||||||
|
protected $CurOrientation;
|
||||||
|
protected $PageFormats = ['a4' => [595.28, 841.89]];
|
||||||
|
protected $DefPageFormat;
|
||||||
|
protected $CurPageFormat;
|
||||||
|
protected $PageSizes = [];
|
||||||
|
protected $wPt, $hPt;
|
||||||
|
protected $w, $h;
|
||||||
|
protected $lMargin;
|
||||||
|
protected $tMargin;
|
||||||
|
protected $rMargin;
|
||||||
|
protected $bMargin;
|
||||||
|
protected $cMargin;
|
||||||
|
protected $x, $y;
|
||||||
|
protected $lasth = 0;
|
||||||
|
protected $LineWidth;
|
||||||
|
protected $CoreFonts = ['helvetica'];
|
||||||
|
protected $fonts = [];
|
||||||
|
protected $FontFiles = [];
|
||||||
|
protected $diffs = [];
|
||||||
|
protected $FontFamily = '';
|
||||||
|
protected $FontStyle = '';
|
||||||
|
protected $underline = false;
|
||||||
|
protected $CurrentFont;
|
||||||
|
protected $FontSizePt = 12;
|
||||||
|
protected $FontSize;
|
||||||
|
protected $DrawColor = '0 G';
|
||||||
|
protected $FillColor = '0 g';
|
||||||
|
protected $TextColor = '0 g';
|
||||||
|
protected $ColorFlag = false;
|
||||||
|
protected $ws = 0;
|
||||||
|
protected $images = [];
|
||||||
|
protected $PageLinks = [];
|
||||||
|
protected $links = [];
|
||||||
|
protected $AutoPageBreak = true;
|
||||||
|
protected $PageBreakTrigger;
|
||||||
|
protected $InHeader = false;
|
||||||
|
protected $InFooter = false;
|
||||||
|
protected $ZoomMode;
|
||||||
|
protected $LayoutMode;
|
||||||
|
protected $title = '';
|
||||||
|
protected $subject = '';
|
||||||
|
protected $author = '';
|
||||||
|
protected $keywords = '';
|
||||||
|
protected $creator = '';
|
||||||
|
protected $AliasNbPages = '';
|
||||||
|
protected $PDFVersion = '1.3';
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->DefPageFormat = 'A4';
|
||||||
|
$this->CurPageFormat = $this->PageFormats['a4'];
|
||||||
|
$this->DefOrientation = 'P';
|
||||||
|
$this->CurOrientation = $this->DefOrientation;
|
||||||
|
$this->k = 72 / 25.4; // Conversion factor
|
||||||
|
|
||||||
|
// Page dimensions
|
||||||
|
$this->wPt = $this->CurPageFormat[0];
|
||||||
|
$this->hPt = $this->CurPageFormat[1];
|
||||||
|
$this->w = $this->wPt / $this->k;
|
||||||
|
$this->h = $this->hPt / $this->k;
|
||||||
|
|
||||||
|
// Page margins (1 cm)
|
||||||
|
$margin = 28.35 / $this->k;
|
||||||
|
$this->SetMargins($margin, $margin);
|
||||||
|
$this->cMargin = $margin / 10;
|
||||||
|
$this->LineWidth = .567 / $this->k;
|
||||||
|
$this->SetAutoPageBreak(true, 2 * $margin);
|
||||||
|
$this->SetDisplayMode('default');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetMargins($left, $top, $right = null) {
|
||||||
|
$this->lMargin = $left;
|
||||||
|
$this->tMargin = $top;
|
||||||
|
if($right === null)
|
||||||
|
$right = $left;
|
||||||
|
$this->rMargin = $right;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetAutoPageBreak($auto, $margin = 0) {
|
||||||
|
$this->AutoPageBreak = $auto;
|
||||||
|
$this->bMargin = $margin;
|
||||||
|
$this->PageBreakTrigger = $this->h - $margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetDisplayMode($zoom, $layout = 'default') {
|
||||||
|
$this->ZoomMode = $zoom;
|
||||||
|
$this->LayoutMode = $layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function AddPage($orientation = '', $format = '') {
|
||||||
|
if($this->state == 0)
|
||||||
|
$this->Open();
|
||||||
|
|
||||||
|
$family = $this->FontFamily;
|
||||||
|
$style = $this->FontStyle . ($this->underline ? 'U' : '');
|
||||||
|
$fontsize = $this->FontSizePt;
|
||||||
|
$lw = $this->LineWidth;
|
||||||
|
$dc = $this->DrawColor;
|
||||||
|
$fc = $this->FillColor;
|
||||||
|
$tc = $this->TextColor;
|
||||||
|
$cf = $this->ColorFlag;
|
||||||
|
|
||||||
|
if($this->page > 0) {
|
||||||
|
$this->_endpage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_beginpage($orientation, $format);
|
||||||
|
$this->_out('2 J');
|
||||||
|
$this->LineWidth = $lw;
|
||||||
|
$this->_out(sprintf('%.2F w', $lw * $this->k));
|
||||||
|
|
||||||
|
if($family)
|
||||||
|
$this->SetFont($family, $style, $fontsize);
|
||||||
|
|
||||||
|
$this->DrawColor = $dc;
|
||||||
|
if($dc != '0 G')
|
||||||
|
$this->_out($dc);
|
||||||
|
$this->FillColor = $fc;
|
||||||
|
if($fc != '0 g')
|
||||||
|
$this->_out($fc);
|
||||||
|
$this->TextColor = $tc;
|
||||||
|
$this->ColorFlag = $cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetFont($family, $style = '', $size = 0) {
|
||||||
|
$family = strtolower($family);
|
||||||
|
if($family == '')
|
||||||
|
$family = $this->FontFamily;
|
||||||
|
if($family == 'arial')
|
||||||
|
$family = 'helvetica';
|
||||||
|
|
||||||
|
if($size == 0)
|
||||||
|
$size = $this->FontSizePt;
|
||||||
|
|
||||||
|
if($this->FontFamily == $family && $this->FontStyle == $style && $this->FontSizePt == $size)
|
||||||
|
return;
|
||||||
|
|
||||||
|
$this->FontFamily = $family;
|
||||||
|
$this->FontStyle = $style;
|
||||||
|
$this->FontSizePt = $size;
|
||||||
|
$this->FontSize = $size / $this->k;
|
||||||
|
|
||||||
|
if($this->page > 0)
|
||||||
|
$this->_out(sprintf('BT /F%d %.2F Tf ET', 1, $this->FontSizePt));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Text($x, $y, $txt) {
|
||||||
|
$s = sprintf('BT %.2F %.2F Td (%s) Tj ET', $x * $this->k, ($this->h - $y) * $this->k, $this->_escape($txt));
|
||||||
|
if($this->underline && $txt != '')
|
||||||
|
$s .= ' ' . $this->_dounderline($x, $y, $txt);
|
||||||
|
if($this->ColorFlag)
|
||||||
|
$s = 'q ' . $this->TextColor . ' ' . $s . ' Q';
|
||||||
|
$this->_out($s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Cell($w, $h = 0, $txt = '', $border = 0, $ln = 0, $align = '', $fill = false) {
|
||||||
|
$k = $this->k;
|
||||||
|
if($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AutoPageBreak) {
|
||||||
|
$x = $this->x;
|
||||||
|
$ws = $this->ws;
|
||||||
|
if($ws > 0) {
|
||||||
|
$this->ws = 0;
|
||||||
|
$this->_out('0 Tw');
|
||||||
|
}
|
||||||
|
$this->AddPage($this->CurOrientation, $this->CurPageFormat);
|
||||||
|
$this->x = $x;
|
||||||
|
if($ws > 0) {
|
||||||
|
$this->ws = $ws;
|
||||||
|
$this->_out(sprintf('%.3F Tw', $ws * $k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($w == 0)
|
||||||
|
$w = $this->w - $this->rMargin - $this->x;
|
||||||
|
|
||||||
|
$s = '';
|
||||||
|
if($fill || $border == 1) {
|
||||||
|
if($fill)
|
||||||
|
$op = ($border == 1) ? 'B' : 'f';
|
||||||
|
else
|
||||||
|
$op = 'S';
|
||||||
|
$s = sprintf('%.2F %.2F %.2F %.2F re %s ',
|
||||||
|
$this->x * $k, ($this->h - $this->y) * $k, $w * $k, -$h * $k, $op);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(is_string($border)) {
|
||||||
|
$x = $this->x;
|
||||||
|
$y = $this->y;
|
||||||
|
if(strpos($border, 'L') !== false)
|
||||||
|
$s .= sprintf('%.2F %.2F m %.2F %.2F l S ',
|
||||||
|
$x * $k, ($this->h - $y) * $k, $x * $k, ($this->h - ($y + $h)) * $k);
|
||||||
|
if(strpos($border, 'T') !== false)
|
||||||
|
$s .= sprintf('%.2F %.2F m %.2F %.2F l S ',
|
||||||
|
$x * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - $y) * $k);
|
||||||
|
if(strpos($border, 'R') !== false)
|
||||||
|
$s .= sprintf('%.2F %.2F m %.2F %.2F l S ',
|
||||||
|
($x + $w) * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k);
|
||||||
|
if(strpos($border, 'B') !== false)
|
||||||
|
$s .= sprintf('%.2F %.2F m %.2F %.2F l S ',
|
||||||
|
$x * $k, ($this->h - ($y + $h)) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($txt !== '') {
|
||||||
|
if($align == 'R')
|
||||||
|
$dx = $w - $this->cMargin - $this->GetStringWidth($txt);
|
||||||
|
elseif($align == 'C')
|
||||||
|
$dx = ($w - $this->GetStringWidth($txt)) / 2;
|
||||||
|
else
|
||||||
|
$dx = $this->cMargin;
|
||||||
|
|
||||||
|
if($this->ColorFlag)
|
||||||
|
$s .= 'q ' . $this->TextColor . ' ';
|
||||||
|
|
||||||
|
$txt2 = str_replace(')', '\\)', str_replace('(', '\\(', str_replace('\\', '\\\\', $txt)));
|
||||||
|
$s .= sprintf('BT %.2F %.2F Td (%s) Tj ET',
|
||||||
|
($this->x + $dx) * $k, ($this->h - ($this->y + .5 * $h + .3 * $this->FontSize)) * $k, $txt2);
|
||||||
|
|
||||||
|
if($this->underline)
|
||||||
|
$s .= ' ' . $this->_dounderline($this->x + $dx, $this->y + .5 * $h + .3 * $this->FontSize, $txt);
|
||||||
|
|
||||||
|
if($this->ColorFlag)
|
||||||
|
$s .= ' Q';
|
||||||
|
}
|
||||||
|
|
||||||
|
if($s)
|
||||||
|
$this->_out($s);
|
||||||
|
|
||||||
|
$this->lasth = $h;
|
||||||
|
|
||||||
|
if($ln > 0) {
|
||||||
|
$this->y += $h;
|
||||||
|
if($ln == 1)
|
||||||
|
$this->x = $this->lMargin;
|
||||||
|
} else
|
||||||
|
$this->x += $w;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Ln($h = null) {
|
||||||
|
$this->x = $this->lMargin;
|
||||||
|
if($h === null)
|
||||||
|
$this->y += $this->lasth;
|
||||||
|
else
|
||||||
|
$this->y += $h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Image($file, $x = null, $y = null, $w = 0, $h = 0) {
|
||||||
|
// Pour simplifier, on va juste créer un rectangle avec texte "LOGO"
|
||||||
|
// Dans une vraie implémentation, il faudrait encoder l'image
|
||||||
|
|
||||||
|
if($x === null)
|
||||||
|
$x = $this->x;
|
||||||
|
if($y === null)
|
||||||
|
$y = $this->y;
|
||||||
|
|
||||||
|
if($w == 0)
|
||||||
|
$w = 30;
|
||||||
|
if($h == 0)
|
||||||
|
$h = 30;
|
||||||
|
|
||||||
|
// Dessiner un rectangle pour représenter le logo
|
||||||
|
$this->Rect($x, $y, $w, $h);
|
||||||
|
|
||||||
|
// Ajouter le texte LOGO au centre
|
||||||
|
$oldX = $this->x;
|
||||||
|
$oldY = $this->y;
|
||||||
|
$this->SetXY($x + $w/2 - 8, $y + $h/2 - 2);
|
||||||
|
$this->Cell(16, 4, 'LOGO', 0, 0, 'C');
|
||||||
|
$this->SetXY($oldX, $oldY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Rect($x, $y, $w, $h, $style = '') {
|
||||||
|
if($style == 'F')
|
||||||
|
$op = 'f';
|
||||||
|
elseif($style == 'FD' || $style == 'DF')
|
||||||
|
$op = 'B';
|
||||||
|
else
|
||||||
|
$op = 'S';
|
||||||
|
$this->_out(sprintf('%.2F %.2F %.2F %.2F re %s',
|
||||||
|
$x * $this->k, ($this->h - $y) * $this->k, $w * $this->k, -$h * $this->k, $op));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Line($x1, $y1, $x2, $y2) {
|
||||||
|
$this->_out(sprintf('%.2F %.2F m %.2F %.2F l S',
|
||||||
|
$x1 * $this->k, ($this->h - $y1) * $this->k, $x2 * $this->k, ($this->h - $y2) * $this->k));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetStringWidth($s) {
|
||||||
|
$cw = ['helvetica' => [' ' => 278, '!' => 278, '"' => 355, '#' => 556, '$' => 556, '%' => 889, '&' => 667]];
|
||||||
|
$w = 0;
|
||||||
|
$l = strlen($s);
|
||||||
|
for($i = 0; $i < $l; $i++)
|
||||||
|
$w += 600; // Approximation
|
||||||
|
return $w * $this->FontSize / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetXY($x, $y) {
|
||||||
|
$this->SetX($x);
|
||||||
|
$this->SetY($y, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetX($x) {
|
||||||
|
if($x >= 0)
|
||||||
|
$this->x = $x;
|
||||||
|
else
|
||||||
|
$this->x = $this->w + $x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetY($y, $resetX = true) {
|
||||||
|
$this->y = $y;
|
||||||
|
if($resetX)
|
||||||
|
$this->x = $this->lMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Output() {
|
||||||
|
if($this->state < 3)
|
||||||
|
$this->Close();
|
||||||
|
return $this->buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Open() {
|
||||||
|
$this->state = 1;
|
||||||
|
$this->_out('%PDF-' . $this->PDFVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Close() {
|
||||||
|
if($this->state == 3)
|
||||||
|
return;
|
||||||
|
if($this->page == 0)
|
||||||
|
$this->AddPage();
|
||||||
|
|
||||||
|
$this->_endpage();
|
||||||
|
$this->_enddoc();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _beginpage($orientation, $format) {
|
||||||
|
$this->page++;
|
||||||
|
$this->pages[$this->page] = '';
|
||||||
|
$this->state = 2;
|
||||||
|
$this->x = $this->lMargin;
|
||||||
|
$this->y = $this->tMargin;
|
||||||
|
$this->FontFamily = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _endpage() {
|
||||||
|
$this->state = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _escape($s) {
|
||||||
|
$s = str_replace('\\', '\\\\', $s);
|
||||||
|
$s = str_replace('(', '\\(', $s);
|
||||||
|
$s = str_replace(')', '\\)', $s);
|
||||||
|
$s = str_replace("\r", '\\r', $s);
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _dounderline($x, $y, $txt) {
|
||||||
|
$up = -100;
|
||||||
|
$ut = 50;
|
||||||
|
$w = $this->GetStringWidth($txt) + $this->ws * substr_count($txt, ' ');
|
||||||
|
return sprintf('%.2F %.2F %.2F %.2F re f',
|
||||||
|
$x * $this->k, ($this->h - ($y - $up / 1000 * $this->FontSize)) * $this->k,
|
||||||
|
$w * $this->k, -$ut / 1000 * $this->FontSizePt);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _out($s) {
|
||||||
|
if($this->state == 2)
|
||||||
|
$this->pages[$this->page] .= $s . "\n";
|
||||||
|
else
|
||||||
|
$this->buffer .= $s . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _enddoc() {
|
||||||
|
$this->_putheader();
|
||||||
|
$this->_putpages();
|
||||||
|
$this->_putresources();
|
||||||
|
|
||||||
|
$this->_newobj();
|
||||||
|
$this->_out('<<');
|
||||||
|
$this->_out('/Type /Catalog');
|
||||||
|
$this->_out('/Pages 1 0 R');
|
||||||
|
$this->_out('>>');
|
||||||
|
$this->_out('endobj');
|
||||||
|
|
||||||
|
$o = strlen($this->buffer);
|
||||||
|
$this->_out('xref');
|
||||||
|
$this->_out('0 ' . ($this->n + 1));
|
||||||
|
$this->_out('0000000000 65535 f ');
|
||||||
|
for($i = 1; $i <= $this->n; $i++)
|
||||||
|
$this->_out(sprintf('%010d 00000 n ', $this->offsets[$i]));
|
||||||
|
|
||||||
|
$this->_out('trailer');
|
||||||
|
$this->_out('<<');
|
||||||
|
$this->_out('/Size ' . ($this->n + 1));
|
||||||
|
$this->_out('/Root ' . $this->n . ' 0 R');
|
||||||
|
$this->_out('/Info ' . ($this->n - 1) . ' 0 R');
|
||||||
|
$this->_out('>>');
|
||||||
|
$this->_out('startxref');
|
||||||
|
$this->_out($o);
|
||||||
|
$this->_out('%%EOF');
|
||||||
|
$this->state = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _putheader() {
|
||||||
|
$this->_out('%PDF-' . $this->PDFVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _putpages() {
|
||||||
|
$nb = $this->page;
|
||||||
|
$n = $this->n;
|
||||||
|
|
||||||
|
for($page = 1; $page <= $nb; $page++) {
|
||||||
|
$this->_newobj();
|
||||||
|
$this->_out('<</Type /Page');
|
||||||
|
$this->_out('/Parent 1 0 R');
|
||||||
|
$this->_out('/Resources 2 0 R');
|
||||||
|
$this->_out('/Contents ' . ($this->n + 1) . ' 0 R>>');
|
||||||
|
$this->_out('endobj');
|
||||||
|
|
||||||
|
$this->_newobj();
|
||||||
|
$filter = ($this->compress) ? '/Filter /FlateDecode ' : '';
|
||||||
|
$p = ($this->compress) ? gzcompress($this->pages[$page]) : $this->pages[$page];
|
||||||
|
$this->_out('<<' . $filter . '/Length ' . strlen($p) . '>>');
|
||||||
|
$this->_putstream($p);
|
||||||
|
$this->_out('endobj');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->offsets[1] = strlen($this->buffer);
|
||||||
|
$this->_out('1 0 obj');
|
||||||
|
$this->_out('<</Type /Pages');
|
||||||
|
$kids = '/Kids [';
|
||||||
|
for($i = 0; $i < $nb; $i++)
|
||||||
|
$kids .= (3 + 2 * $i) . ' 0 R ';
|
||||||
|
$this->_out($kids . ']');
|
||||||
|
$this->_out('/Count ' . $nb);
|
||||||
|
$this->_out(sprintf('/MediaBox [0 0 %.2F %.2F]', $this->wPt, $this->hPt));
|
||||||
|
$this->_out('>>');
|
||||||
|
$this->_out('endobj');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _putresources() {
|
||||||
|
$this->_putfonts();
|
||||||
|
|
||||||
|
$this->offsets[2] = strlen($this->buffer);
|
||||||
|
$this->_out('2 0 obj');
|
||||||
|
$this->_out('<<');
|
||||||
|
$this->_out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
|
||||||
|
$this->_out('/Font <<');
|
||||||
|
$this->_out('/F1 <</Type /Font /Subtype /Type1 /BaseFont /Helvetica>>');
|
||||||
|
$this->_out('>>');
|
||||||
|
$this->_out('>>');
|
||||||
|
$this->_out('endobj');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _putfonts() {
|
||||||
|
// Simplified - fonts are embedded in resources
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _newobj() {
|
||||||
|
$this->n++;
|
||||||
|
$this->offsets[$this->n] = strlen($this->buffer);
|
||||||
|
$this->_out($this->n . ' 0 obj');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _putstream($s) {
|
||||||
|
$this->_out('stream');
|
||||||
|
$this->buffer .= $s;
|
||||||
|
$this->_out('endstream');
|
||||||
|
}
|
||||||
|
}
|
||||||
415
api/src/Services/PasswordSecurityService.php
Normal file
415
api/src/Services/PasswordSecurityService.php
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use LogService;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/LogService.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de sécurité des mots de passe conforme à NIST SP 800-63B
|
||||||
|
* Vérifie les mots de passe contre la base de données Have I Been Pwned
|
||||||
|
* Utilise l'API k-anonymity pour préserver la confidentialité
|
||||||
|
*/
|
||||||
|
class PasswordSecurityService {
|
||||||
|
|
||||||
|
private const HIBP_API_URL = 'https://api.pwnedpasswords.com/range/';
|
||||||
|
private const MIN_PASSWORD_LENGTH = 8;
|
||||||
|
private const MAX_PASSWORD_LENGTH = 64;
|
||||||
|
private const REQUEST_TIMEOUT = 5; // secondes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un mot de passe a été compromis
|
||||||
|
* Utilise l'API Have I Been Pwned avec k-anonymity
|
||||||
|
*
|
||||||
|
* @param string $password Le mot de passe à vérifier
|
||||||
|
* @return array ['compromised' => bool, 'occurrences' => int, 'error' => string|null]
|
||||||
|
*/
|
||||||
|
public static function checkPasswordCompromised(string $password): array {
|
||||||
|
try {
|
||||||
|
// Calculer le hash SHA-1 du mot de passe
|
||||||
|
$sha1 = strtoupper(sha1($password));
|
||||||
|
|
||||||
|
// Extraire les 5 premiers caractères pour k-anonymity
|
||||||
|
$prefix = substr($sha1, 0, 5);
|
||||||
|
$suffix = substr($sha1, 5);
|
||||||
|
|
||||||
|
// Appeler l'API HIBP
|
||||||
|
$response = self::callHibpApi($prefix);
|
||||||
|
|
||||||
|
if ($response === null) {
|
||||||
|
// En cas d'erreur API, on laisse passer le mot de passe
|
||||||
|
// pour ne pas bloquer l'utilisateur (fail open)
|
||||||
|
return [
|
||||||
|
'compromised' => false,
|
||||||
|
'occurrences' => 0,
|
||||||
|
'error' => 'Impossible de vérifier le mot de passe contre la base de données'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rechercher le suffixe dans la réponse
|
||||||
|
$lines = explode("\n", $response);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (empty($line)) continue;
|
||||||
|
|
||||||
|
[$hashSuffix, $count] = explode(':', $line);
|
||||||
|
if ($hashSuffix === $suffix) {
|
||||||
|
LogService::log('Mot de passe compromis détecté', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'occurrences' => intval($count)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'compromised' => true,
|
||||||
|
'occurrences' => intval($count),
|
||||||
|
'error' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mot de passe non trouvé dans la base de données
|
||||||
|
return [
|
||||||
|
'compromised' => false,
|
||||||
|
'occurrences' => 0,
|
||||||
|
'error' => null
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la vérification HIBP', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// En cas d'erreur, on laisse passer (fail open)
|
||||||
|
return [
|
||||||
|
'compromised' => false,
|
||||||
|
'occurrences' => 0,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appelle l'API Have I Been Pwned
|
||||||
|
*
|
||||||
|
* @param string $prefix Les 5 premiers caractères du hash SHA-1
|
||||||
|
* @return string|null La réponse de l'API ou null en cas d'erreur
|
||||||
|
*/
|
||||||
|
private static function callHibpApi(string $prefix): ?string {
|
||||||
|
$url = self::HIBP_API_URL . $prefix;
|
||||||
|
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'header' => [
|
||||||
|
'User-Agent: GeoSector-API',
|
||||||
|
'Accept: text/plain'
|
||||||
|
],
|
||||||
|
'timeout' => self::REQUEST_TIMEOUT,
|
||||||
|
'ignore_errors' => false
|
||||||
|
],
|
||||||
|
'ssl' => [
|
||||||
|
'verify_peer' => true,
|
||||||
|
'verify_peer_name' => true
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = @file_get_contents($url, false, $context);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
LogService::log('Échec de l\'appel à l\'API HIBP', [
|
||||||
|
'level' => 'error',
|
||||||
|
'url' => $url
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide un mot de passe selon les critères NIST SP 800-63B
|
||||||
|
* NIST recommande d'être très permissif : pas d'obligation de composition
|
||||||
|
*
|
||||||
|
* @param string $password Le mot de passe à valider
|
||||||
|
* @param bool $checkCompromised Vérifier si le mot de passe est compromis
|
||||||
|
* @return array ['valid' => bool, 'errors' => array, 'warnings' => array]
|
||||||
|
*/
|
||||||
|
public static function validatePassword(string $password, bool $checkCompromised = true): array {
|
||||||
|
$errors = [];
|
||||||
|
$warnings = [];
|
||||||
|
|
||||||
|
// Calculer la longueur réelle en tenant compte de l'UTF-8
|
||||||
|
$length = mb_strlen($password, 'UTF-8');
|
||||||
|
|
||||||
|
// Vérification de la longueur minimale (NIST : minimum 8)
|
||||||
|
if ($length < self::MIN_PASSWORD_LENGTH) {
|
||||||
|
$errors[] = sprintf('Le mot de passe doit contenir au moins %d caractères', self::MIN_PASSWORD_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification de la longueur maximale (NIST : maximum 64 minimum)
|
||||||
|
if ($length > self::MAX_PASSWORD_LENGTH) {
|
||||||
|
$errors[] = sprintf('Le mot de passe ne doit pas dépasser %d caractères', self::MAX_PASSWORD_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NIST : Les espaces sont acceptés (pas d'erreur, juste un avertissement informatif)
|
||||||
|
if ($password !== trim($password)) {
|
||||||
|
// C'est juste informatif, pas une erreur selon NIST
|
||||||
|
$warnings[] = 'Note : Le mot de passe contient des espaces en début ou fin (c\'est autorisé)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification contre les mots de passe compromis (NIST : obligatoire)
|
||||||
|
if ($checkCompromised && empty($errors)) {
|
||||||
|
$compromisedCheck = self::checkPasswordCompromised($password);
|
||||||
|
|
||||||
|
if ($compromisedCheck['compromised']) {
|
||||||
|
$errors[] = sprintf(
|
||||||
|
'Ce mot de passe a été trouvé %s fois dans des fuites de données. Veuillez en choisir un autre.',
|
||||||
|
number_format($compromisedCheck['occurrences'], 0, ',', ' ')
|
||||||
|
);
|
||||||
|
} elseif ($compromisedCheck['error']) {
|
||||||
|
$warnings[] = 'Impossible de vérifier si le mot de passe a été compromis';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avertissements optionnels (pas des erreurs selon NIST)
|
||||||
|
// Ces vérifications sont juste informatives
|
||||||
|
if (self::hasSimplePattern($password)) {
|
||||||
|
$warnings[] = 'Suggestion : Évitez les motifs répétitifs pour plus de sécurité';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::hasCommonSequence($password)) {
|
||||||
|
$warnings[] = 'Suggestion : Évitez les séquences communes pour plus de sécurité';
|
||||||
|
}
|
||||||
|
|
||||||
|
// NIST : Pas d'obligation de majuscules, minuscules, chiffres ou caractères spéciaux
|
||||||
|
// On peut ajouter des suggestions mais PAS d'erreurs
|
||||||
|
$hasLower = preg_match('/[a-z]/u', $password);
|
||||||
|
$hasUpper = preg_match('/[A-Z]/u', $password);
|
||||||
|
$hasDigit = preg_match('/[0-9]/u', $password);
|
||||||
|
$hasSpecial = preg_match('/[^a-zA-Z0-9]/u', $password);
|
||||||
|
|
||||||
|
$complexity = ($hasLower ? 1 : 0) + ($hasUpper ? 1 : 0) + ($hasDigit ? 1 : 0) + ($hasSpecial ? 1 : 0);
|
||||||
|
|
||||||
|
if ($complexity < 2 && $length < 12) {
|
||||||
|
$warnings[] = 'Suggestion : Un mot de passe plus long ou plus varié serait plus sécurisé';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'valid' => empty($errors),
|
||||||
|
'errors' => $errors,
|
||||||
|
'warnings' => $warnings
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un mot de passe sécurisé non compromis
|
||||||
|
*
|
||||||
|
* @param int $length Longueur du mot de passe (12-20 caractères)
|
||||||
|
* @param int $maxAttempts Nombre maximum de tentatives
|
||||||
|
* @return string|null Le mot de passe généré ou null si échec
|
||||||
|
*/
|
||||||
|
public static function generateSecurePassword(int $length = 14, int $maxAttempts = 10): ?string {
|
||||||
|
$length = max(12, min(20, $length));
|
||||||
|
|
||||||
|
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
|
||||||
|
// Générer un mot de passe aléatoire
|
||||||
|
$password = self::generateRandomPassword($length);
|
||||||
|
|
||||||
|
// Vérifier s'il est compromis
|
||||||
|
$check = self::checkPasswordCompromised($password);
|
||||||
|
|
||||||
|
if (!$check['compromised']) {
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogService::log('Mot de passe généré était compromis, nouvelle tentative', [
|
||||||
|
'level' => 'info',
|
||||||
|
'attempt' => $attempt + 1,
|
||||||
|
'occurrences' => $check['occurrences']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogService::log('Impossible de générer un mot de passe non compromis', [
|
||||||
|
'level' => 'error',
|
||||||
|
'attempts' => $maxAttempts
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un mot de passe aléatoire
|
||||||
|
*
|
||||||
|
* @param int $length Longueur du mot de passe
|
||||||
|
* @return string Le mot de passe généré
|
||||||
|
*/
|
||||||
|
private static function generateRandomPassword(int $length): string {
|
||||||
|
// Caractères autorisés (sans ambiguïté visuelle)
|
||||||
|
$lowercase = 'abcdefghijkmnopqrstuvwxyz'; // sans l
|
||||||
|
$uppercase = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // sans I, O
|
||||||
|
$numbers = '23456789'; // sans 0, 1
|
||||||
|
$special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||||
|
|
||||||
|
$password = '';
|
||||||
|
|
||||||
|
// Garantir au moins un caractère de chaque type
|
||||||
|
$password .= $lowercase[random_int(0, strlen($lowercase) - 1)];
|
||||||
|
$password .= $uppercase[random_int(0, strlen($uppercase) - 1)];
|
||||||
|
$password .= $numbers[random_int(0, strlen($numbers) - 1)];
|
||||||
|
$password .= $special[random_int(0, strlen($special) - 1)];
|
||||||
|
|
||||||
|
// Compléter avec des caractères aléatoires
|
||||||
|
$allChars = $lowercase . $uppercase . $numbers . $special;
|
||||||
|
for ($i = strlen($password); $i < $length; $i++) {
|
||||||
|
$password .= $allChars[random_int(0, strlen($allChars) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mélanger les caractères
|
||||||
|
$passwordArray = str_split($password);
|
||||||
|
shuffle($passwordArray);
|
||||||
|
|
||||||
|
return implode('', $passwordArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le mot de passe contient des motifs répétitifs simples
|
||||||
|
*
|
||||||
|
* @param string $password Le mot de passe à vérifier
|
||||||
|
* @return bool True si des motifs répétitifs sont détectés
|
||||||
|
*/
|
||||||
|
private static function hasSimplePattern(string $password): bool {
|
||||||
|
$lowPassword = strtolower($password);
|
||||||
|
|
||||||
|
// Vérifier les caractères répétés (aaa, 111, etc.)
|
||||||
|
if (preg_match('/(.)\1{2,}/', $lowPassword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les motifs répétés (ababab, 121212, etc.)
|
||||||
|
if (preg_match('/(.{2,})\1{2,}/', $lowPassword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le mot de passe contient des séquences communes
|
||||||
|
*
|
||||||
|
* @param string $password Le mot de passe à vérifier
|
||||||
|
* @return bool True si des séquences communes sont détectées
|
||||||
|
*/
|
||||||
|
private static function hasCommonSequence(string $password): bool {
|
||||||
|
$lowPassword = strtolower($password);
|
||||||
|
|
||||||
|
$commonSequences = [
|
||||||
|
'123', '234', '345', '456', '567', '678', '789',
|
||||||
|
'abc', 'bcd', 'cde', 'def', 'efg', 'fgh',
|
||||||
|
'qwerty', 'azerty', 'qwertz',
|
||||||
|
'password', 'motdepasse', 'admin', 'user'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($commonSequences as $sequence) {
|
||||||
|
if (stripos($lowPassword, $sequence) !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estime la force d'un mot de passe selon l'approche NIST
|
||||||
|
* NIST privilégie la longueur sur la complexité
|
||||||
|
*
|
||||||
|
* @param string $password Le mot de passe à évaluer
|
||||||
|
* @return array ['score' => int (0-100), 'strength' => string, 'feedback' => array]
|
||||||
|
*/
|
||||||
|
public static function estimatePasswordStrength(string $password): array {
|
||||||
|
$score = 0;
|
||||||
|
$feedback = [];
|
||||||
|
|
||||||
|
// Longueur (NIST : facteur le plus important)
|
||||||
|
$length = mb_strlen($password, 'UTF-8');
|
||||||
|
if ($length >= 8) $score += 20; // Minimum requis
|
||||||
|
if ($length >= 12) $score += 20; // Bon
|
||||||
|
if ($length >= 16) $score += 20; // Très bon
|
||||||
|
if ($length >= 20) $score += 15; // Excellent
|
||||||
|
if ($length >= 30) $score += 10; // Exceptionnel
|
||||||
|
|
||||||
|
// Diversité des caractères (bonus, pas obligatoire selon NIST)
|
||||||
|
$hasLower = preg_match('/[a-z]/u', $password);
|
||||||
|
$hasUpper = preg_match('/[A-Z]/u', $password);
|
||||||
|
$hasDigit = preg_match('/[0-9]/u', $password);
|
||||||
|
$hasSpecial = preg_match('/[^a-zA-Z0-9]/u', $password);
|
||||||
|
|
||||||
|
$diversity = ($hasLower ? 1 : 0) + ($hasUpper ? 1 : 0) + ($hasDigit ? 1 : 0) + ($hasSpecial ? 1 : 0);
|
||||||
|
|
||||||
|
// Bonus pour la diversité (mais pas de pénalité si absent)
|
||||||
|
if ($diversity >= 4) {
|
||||||
|
$score += 15;
|
||||||
|
} elseif ($diversity >= 3) {
|
||||||
|
$score += 10;
|
||||||
|
} elseif ($diversity >= 2) {
|
||||||
|
$score += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestions constructives (pas de pénalités selon NIST)
|
||||||
|
if ($length < 12) {
|
||||||
|
$feedback[] = 'Suggestion : Un mot de passe plus long est plus sécurisé';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diversity < 2 && $length < 16) {
|
||||||
|
$feedback[] = 'Suggestion : Variez les types de caractères ou augmentez la longueur';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pénalités légères pour les mauvaises pratiques évidentes
|
||||||
|
if (self::hasSimplePattern($password)) {
|
||||||
|
$score = max(0, $score - 10);
|
||||||
|
$feedback[] = 'Attention : Motifs répétitifs détectés';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::hasCommonSequence($password)) {
|
||||||
|
$score = max(0, $score - 10);
|
||||||
|
$feedback[] = 'Attention : Séquences communes détectées';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification compromission (critique selon NIST)
|
||||||
|
$compromisedCheck = self::checkPasswordCompromised($password);
|
||||||
|
if ($compromisedCheck['compromised']) {
|
||||||
|
$score = min($score, 10); // Score très bas si compromis
|
||||||
|
$feedback[] = sprintf(
|
||||||
|
'CRITIQUE : Mot de passe trouvé %s fois dans des fuites de données',
|
||||||
|
number_format($compromisedCheck['occurrences'], 0, ',', ' ')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déterminer la force basée principalement sur la longueur (approche NIST)
|
||||||
|
$strength = 'Très faible';
|
||||||
|
if ($compromisedCheck['compromised']) {
|
||||||
|
$strength = 'Compromis';
|
||||||
|
} elseif ($length >= 20) {
|
||||||
|
$strength = 'Très fort';
|
||||||
|
} elseif ($length >= 16) {
|
||||||
|
$strength = 'Fort';
|
||||||
|
} elseif ($length >= 12) {
|
||||||
|
$strength = 'Bon';
|
||||||
|
} elseif ($length >= 8) {
|
||||||
|
$strength = 'Acceptable';
|
||||||
|
} else {
|
||||||
|
$strength = 'Trop court';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'score' => max(0, min(100, $score)),
|
||||||
|
'strength' => $strength,
|
||||||
|
'feedback' => $feedback,
|
||||||
|
'length' => $length,
|
||||||
|
'diversity' => $diversity
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
190
api/src/Services/ReceiptPDFGenerator.php
Normal file
190
api/src/Services/ReceiptPDFGenerator.php
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
|
||||||
|
use FPDF;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Générateur de reçus PDF avec FPDF
|
||||||
|
* Supporte les logos PNG/JPG
|
||||||
|
*/
|
||||||
|
class ReceiptPDFGenerator extends FPDF {
|
||||||
|
|
||||||
|
private const DEFAULT_LOGO_PATH = __DIR__ . '/../../docs/_logo_recu.png';
|
||||||
|
private const LOGO_WIDTH = 40; // Largeur du logo en mm
|
||||||
|
private const LOGO_HEIGHT = 40; // Hauteur du logo en mm
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un reçu fiscal PDF
|
||||||
|
*/
|
||||||
|
public function generateReceipt(array $data, ?string $logoPath = null): string {
|
||||||
|
$this->AddPage();
|
||||||
|
$this->SetFont('Arial', '', 12);
|
||||||
|
|
||||||
|
// Déterminer quel logo utiliser
|
||||||
|
$logoToUse = null;
|
||||||
|
if ($logoPath && file_exists($logoPath)) {
|
||||||
|
$logoToUse = $logoPath;
|
||||||
|
} elseif (file_exists(self::DEFAULT_LOGO_PATH)) {
|
||||||
|
$logoToUse = self::DEFAULT_LOGO_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter le logo (PNG ou JPG)
|
||||||
|
if ($logoToUse) {
|
||||||
|
try {
|
||||||
|
// Déterminer le type d'image
|
||||||
|
$imageInfo = getimagesize($logoToUse);
|
||||||
|
if ($imageInfo !== false) {
|
||||||
|
$type = '';
|
||||||
|
switch ($imageInfo[2]) {
|
||||||
|
case IMAGETYPE_JPEG:
|
||||||
|
$type = 'JPG';
|
||||||
|
break;
|
||||||
|
case IMAGETYPE_PNG:
|
||||||
|
$type = 'PNG';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type) {
|
||||||
|
// Position du logo : x=10, y=10, largeur=40mm, hauteur=40mm
|
||||||
|
$this->Image($logoToUse, 10, 10, self::LOGO_WIDTH, self::LOGO_HEIGHT, $type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Si erreur avec le logo, continuer sans
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// En-tête à droite du logo
|
||||||
|
$this->SetXY(60, 20);
|
||||||
|
$this->SetFont('Arial', 'B', 14);
|
||||||
|
$this->Cell(130, 6, $this->cleanText(strtoupper($data['entite_name'] ?? '')), 0, 1, 'C');
|
||||||
|
|
||||||
|
if (!empty($data['entite_city'])) {
|
||||||
|
$this->SetX(60);
|
||||||
|
$this->SetFont('Arial', '', 11);
|
||||||
|
$this->Cell(130, 5, $this->cleanText(strtoupper($data['entite_city'])), 0, 1, 'C');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['entite_address'])) {
|
||||||
|
$this->SetX(60);
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell(130, 5, $this->cleanText($data['entite_address']), 0, 1, 'C');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Titre du reçu
|
||||||
|
$this->SetY(65);
|
||||||
|
$this->SetFont('Arial', 'B', 16);
|
||||||
|
$this->Cell(0, 10, $this->cleanText('REÇU DE DON'), 0, 1, 'C');
|
||||||
|
|
||||||
|
$this->SetFont('Arial', 'B', 14);
|
||||||
|
$this->Cell(0, 8, $this->cleanText('N° ' . ($data['receipt_number'] ?? '')), 0, 1, 'C');
|
||||||
|
|
||||||
|
// Ligne de séparation
|
||||||
|
$this->Ln(5);
|
||||||
|
$this->Line(20, $this->GetY(), 190, $this->GetY());
|
||||||
|
$this->Ln(8);
|
||||||
|
|
||||||
|
// Informations du donateur
|
||||||
|
$this->SetFont('Arial', 'B', 12);
|
||||||
|
$this->Cell(0, 6, $this->cleanText('DONATEUR :'), 0, 1, 'L');
|
||||||
|
|
||||||
|
$this->SetFont('Arial', '', 11);
|
||||||
|
$this->Cell(0, 6, $this->cleanText($data['donor_name'] ?? ''), 0, 1, 'L');
|
||||||
|
|
||||||
|
if (!empty($data['donor_address'])) {
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell(0, 5, $this->cleanText($data['donor_address']), 0, 1, 'L');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->Ln(8);
|
||||||
|
|
||||||
|
// Cadre pour le montant
|
||||||
|
$this->SetFillColor(240, 240, 240);
|
||||||
|
$this->Rect(20, $this->GetY(), 170, 25, 'F');
|
||||||
|
|
||||||
|
// Montant en gros et centré
|
||||||
|
$this->Ln(5);
|
||||||
|
$this->SetFont('Arial', 'B', 18);
|
||||||
|
$this->Cell(0, 8, $this->cleanText($data['amount'] ?? '0') . $this->cleanText(' euros'), 0, 1, 'C');
|
||||||
|
|
||||||
|
// Date centrée
|
||||||
|
$this->SetFont('Arial', '', 12);
|
||||||
|
$this->Cell(0, 6, $this->cleanText('Don du ' . ($data['donation_date'] ?? date('d/m/Y'))), 0, 1, 'C');
|
||||||
|
|
||||||
|
$this->Ln(10);
|
||||||
|
|
||||||
|
if (!empty($data['payment_method'])) {
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell(0, 5, $this->cleanText('Mode de règlement : ') . $this->cleanText($data['payment_method']), 0, 1, 'L');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['operation_name'])) {
|
||||||
|
$this->SetFont('Arial', 'I', 10);
|
||||||
|
$this->Cell(0, 5, $this->cleanText('Campagne : ') . $this->cleanText($data['operation_name']), 0, 1, 'L');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mention de remerciement
|
||||||
|
$this->Ln(15);
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->MultiCell(0, 5, $this->cleanText(
|
||||||
|
"Nous vous remercions pour votre généreux soutien à notre amicale.\n" .
|
||||||
|
"Votre don contribue au financement de nos activités et équipements."
|
||||||
|
), 0, 'C');
|
||||||
|
|
||||||
|
// Signature
|
||||||
|
$this->SetY(-60);
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell(0, 5, $this->cleanText('Fait à ' . ($data['entite_city'] ?? '') . ', le ' . ($data['signature_date'] ?? date('d/m/Y'))), 0, 1, 'R');
|
||||||
|
$this->Ln(5);
|
||||||
|
$this->Cell(0, 5, $this->cleanText('Le Président'), 0, 1, 'R');
|
||||||
|
$this->Ln(15);
|
||||||
|
$this->Cell(0, 5, $this->cleanText('(Signature et cachet)'), 0, 1, 'R');
|
||||||
|
|
||||||
|
// Retourner le PDF en string
|
||||||
|
return $this->Output('S');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie le texte pour le PDF (supprime ou remplace les caractères problématiques)
|
||||||
|
*/
|
||||||
|
private function cleanText(string $text): string {
|
||||||
|
// Vérifier que le texte n'est pas vide
|
||||||
|
if (empty($text)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remplacer d'abord les caractères problématiques avant la conversion
|
||||||
|
$replacements = [
|
||||||
|
'€' => 'EUR',
|
||||||
|
'—' => '-',
|
||||||
|
'–' => '-',
|
||||||
|
'"' => '"',
|
||||||
|
'"' => '"',
|
||||||
|
"'" => "'",
|
||||||
|
"'" => "'",
|
||||||
|
'…' => '...'
|
||||||
|
];
|
||||||
|
|
||||||
|
$text = str_replace(array_keys($replacements), array_values($replacements), $text);
|
||||||
|
|
||||||
|
// Tentative de conversion UTF-8 vers ISO-8859-1 pour FPDF
|
||||||
|
$converted = @iconv('UTF-8', 'ISO-8859-1//TRANSLIT//IGNORE', $text);
|
||||||
|
|
||||||
|
// Si la conversion échoue, utiliser utf8_decode en fallback
|
||||||
|
if ($converted === false) {
|
||||||
|
$converted = @utf8_decode($text);
|
||||||
|
|
||||||
|
// Si utf8_decode échoue aussi, supprimer les caractères non-ASCII
|
||||||
|
if ($converted === false) {
|
||||||
|
$converted = preg_replace('/[^\x20-\x7E]/', '?', $text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $converted;
|
||||||
|
}
|
||||||
|
}
|
||||||
457
api/src/Services/ReceiptService.php
Normal file
457
api/src/Services/ReceiptService.php
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/LogService.php';
|
||||||
|
require_once __DIR__ . '/ApiService.php';
|
||||||
|
require_once __DIR__ . '/FileService.php';
|
||||||
|
require_once __DIR__ . '/ReceiptPDFGenerator.php';
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use Database;
|
||||||
|
use LogService;
|
||||||
|
use ApiService;
|
||||||
|
use FileService;
|
||||||
|
use Exception;
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des reçus pour les passages de type don (fk_type=1)
|
||||||
|
* Optimisé pour générer des PDF très légers (< 20KB)
|
||||||
|
*/
|
||||||
|
class ReceiptService {
|
||||||
|
private PDO $db;
|
||||||
|
private FileService $fileService;
|
||||||
|
private const DEFAULT_LOGO_PATH = __DIR__ . '/../../docs/_logo_recu.png';
|
||||||
|
private const LOGO_WIDTH = 40; // Largeur du logo en mm (80 est trop grand pour un A4)
|
||||||
|
private const LOGO_HEIGHT = 40; // Hauteur du logo en mm
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->db = Database::getInstance();
|
||||||
|
$this->fileService = new FileService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un reçu pour un passage de type don avec email valide
|
||||||
|
*
|
||||||
|
* @param int $passageId ID du passage
|
||||||
|
* @return bool True si le reçu a été généré avec succès
|
||||||
|
*/
|
||||||
|
public function generateReceiptForPassage(int $passageId): bool {
|
||||||
|
try {
|
||||||
|
// Récupérer les données du passage
|
||||||
|
$passageData = $this->getPassageData($passageId);
|
||||||
|
if (!$passageData) {
|
||||||
|
LogService::log('Passage non trouvé pour génération de reçu', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que c'est un don effectué (fk_type = 1) avec email valide
|
||||||
|
if ((int)$passageData['fk_type'] !== 1) {
|
||||||
|
return false; // Pas un don, pas de reçu
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déchiffrer et vérifier l'email
|
||||||
|
$email = '';
|
||||||
|
if (!empty($passageData['encrypted_email'])) {
|
||||||
|
$email = ApiService::decryptSearchableData($passageData['encrypted_email']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
LogService::log('Email invalide ou manquant pour le reçu', [
|
||||||
|
'level' => 'info',
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les données de l'opération
|
||||||
|
$operationData = $this->getOperationData($passageData['fk_operation']);
|
||||||
|
if (!$operationData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les données de l'entité
|
||||||
|
$entiteData = $this->getEntiteData($operationData['fk_entite']);
|
||||||
|
if (!$entiteData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le logo de l'entité
|
||||||
|
$logoPath = $this->getEntiteLogo($operationData['fk_entite']);
|
||||||
|
|
||||||
|
// Préparer les données pour la génération du PDF
|
||||||
|
$receiptData = $this->prepareReceiptData($passageData, $operationData, $entiteData, $email);
|
||||||
|
|
||||||
|
// Générer le PDF optimisé
|
||||||
|
$pdfContent = $this->generateOptimizedPDF($receiptData, $logoPath);
|
||||||
|
|
||||||
|
// Créer le répertoire de stockage
|
||||||
|
$uploadPath = "/{$operationData['fk_entite']}/recus/{$operationData['id']}";
|
||||||
|
$fullPath = $this->fileService->createDirectory($operationData['fk_entite'], $uploadPath);
|
||||||
|
|
||||||
|
// Nom du fichier
|
||||||
|
$fileName = 'recu_' . $passageId . '.pdf';
|
||||||
|
$filePath = $fullPath . '/' . $fileName;
|
||||||
|
|
||||||
|
// Sauvegarder le fichier
|
||||||
|
if (file_put_contents($filePath, $pdfContent) === false) {
|
||||||
|
throw new Exception('Impossible de sauvegarder le fichier PDF');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appliquer les permissions
|
||||||
|
$this->fileService->setFilePermissions($filePath);
|
||||||
|
|
||||||
|
// Enregistrer dans la table medias
|
||||||
|
$mediaId = $this->saveToMedias(
|
||||||
|
$operationData['fk_entite'],
|
||||||
|
$operationData['id'],
|
||||||
|
$passageId,
|
||||||
|
$fileName,
|
||||||
|
$filePath,
|
||||||
|
strlen($pdfContent)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mettre à jour le passage avec les infos du reçu
|
||||||
|
$this->updatePassageReceipt($passageId, $fileName);
|
||||||
|
|
||||||
|
// Ajouter à la queue d'email
|
||||||
|
$this->queueReceiptEmail($passageId, $email, $receiptData, $pdfContent);
|
||||||
|
|
||||||
|
LogService::log('Reçu généré avec succès', [
|
||||||
|
'level' => 'info',
|
||||||
|
'passageId' => $passageId,
|
||||||
|
'mediaId' => $mediaId,
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'fileSize' => strlen($pdfContent)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la génération du reçu', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un PDF optimisé avec logo et mise en page épurée
|
||||||
|
*/
|
||||||
|
private function generateOptimizedPDF(array $data, ?string $logoPath): string {
|
||||||
|
$pdf = new ReceiptPDFGenerator();
|
||||||
|
return $pdf->generateReceipt($data, $logoPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les données du passage
|
||||||
|
*/
|
||||||
|
private function getPassageData(int $passageId): ?array {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT p.*,
|
||||||
|
u.encrypted_name as user_encrypted_name,
|
||||||
|
u.encrypted_email as user_encrypted_email,
|
||||||
|
u.encrypted_phone as user_encrypted_phone
|
||||||
|
FROM ope_pass p
|
||||||
|
LEFT JOIN users u ON p.fk_user = u.id
|
||||||
|
WHERE p.id = ? AND p.chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$passageId]);
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les données de l'opération
|
||||||
|
*/
|
||||||
|
private function getOperationData(int $operationId): ?array {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT * FROM operations
|
||||||
|
WHERE id = ? AND chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$operationId]);
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les données de l'entité
|
||||||
|
*/
|
||||||
|
private function getEntiteData(int $entiteId): ?array {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT * FROM entites
|
||||||
|
WHERE id = ? AND chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$entiteId]);
|
||||||
|
$entite = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($entite) {
|
||||||
|
// Déchiffrer les données
|
||||||
|
if (!empty($entite['encrypted_name'])) {
|
||||||
|
$entite['name'] = ApiService::decryptData($entite['encrypted_name']);
|
||||||
|
}
|
||||||
|
if (!empty($entite['encrypted_email'])) {
|
||||||
|
$entite['email'] = ApiService::decryptSearchableData($entite['encrypted_email']);
|
||||||
|
}
|
||||||
|
if (!empty($entite['encrypted_phone'])) {
|
||||||
|
$entite['phone'] = ApiService::decryptData($entite['encrypted_phone']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entite ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le chemin du logo de l'entité
|
||||||
|
*/
|
||||||
|
private function getEntiteLogo(int $entiteId): ?string {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT file_path 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 && !empty($logo['file_path']) && file_exists($logo['file_path'])) {
|
||||||
|
return $logo['file_path'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utiliser le logo par défaut si disponible
|
||||||
|
if (file_exists(self::DEFAULT_LOGO_PATH)) {
|
||||||
|
return self::DEFAULT_LOGO_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prépare les données pour le reçu
|
||||||
|
*/
|
||||||
|
private function prepareReceiptData(array $passage, array $operation, array $entite, string $email): array {
|
||||||
|
// Déchiffrer le nom du donateur
|
||||||
|
$donorName = '';
|
||||||
|
if (!empty($passage['encrypted_name'])) {
|
||||||
|
$donorName = ApiService::decryptData($passage['encrypted_name']);
|
||||||
|
} elseif (!empty($passage['user_encrypted_name'])) {
|
||||||
|
$donorName = ApiService::decryptData($passage['user_encrypted_name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construire l'adresse du donateur
|
||||||
|
$donorAddress = [];
|
||||||
|
if (!empty($passage['numero'])) $donorAddress[] = $passage['numero'];
|
||||||
|
if (!empty($passage['rue'])) $donorAddress[] = $passage['rue'];
|
||||||
|
if (!empty($passage['rue_bis'])) $donorAddress[] = $passage['rue_bis'];
|
||||||
|
if (!empty($passage['ville'])) $donorAddress[] = $passage['ville'];
|
||||||
|
|
||||||
|
// Date du don
|
||||||
|
$donationDate = '';
|
||||||
|
if (!empty($passage['passed_at'])) {
|
||||||
|
$donationDate = date('d/m/Y', strtotime($passage['passed_at']));
|
||||||
|
} elseif (!empty($passage['created_at'])) {
|
||||||
|
$donationDate = date('d/m/Y', strtotime($passage['created_at']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode de règlement
|
||||||
|
$paymentMethod = $this->getPaymentMethodLabel((int)($passage['fk_type_reglement'] ?? 1));
|
||||||
|
|
||||||
|
// Adresse de l'entité
|
||||||
|
$entiteAddress = [];
|
||||||
|
if (!empty($entite['adresse1'])) $entiteAddress[] = $entite['adresse1'];
|
||||||
|
if (!empty($entite['adresse2'])) $entiteAddress[] = $entite['adresse2'];
|
||||||
|
if (!empty($entite['code_postal']) || !empty($entite['ville'])) {
|
||||||
|
$entiteAddress[] = trim($entite['code_postal'] . ' ' . $entite['ville']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'receipt_number' => $passage['id'],
|
||||||
|
'entite_name' => $entite['name'] ?? 'Amicale des Sapeurs-Pompiers',
|
||||||
|
'entite_address' => implode(' ', $entiteAddress),
|
||||||
|
'entite_city' => $entite['ville'] ?? '',
|
||||||
|
'entite_email' => $entite['email'] ?? '',
|
||||||
|
'entite_phone' => $entite['phone'] ?? '',
|
||||||
|
'donor_name' => $donorName,
|
||||||
|
'donor_address' => implode(' ', $donorAddress),
|
||||||
|
'donor_email' => $email,
|
||||||
|
'donation_date' => $donationDate,
|
||||||
|
'amount' => number_format((float)($passage['montant'] ?? 0), 2, ',', ' '),
|
||||||
|
'payment_method' => $paymentMethod,
|
||||||
|
'operation_name' => $operation['libelle'] ?? '',
|
||||||
|
'signature_date' => date('d/m/Y')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le libellé du mode de règlement
|
||||||
|
*/
|
||||||
|
private function getPaymentMethodLabel(int $typeReglement): string {
|
||||||
|
$stmt = $this->db->prepare('SELECT libelle FROM x_types_reglements WHERE id = ?');
|
||||||
|
$stmt->execute([$typeReglement]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $result ? $result['libelle'] : 'Espèces';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre le fichier dans la table medias
|
||||||
|
*/
|
||||||
|
private function saveToMedias(int $entiteId, int $operationId, int $passageId, string $fileName, string $filePath, int $fileSize): int {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO medias (
|
||||||
|
support, support_id, fichier, file_type, file_category,
|
||||||
|
file_size, mime_type, original_name, fk_entite, fk_operation,
|
||||||
|
file_path, description, created_at, fk_user_creat
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'passage', // support
|
||||||
|
$passageId, // support_id
|
||||||
|
$fileName, // fichier
|
||||||
|
'pdf', // file_type
|
||||||
|
'recu', // file_category
|
||||||
|
$fileSize, // file_size
|
||||||
|
'application/pdf', // mime_type
|
||||||
|
$fileName, // original_name
|
||||||
|
$entiteId, // fk_entite
|
||||||
|
$operationId, // fk_operation
|
||||||
|
$filePath, // file_path
|
||||||
|
'Reçu de don', // description
|
||||||
|
0 // fk_user_creat (système)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int)$this->db->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le passage avec les informations du reçu
|
||||||
|
*/
|
||||||
|
private function updatePassageReceipt(int $passageId, string $fileName): void {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
UPDATE ope_pass
|
||||||
|
SET nom_recu = ?, date_creat_recu = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$fileName, $passageId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute le reçu à la queue d'email
|
||||||
|
*/
|
||||||
|
private function queueReceiptEmail(int $passageId, string $email, array $receiptData, string $pdfContent): void {
|
||||||
|
// Préparer le sujet
|
||||||
|
$subject = "Votre reçu de don N°" . $receiptData['receipt_number'];
|
||||||
|
|
||||||
|
// Préparer le corps de l'email
|
||||||
|
$body = $this->generateEmailBody($receiptData);
|
||||||
|
|
||||||
|
// Préparer les headers avec pièce jointe
|
||||||
|
$boundary = md5((string)time());
|
||||||
|
$headers = "MIME-Version: 1.0\r\n";
|
||||||
|
$headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n";
|
||||||
|
|
||||||
|
// Corps complet avec pièce jointe
|
||||||
|
$fullBody = "--$boundary\r\n";
|
||||||
|
$fullBody .= "Content-Type: text/html; charset=UTF-8\r\n";
|
||||||
|
$fullBody .= "Content-Transfer-Encoding: 7bit\r\n\r\n";
|
||||||
|
$fullBody .= $body . "\r\n\r\n";
|
||||||
|
|
||||||
|
// Pièce jointe PDF
|
||||||
|
$fullBody .= "--$boundary\r\n";
|
||||||
|
$fullBody .= "Content-Type: application/pdf; name=\"recu_" . $receiptData['receipt_number'] . ".pdf\"\r\n";
|
||||||
|
$fullBody .= "Content-Transfer-Encoding: base64\r\n";
|
||||||
|
$fullBody .= "Content-Disposition: attachment; filename=\"recu_" . $receiptData['receipt_number'] . ".pdf\"\r\n\r\n";
|
||||||
|
$fullBody .= chunk_split(base64_encode($pdfContent)) . "\r\n";
|
||||||
|
$fullBody .= "--$boundary--";
|
||||||
|
|
||||||
|
// Insérer dans la queue
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO email_queue (
|
||||||
|
fk_pass, to_email, subject, body, headers, created_at, status
|
||||||
|
) VALUES (?, ?, ?, ?, ?, NOW(), ?)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$passageId,
|
||||||
|
$email,
|
||||||
|
$subject,
|
||||||
|
$fullBody,
|
||||||
|
$headers,
|
||||||
|
'pending'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le corps HTML de l'email
|
||||||
|
*/
|
||||||
|
private function generateEmailBody(array $data): string {
|
||||||
|
// Convertir toutes les valeurs en string pour htmlspecialchars
|
||||||
|
$safeData = array_map(function($value) {
|
||||||
|
return is_string($value) ? $value : (string)$value;
|
||||||
|
}, $data);
|
||||||
|
|
||||||
|
$html = '<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background-color: #f4f4f4; padding: 20px; text-align: center; }
|
||||||
|
.content { padding: 20px; }
|
||||||
|
.footer { background-color: #f4f4f4; padding: 10px; text-align: center; font-size: 12px; }
|
||||||
|
.amount { font-size: 24px; font-weight: bold; color: #2c5aa0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h2>' . htmlspecialchars($safeData['entite_name']) . '</h2>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Bonjour ' . htmlspecialchars($safeData['donor_name']) . ',</p>
|
||||||
|
|
||||||
|
<p>Nous vous remercions chaleureusement pour votre don de <span class="amount">' .
|
||||||
|
htmlspecialchars($safeData['amount']) . ' €</span> effectué le ' .
|
||||||
|
htmlspecialchars($safeData['donation_date']) . '.</p>
|
||||||
|
|
||||||
|
<p>Vous trouverez ci-joint votre reçu fiscal N°' . htmlspecialchars($safeData['receipt_number']) .
|
||||||
|
' qui vous permettra de bénéficier d\'une réduction d\'impôt égale à 66% du montant de votre don.</p>
|
||||||
|
|
||||||
|
<p>Votre soutien est précieux pour nous permettre de poursuivre nos actions.</p>
|
||||||
|
|
||||||
|
<p>Cordialement,<br>
|
||||||
|
L\'équipe de ' . htmlspecialchars($safeData['entite_name']) . '</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Conservez ce reçu pour votre déclaration fiscale</p>
|
||||||
|
<p>' . htmlspecialchars($safeData['entite_name']) . '<br>
|
||||||
|
' . htmlspecialchars($safeData['entite_address']) . '<br>
|
||||||
|
' . htmlspecialchars($safeData['entite_email']) . '</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour la date d'envoi du reçu
|
||||||
|
*/
|
||||||
|
public function markReceiptAsSent(int $passageId): void {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
UPDATE ope_pass
|
||||||
|
SET date_sent_recu = NOW(), chk_email_sent = 1
|
||||||
|
WHERE id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$passageId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
553
api/src/Services/Security/AlertService.php
Normal file
553
api/src/Services/Security/AlertService.php
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Security;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../Services/ApiService.php';
|
||||||
|
require_once __DIR__ . '/EmailThrottler.php';
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use Database;
|
||||||
|
use ApiService;
|
||||||
|
use AppConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service central de gestion des alertes de sécurité et monitoring
|
||||||
|
*/
|
||||||
|
class AlertService {
|
||||||
|
|
||||||
|
// Niveaux d'alerte avec leur configuration
|
||||||
|
const ALERT_LEVELS = [
|
||||||
|
'INFO' => ['email' => false, 'log' => true, 'throttle' => 0],
|
||||||
|
'WARNING' => ['email' => true, 'log' => true, 'throttle' => 3600], // 1h
|
||||||
|
'ERROR' => ['email' => true, 'log' => true, 'throttle' => 900], // 15min
|
||||||
|
'CRITICAL' => ['email' => true, 'log' => true, 'throttle' => 300], // 5min
|
||||||
|
'SECURITY' => ['email' => true, 'log' => true, 'throttle' => 0, 'priority' => true]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Types d'alertes avec leur niveau par défaut
|
||||||
|
const ALERT_TYPES = [
|
||||||
|
'BRUTE_FORCE' => 'SECURITY',
|
||||||
|
'UNAUTHORIZED_ACCESS' => 'CRITICAL',
|
||||||
|
'SQL_INJECTION' => 'SECURITY',
|
||||||
|
'SQL_ERROR' => 'ERROR',
|
||||||
|
'DB_CONNECTION' => 'CRITICAL',
|
||||||
|
'DB_DEADLOCK' => 'ERROR',
|
||||||
|
'PERFORMANCE_SLOW' => 'WARNING',
|
||||||
|
'PERFORMANCE_CRITICAL' => 'ERROR',
|
||||||
|
'HTTP_500' => 'ERROR',
|
||||||
|
'HTTP_404_PATTERN' => 'WARNING',
|
||||||
|
'SUSPICIOUS_PATTERN' => 'WARNING',
|
||||||
|
'MEMORY_HIGH' => 'WARNING',
|
||||||
|
'DISK_SPACE' => 'CRITICAL'
|
||||||
|
];
|
||||||
|
|
||||||
|
private static ?PDO $db = null;
|
||||||
|
private static ?EmailThrottler $throttler = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclencher une alerte
|
||||||
|
*/
|
||||||
|
public static function trigger(string $type, array $data = [], ?string $level = null): void {
|
||||||
|
try {
|
||||||
|
// Déterminer le niveau si non fourni
|
||||||
|
if ($level === null) {
|
||||||
|
$level = self::ALERT_TYPES[$type] ?? 'WARNING';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrichir le contexte
|
||||||
|
$context = self::enrichContext($type, $level, $data);
|
||||||
|
|
||||||
|
// Enregistrer en base de données
|
||||||
|
$alertId = self::saveAlert($type, $level, $context);
|
||||||
|
|
||||||
|
// Logger
|
||||||
|
self::logAlert($type, $level, $context);
|
||||||
|
|
||||||
|
// Envoyer email si nécessaire et non throttled
|
||||||
|
if (self::shouldSendEmail($type, $level)) {
|
||||||
|
self::sendAlertEmail($type, $level, $context, $alertId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions automatiques selon le type
|
||||||
|
self::executeAutomaticActions($type, $context);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("AlertService Error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enrichir le contexte avec des informations système
|
||||||
|
*/
|
||||||
|
private static function enrichContext(string $type, string $level, array $data): array {
|
||||||
|
$context = $data;
|
||||||
|
|
||||||
|
// Ajouter les informations de base
|
||||||
|
$context['timestamp'] = date('Y-m-d H:i:s');
|
||||||
|
$context['environment'] = AppConfig::getInstance()->getEnvironment();
|
||||||
|
$context['server'] = $_SERVER['SERVER_NAME'] ?? 'unknown';
|
||||||
|
|
||||||
|
// Ajouter les informations de requête
|
||||||
|
if (isset($_SERVER['REQUEST_URI'])) {
|
||||||
|
$context['request'] = [
|
||||||
|
'uri' => $_SERVER['REQUEST_URI'],
|
||||||
|
'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
|
||||||
|
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||||
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les informations de session
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
$context['session'] = [
|
||||||
|
'user_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'entity_id' => $_SESSION['entity_id'] ?? null,
|
||||||
|
'session_id' => substr(session_id(), 0, 8) . '...' // Tronquer pour sécurité
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les métriques système
|
||||||
|
$context['system'] = [
|
||||||
|
'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
|
||||||
|
'memory_peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
|
||||||
|
'load_average' => sys_getloadavg()
|
||||||
|
];
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarder l'alerte en base de données
|
||||||
|
*/
|
||||||
|
private static function saveAlert(string $type, string $level, array $context): int {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
// Vérifier si une alerte similaire existe déjà (dans les 5 dernières minutes)
|
||||||
|
$checkStmt = $db->prepare('
|
||||||
|
SELECT id, occurrences
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE alert_type = :type
|
||||||
|
AND alert_level = :level
|
||||||
|
AND ip_address = :ip
|
||||||
|
AND endpoint = :endpoint
|
||||||
|
AND resolved = 0
|
||||||
|
AND last_seen >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
|
||||||
|
$ip = $context['request']['ip'] ?? null;
|
||||||
|
$endpoint = $context['request']['uri'] ?? null;
|
||||||
|
|
||||||
|
$checkStmt->execute([
|
||||||
|
'type' => $type,
|
||||||
|
'level' => $level,
|
||||||
|
'ip' => $ip,
|
||||||
|
'endpoint' => $endpoint
|
||||||
|
]);
|
||||||
|
|
||||||
|
$existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
// Mettre à jour l'alerte existante
|
||||||
|
$updateStmt = $db->prepare('
|
||||||
|
UPDATE sec_alerts
|
||||||
|
SET occurrences = occurrences + 1,
|
||||||
|
last_seen = NOW(),
|
||||||
|
details = :details
|
||||||
|
WHERE id = :id
|
||||||
|
');
|
||||||
|
|
||||||
|
$updateStmt->execute([
|
||||||
|
'id' => $existing['id'],
|
||||||
|
'details' => json_encode($context)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int)$existing['id'];
|
||||||
|
} else {
|
||||||
|
// Créer une nouvelle alerte
|
||||||
|
$insertStmt = $db->prepare('
|
||||||
|
INSERT INTO sec_alerts (
|
||||||
|
alert_type, alert_level, ip_address, user_id, username,
|
||||||
|
endpoint, method, details, first_seen, last_seen
|
||||||
|
) VALUES (
|
||||||
|
:type, :level, :ip, :user_id, :username,
|
||||||
|
:endpoint, :method, :details, NOW(), NOW()
|
||||||
|
)
|
||||||
|
');
|
||||||
|
|
||||||
|
$insertStmt->execute([
|
||||||
|
'type' => $type,
|
||||||
|
'level' => $level,
|
||||||
|
'ip' => $ip,
|
||||||
|
'user_id' => $context['session']['user_id'] ?? null,
|
||||||
|
'username' => $context['username'] ?? null,
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'method' => $context['request']['method'] ?? null,
|
||||||
|
'details' => json_encode($context)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int)$db->lastInsertId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger l'alerte
|
||||||
|
*/
|
||||||
|
private static function logAlert(string $type, string $level, array $context): void {
|
||||||
|
$message = sprintf(
|
||||||
|
"[ALERT] %s - %s: %s",
|
||||||
|
$level,
|
||||||
|
$type,
|
||||||
|
$context['message'] ?? 'No message provided'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Utiliser LogService existant
|
||||||
|
if (class_exists('LogService')) {
|
||||||
|
\LogService::log($message, [
|
||||||
|
'level' => strtolower($level),
|
||||||
|
'alert_type' => $type,
|
||||||
|
'context' => $context
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
error_log($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier si un email doit être envoyé
|
||||||
|
*/
|
||||||
|
private static function shouldSendEmail(string $type, string $level): bool {
|
||||||
|
$config = self::ALERT_LEVELS[$level] ?? [];
|
||||||
|
|
||||||
|
if (!($config['email'] ?? false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier l'environnement
|
||||||
|
$env = AppConfig::getInstance()->getEnvironment();
|
||||||
|
if ($env === 'development' && $level !== 'SECURITY') {
|
||||||
|
return false; // Pas d'emails en dev sauf sécurité
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le throttling
|
||||||
|
$throttler = self::getThrottler();
|
||||||
|
$throttleTime = $config['throttle'] ?? 0;
|
||||||
|
|
||||||
|
if ($throttleTime > 0 && !$throttler->canSend($type, $throttleTime)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoyer l'email d'alerte
|
||||||
|
*/
|
||||||
|
private static function sendAlertEmail(string $type, string $level, array $context, int $alertId): void {
|
||||||
|
try {
|
||||||
|
$throttler = self::getThrottler();
|
||||||
|
|
||||||
|
// Préparer le contenu de l'email
|
||||||
|
$subject = sprintf(
|
||||||
|
"[%s] GeoSector %s - %s",
|
||||||
|
$level,
|
||||||
|
strtoupper($context['environment'] ?? 'PROD'),
|
||||||
|
str_replace('_', ' ', $type)
|
||||||
|
);
|
||||||
|
|
||||||
|
$body = self::formatEmailBody($type, $level, $context, $alertId);
|
||||||
|
|
||||||
|
// Destinataire principal
|
||||||
|
$to = 'support@unikoffice.com';
|
||||||
|
|
||||||
|
// CC pour les alertes critiques
|
||||||
|
$cc = [];
|
||||||
|
if (in_array($level, ['CRITICAL', 'SECURITY'])) {
|
||||||
|
$cc[] = 'admin@geosector.fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envoyer l'email
|
||||||
|
$emailSent = ApiService::sendEmail(
|
||||||
|
$to,
|
||||||
|
'GeoSector Security',
|
||||||
|
'security_alert',
|
||||||
|
[
|
||||||
|
'subject' => $subject,
|
||||||
|
'body' => $body,
|
||||||
|
'type' => $type,
|
||||||
|
'level' => $level,
|
||||||
|
'context' => $context
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($emailSent) {
|
||||||
|
// Marquer l'alerte comme email envoyé
|
||||||
|
$db = self::getDb();
|
||||||
|
$updateStmt = $db->prepare('
|
||||||
|
UPDATE sec_alerts
|
||||||
|
SET email_sent = 1, email_sent_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
');
|
||||||
|
$updateStmt->execute(['id' => $alertId]);
|
||||||
|
|
||||||
|
// Enregistrer l'envoi pour throttling
|
||||||
|
$throttler->recordSent($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to send alert email: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formater le corps de l'email
|
||||||
|
*/
|
||||||
|
private static function formatEmailBody(string $type, string $level, array $context, int $alertId): string {
|
||||||
|
$env = strtoupper($context['environment'] ?? 'PRODUCTION');
|
||||||
|
$timestamp = $context['timestamp'] ?? date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$body = "
|
||||||
|
========================================
|
||||||
|
ALERTE DE SÉCURITÉ - ACTION REQUISE
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Environnement: $env
|
||||||
|
Date/Heure: $timestamp
|
||||||
|
Type: $type
|
||||||
|
Niveau: $level
|
||||||
|
ID Alerte: #$alertId
|
||||||
|
|
||||||
|
RÉSUMÉ
|
||||||
|
------
|
||||||
|
" . ($context['message'] ?? 'Alerte détectée sans message spécifique') . "
|
||||||
|
|
||||||
|
DÉTAILS TECHNIQUES
|
||||||
|
------------------
|
||||||
|
";
|
||||||
|
|
||||||
|
if (isset($context['request'])) {
|
||||||
|
$body .= "
|
||||||
|
Requête:
|
||||||
|
- Endpoint: " . $context['request']['uri'] . "
|
||||||
|
- Méthode: " . $context['request']['method'] . "
|
||||||
|
- IP: " . $context['request']['ip'] . "
|
||||||
|
- User Agent: " . substr($context['request']['user_agent'] ?? 'N/A', 0, 100) . "
|
||||||
|
";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($context['session']) && $context['session']['user_id']) {
|
||||||
|
$body .= "
|
||||||
|
Session:
|
||||||
|
- User ID: " . $context['session']['user_id'] . "
|
||||||
|
- Entity ID: " . ($context['session']['entity_id'] ?? 'N/A') . "
|
||||||
|
";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($context['system'])) {
|
||||||
|
$body .= "
|
||||||
|
Système:
|
||||||
|
- Mémoire: " . $context['system']['memory_usage_mb'] . " MB / " . $context['system']['memory_peak_mb'] . " MB (peak)
|
||||||
|
- Load Average: " . implode(', ', $context['system']['load_average'] ?? []) . "
|
||||||
|
";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les données spécifiques selon le type
|
||||||
|
$body .= self::getTypeSpecificDetails($type, $context);
|
||||||
|
|
||||||
|
// Actions recommandées
|
||||||
|
$body .= "
|
||||||
|
|
||||||
|
ACTIONS RECOMMANDÉES
|
||||||
|
--------------------
|
||||||
|
" . self::getRecommendedActions($type, $level, $context) . "
|
||||||
|
|
||||||
|
LIENS UTILES
|
||||||
|
------------
|
||||||
|
- Logs: https://dapp.geosector.fr/admin/logs
|
||||||
|
- Dashboard: https://dapp.geosector.fr/admin/security
|
||||||
|
- Bloquer IP: https://dapp.geosector.fr/admin/block-ip/" . ($context['request']['ip'] ?? '') . "
|
||||||
|
|
||||||
|
--
|
||||||
|
Email automatique généré par GeoSector Security
|
||||||
|
Ne pas répondre à cet email
|
||||||
|
";
|
||||||
|
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détails spécifiques selon le type d'alerte
|
||||||
|
*/
|
||||||
|
private static function getTypeSpecificDetails(string $type, array $context): string {
|
||||||
|
$details = "\nDÉTAILS SPÉCIFIQUES\n-------------------\n";
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'BRUTE_FORCE':
|
||||||
|
$details .= "- Tentatives: " . ($context['attempts'] ?? 'N/A') . "\n";
|
||||||
|
$details .= "- Username ciblé: " . ($context['username'] ?? 'N/A') . "\n";
|
||||||
|
$details .= "- Période: " . ($context['timeframe'] ?? 'N/A') . "\n";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SQL_ERROR':
|
||||||
|
$details .= "- Erreur SQL: " . ($context['sql_error'] ?? 'N/A') . "\n";
|
||||||
|
$details .= "- Code: " . ($context['sql_code'] ?? 'N/A') . "\n";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PERFORMANCE_SLOW':
|
||||||
|
case 'PERFORMANCE_CRITICAL':
|
||||||
|
$details .= "- Temps réponse: " . ($context['response_time_ms'] ?? 'N/A') . " ms\n";
|
||||||
|
$details .= "- Temps DB: " . ($context['db_time_ms'] ?? 'N/A') . " ms\n";
|
||||||
|
$details .= "- Seuil dépassé: " . ($context['threshold_ms'] ?? 'N/A') . " ms\n";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'HTTP_500':
|
||||||
|
$details .= "- Message d'erreur: " . ($context['error_message'] ?? 'N/A') . "\n";
|
||||||
|
$details .= "- Stack trace: " . substr($context['stack_trace'] ?? 'N/A', 0, 500) . "\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions recommandées selon le type
|
||||||
|
*/
|
||||||
|
private static function getRecommendedActions(string $type, string $level, array $context): string {
|
||||||
|
$actions = [];
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'BRUTE_FORCE':
|
||||||
|
$actions[] = "1. Vérifier les logs de connexion";
|
||||||
|
$actions[] = "2. Bloquer l'IP si nécessaire";
|
||||||
|
$actions[] = "3. Vérifier l'intégrité du compte ciblé";
|
||||||
|
$actions[] = "4. Considérer l'activation de 2FA";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SQL_INJECTION':
|
||||||
|
$actions[] = "1. BLOQUER L'IP IMMÉDIATEMENT";
|
||||||
|
$actions[] = "2. Vérifier l'intégrité de la base de données";
|
||||||
|
$actions[] = "3. Analyser les logs pour d'autres tentatives";
|
||||||
|
$actions[] = "4. Patcher la vulnérabilité identifiée";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'DB_CONNECTION':
|
||||||
|
$actions[] = "1. Vérifier le statut du serveur MySQL";
|
||||||
|
$actions[] = "2. Vérifier les connexions actives";
|
||||||
|
$actions[] = "3. Redémarrer le service si nécessaire";
|
||||||
|
$actions[] = "4. Vérifier l'espace disque";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PERFORMANCE_CRITICAL':
|
||||||
|
$actions[] = "1. Identifier les requêtes lentes";
|
||||||
|
$actions[] = "2. Vérifier la charge serveur";
|
||||||
|
$actions[] = "3. Optimiser les requêtes problématiques";
|
||||||
|
$actions[] = "4. Considérer un scale-up si récurrent";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$actions[] = "1. Examiner les logs détaillés";
|
||||||
|
$actions[] = "2. Évaluer l'impact sur les utilisateurs";
|
||||||
|
$actions[] = "3. Prendre action selon la criticité";
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", $actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécuter des actions automatiques
|
||||||
|
*/
|
||||||
|
private static function executeAutomaticActions(string $type, array $context): void {
|
||||||
|
switch ($type) {
|
||||||
|
case 'BRUTE_FORCE':
|
||||||
|
// Bloquer automatiquement l'IP
|
||||||
|
if (isset($context['request']['ip'])) {
|
||||||
|
require_once __DIR__ . '/IPBlocker.php';
|
||||||
|
IPBlocker::block(
|
||||||
|
$context['request']['ip'],
|
||||||
|
3600, // 1 heure
|
||||||
|
'Brute force detected: ' . ($context['attempts'] ?? 0) . ' attempts'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SQL_INJECTION':
|
||||||
|
// Bloquer immédiatement et définitivement
|
||||||
|
if (isset($context['request']['ip'])) {
|
||||||
|
require_once __DIR__ . '/IPBlocker.php';
|
||||||
|
IPBlocker::blockPermanent(
|
||||||
|
$context['request']['ip'],
|
||||||
|
'SQL Injection attempt detected'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir l'instance de base de données
|
||||||
|
*/
|
||||||
|
private static function getDb(): PDO {
|
||||||
|
if (self::$db === null) {
|
||||||
|
self::$db = Database::getInstance();
|
||||||
|
}
|
||||||
|
return self::$db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir l'instance du throttler
|
||||||
|
*/
|
||||||
|
private static function getThrottler(): EmailThrottler {
|
||||||
|
if (self::$throttler === null) {
|
||||||
|
self::$throttler = new EmailThrottler();
|
||||||
|
}
|
||||||
|
return self::$throttler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résoudre une alerte
|
||||||
|
*/
|
||||||
|
public static function resolve(int $alertId, int $userId, string $notes = ''): bool {
|
||||||
|
try {
|
||||||
|
$db = self::getDb();
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
UPDATE sec_alerts
|
||||||
|
SET resolved = 1,
|
||||||
|
resolved_at = NOW(),
|
||||||
|
resolved_by = :user_id,
|
||||||
|
notes = :notes
|
||||||
|
WHERE id = :id
|
||||||
|
');
|
||||||
|
|
||||||
|
return $stmt->execute([
|
||||||
|
'id' => $alertId,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'notes' => $notes
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to resolve alert: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les alertes actives
|
||||||
|
*/
|
||||||
|
public static function getActiveAlerts(int $limit = 50): array {
|
||||||
|
try {
|
||||||
|
$db = self::getDb();
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT * FROM sec_active_alerts
|
||||||
|
LIMIT :limit
|
||||||
|
');
|
||||||
|
$stmt->bindValue('limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to get active alerts: " . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
294
api/src/Services/Security/EmailThrottler.php
Normal file
294
api/src/Services/Security/EmailThrottler.php
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Security;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use Database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de throttling pour les emails d'alerte
|
||||||
|
* Évite le spam et groupe les alertes similaires
|
||||||
|
*/
|
||||||
|
class EmailThrottler {
|
||||||
|
|
||||||
|
private static ?PDO $db = null;
|
||||||
|
private static array $cache = [];
|
||||||
|
|
||||||
|
// Configuration par défaut
|
||||||
|
const DEFAULT_CONFIG = [
|
||||||
|
'max_per_hour' => 10,
|
||||||
|
'max_per_day' => 50,
|
||||||
|
'digest_after' => 5, // Grouper après 5 alertes similaires
|
||||||
|
'cooldown_minutes' => 60
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier si on peut envoyer un email pour ce type d'alerte
|
||||||
|
*/
|
||||||
|
public function canSend(string $alertType, int $throttleSeconds = 0): bool {
|
||||||
|
// Vérifier le cache en mémoire d'abord
|
||||||
|
if (isset(self::$cache[$alertType])) {
|
||||||
|
$lastSent = self::$cache[$alertType];
|
||||||
|
if (time() - $lastSent < $throttleSeconds) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier en base de données
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
// Vérifier le throttle spécifique au type
|
||||||
|
if ($throttleSeconds > 0) {
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE alert_type = :type
|
||||||
|
AND email_sent = 1
|
||||||
|
AND email_sent_at >= DATE_SUB(NOW(), INTERVAL :seconds SECOND)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'type' => $alertType,
|
||||||
|
'seconds' => $throttleSeconds
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($result['count'] > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les limites globales
|
||||||
|
return $this->checkGlobalLimits();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier les limites globales (par heure et par jour)
|
||||||
|
*/
|
||||||
|
private function checkGlobalLimits(): bool {
|
||||||
|
$db = self::getDb();
|
||||||
|
$config = self::getConfig();
|
||||||
|
|
||||||
|
// Vérifier limite horaire
|
||||||
|
$hourlyStmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE email_sent = 1
|
||||||
|
AND email_sent_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||||
|
');
|
||||||
|
$hourlyStmt->execute();
|
||||||
|
$hourlyCount = $hourlyStmt->fetch(PDO::FETCH_ASSOC)['count'];
|
||||||
|
|
||||||
|
if ($hourlyCount >= $config['max_per_hour']) {
|
||||||
|
error_log("Email throttler: Hourly limit reached ({$hourlyCount}/{$config['max_per_hour']})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier limite quotidienne
|
||||||
|
$dailyStmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE email_sent = 1
|
||||||
|
AND email_sent_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)
|
||||||
|
');
|
||||||
|
$dailyStmt->execute();
|
||||||
|
$dailyCount = $dailyStmt->fetch(PDO::FETCH_ASSOC)['count'];
|
||||||
|
|
||||||
|
if ($dailyCount >= $config['max_per_day']) {
|
||||||
|
error_log("Email throttler: Daily limit reached ({$dailyCount}/{$config['max_per_day']})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistrer l'envoi d'un email
|
||||||
|
*/
|
||||||
|
public function recordSent(string $alertType): void {
|
||||||
|
// Mettre à jour le cache
|
||||||
|
self::$cache[$alertType] = time();
|
||||||
|
|
||||||
|
// Nettoyer le cache périodiquement
|
||||||
|
if (count(self::$cache) > 100) {
|
||||||
|
$cutoff = time() - 3600; // Garder seulement la dernière heure
|
||||||
|
self::$cache = array_filter(self::$cache, function($time) use ($cutoff) {
|
||||||
|
return $time > $cutoff;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier si on doit envoyer un digest plutôt que des alertes individuelles
|
||||||
|
*/
|
||||||
|
public function shouldSendDigest(string $alertType): bool {
|
||||||
|
$db = self::getDb();
|
||||||
|
$config = self::getConfig();
|
||||||
|
|
||||||
|
// Compter les alertes non envoyées du même type
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE alert_type = :type
|
||||||
|
AND email_sent = 0
|
||||||
|
AND resolved = 0
|
||||||
|
AND created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['type' => $alertType]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return $result['count'] >= $config['digest_after'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les alertes pour un digest
|
||||||
|
*/
|
||||||
|
public function getDigestAlerts(string $alertType, int $limit = 50): array {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT *
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE alert_type = :type
|
||||||
|
AND email_sent = 0
|
||||||
|
AND resolved = 0
|
||||||
|
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||||
|
ORDER BY alert_level DESC, last_seen DESC
|
||||||
|
LIMIT :limit
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->bindValue('type', $alertType, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue('limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marquer les alertes comme envoyées dans un digest
|
||||||
|
*/
|
||||||
|
public function markDigestSent(array $alertIds): void {
|
||||||
|
if (empty($alertIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = self::getDb();
|
||||||
|
$placeholders = implode(',', array_fill(0, count($alertIds), '?'));
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE sec_alerts
|
||||||
|
SET email_sent = 1,
|
||||||
|
email_sent_at = NOW(),
|
||||||
|
notes = CONCAT(IFNULL(notes, ''), '\nInclu dans digest email')
|
||||||
|
WHERE id IN ({$placeholders})
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute($alertIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les statistiques de throttling
|
||||||
|
*/
|
||||||
|
public function getStats(): array {
|
||||||
|
$db = self::getDb();
|
||||||
|
$config = self::getConfig();
|
||||||
|
|
||||||
|
// Emails envoyés dans l'heure
|
||||||
|
$hourlyStmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE email_sent = 1
|
||||||
|
AND email_sent_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||||
|
');
|
||||||
|
$hourlyStmt->execute();
|
||||||
|
$hourlyCount = $hourlyStmt->fetch(PDO::FETCH_ASSOC)['count'];
|
||||||
|
|
||||||
|
// Emails envoyés dans la journée
|
||||||
|
$dailyStmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE email_sent = 1
|
||||||
|
AND email_sent_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)
|
||||||
|
');
|
||||||
|
$dailyStmt->execute();
|
||||||
|
$dailyCount = $dailyStmt->fetch(PDO::FETCH_ASSOC)['count'];
|
||||||
|
|
||||||
|
// Alertes en attente
|
||||||
|
$pendingStmt = $db->prepare('
|
||||||
|
SELECT
|
||||||
|
alert_type,
|
||||||
|
COUNT(*) as count,
|
||||||
|
MIN(first_seen) as oldest
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE email_sent = 0
|
||||||
|
AND resolved = 0
|
||||||
|
GROUP BY alert_type
|
||||||
|
');
|
||||||
|
$pendingStmt->execute();
|
||||||
|
$pending = $pendingStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'hourly' => [
|
||||||
|
'sent' => $hourlyCount,
|
||||||
|
'limit' => $config['max_per_hour'],
|
||||||
|
'remaining' => max(0, $config['max_per_hour'] - $hourlyCount)
|
||||||
|
],
|
||||||
|
'daily' => [
|
||||||
|
'sent' => $dailyCount,
|
||||||
|
'limit' => $config['max_per_day'],
|
||||||
|
'remaining' => max(0, $config['max_per_day'] - $dailyCount)
|
||||||
|
],
|
||||||
|
'pending_alerts' => $pending,
|
||||||
|
'cache_size' => count(self::$cache),
|
||||||
|
'config' => $config
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialiser les compteurs (utile pour les tests)
|
||||||
|
*/
|
||||||
|
public function reset(): void {
|
||||||
|
self::$cache = [];
|
||||||
|
|
||||||
|
// Optionnel : réinitialiser les flags en DB
|
||||||
|
$db = self::getDb();
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
UPDATE sec_alerts
|
||||||
|
SET email_sent = 0, email_sent_at = NULL
|
||||||
|
WHERE email_sent_at < DATE_SUB(NOW(), INTERVAL 1 DAY)
|
||||||
|
');
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir la configuration
|
||||||
|
*/
|
||||||
|
private static function getConfig(): array {
|
||||||
|
// Essayer de charger depuis AppConfig si disponible
|
||||||
|
try {
|
||||||
|
if (class_exists('AppConfig')) {
|
||||||
|
$appConfig = \AppConfig::getInstance();
|
||||||
|
// AppConfig n'a pas de méthode get() générique
|
||||||
|
// On pourrait ajouter une configuration dans getCurrentConfig() si nécessaire
|
||||||
|
// Pour l'instant, utiliser la configuration par défaut
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Utiliser config par défaut
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::DEFAULT_CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir l'instance de base de données
|
||||||
|
*/
|
||||||
|
private static function getDb(): PDO {
|
||||||
|
if (self::$db === null) {
|
||||||
|
self::$db = Database::getInstance();
|
||||||
|
}
|
||||||
|
return self::$db;
|
||||||
|
}
|
||||||
|
}
|
||||||
510
api/src/Services/Security/IPBlocker.php
Normal file
510
api/src/Services/Security/IPBlocker.php
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Security;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/AlertService.php';
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use Database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des blocages d'IP
|
||||||
|
* Bloque temporairement ou définitivement des adresses IP suspectes
|
||||||
|
*/
|
||||||
|
class IPBlocker {
|
||||||
|
|
||||||
|
private static ?PDO $db = null;
|
||||||
|
private static array $cache = [];
|
||||||
|
private static ?int $lastCacheClean = null;
|
||||||
|
|
||||||
|
// IPs en whitelist (jamais bloquées)
|
||||||
|
const WHITELIST = [
|
||||||
|
'127.0.0.1',
|
||||||
|
'::1',
|
||||||
|
'localhost'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier si une IP est bloquée
|
||||||
|
*/
|
||||||
|
public static function isBlocked(string $ip): bool {
|
||||||
|
// Vérifier la whitelist
|
||||||
|
if (in_array($ip, self::WHITELIST)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le cache en mémoire
|
||||||
|
if (isset(self::$cache[$ip])) {
|
||||||
|
$cached = self::$cache[$ip];
|
||||||
|
if ($cached['blocked_until'] > time()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Expirée, retirer du cache
|
||||||
|
unset(self::$cache[$ip]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier en base de données
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT ip_address, blocked_until, permanent, reason
|
||||||
|
FROM sec_blocked_ips
|
||||||
|
WHERE ip_address = :ip
|
||||||
|
AND (permanent = 1 OR blocked_until > NOW())
|
||||||
|
AND unblocked_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['ip' => $ip]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
// Mettre en cache
|
||||||
|
self::$cache[$ip] = [
|
||||||
|
'blocked_until' => strtotime($result['blocked_until']),
|
||||||
|
'permanent' => (bool)$result['permanent'],
|
||||||
|
'reason' => $result['reason']
|
||||||
|
];
|
||||||
|
|
||||||
|
// Nettoyer le cache périodiquement
|
||||||
|
self::cleanCacheIfNeeded();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bloquer une IP temporairement
|
||||||
|
*/
|
||||||
|
public static function block(
|
||||||
|
string $ip,
|
||||||
|
int $durationSeconds = 3600,
|
||||||
|
string $reason = 'Suspicious activity',
|
||||||
|
string $blockedBy = 'system'
|
||||||
|
): bool {
|
||||||
|
// Ne pas bloquer les IPs en whitelist
|
||||||
|
if (in_array($ip, self::WHITELIST)) {
|
||||||
|
error_log("Attempted to block whitelisted IP: $ip");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Vérifier si l'IP est déjà bloquée
|
||||||
|
$checkStmt = $db->prepare('
|
||||||
|
SELECT ip_address, block_count
|
||||||
|
FROM sec_blocked_ips
|
||||||
|
WHERE ip_address = :ip
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
$checkStmt->execute(['ip' => $ip]);
|
||||||
|
$existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$blockedUntil = date('Y-m-d H:i:s', time() + $durationSeconds);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
// Mettre à jour le blocage existant
|
||||||
|
$updateStmt = $db->prepare('
|
||||||
|
UPDATE sec_blocked_ips
|
||||||
|
SET blocked_until = :until,
|
||||||
|
reason = :reason,
|
||||||
|
blocked_at = NOW(),
|
||||||
|
blocked_by = :by,
|
||||||
|
block_count = block_count + 1,
|
||||||
|
unblocked_at = NULL,
|
||||||
|
permanent = 0
|
||||||
|
WHERE ip_address = :ip
|
||||||
|
');
|
||||||
|
|
||||||
|
$updateStmt->execute([
|
||||||
|
'ip' => $ip,
|
||||||
|
'until' => $blockedUntil,
|
||||||
|
'reason' => $reason,
|
||||||
|
'by' => $blockedBy
|
||||||
|
]);
|
||||||
|
|
||||||
|
$blockCount = $existing['block_count'] + 1;
|
||||||
|
|
||||||
|
// Si bloquée plus de 3 fois, envisager un blocage permanent
|
||||||
|
if ($blockCount >= 3) {
|
||||||
|
AlertService::trigger('REPEAT_OFFENDER', [
|
||||||
|
'ip' => $ip,
|
||||||
|
'block_count' => $blockCount,
|
||||||
|
'reason' => $reason,
|
||||||
|
'message' => "IP bloquée pour la {$blockCount}e fois, considérer blocage permanent"
|
||||||
|
], 'WARNING');
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Créer un nouveau blocage
|
||||||
|
$insertStmt = $db->prepare('
|
||||||
|
INSERT INTO sec_blocked_ips (
|
||||||
|
ip_address, reason, blocked_until, blocked_by,
|
||||||
|
permanent, details
|
||||||
|
) VALUES (
|
||||||
|
:ip, :reason, :until, :by, 0, :details
|
||||||
|
)
|
||||||
|
');
|
||||||
|
|
||||||
|
$details = json_encode([
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'duration_seconds' => $durationSeconds,
|
||||||
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||||
|
'endpoint' => $_SERVER['REQUEST_URI'] ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$insertStmt->execute([
|
||||||
|
'ip' => $ip,
|
||||||
|
'reason' => $reason,
|
||||||
|
'until' => $blockedUntil,
|
||||||
|
'by' => $blockedBy,
|
||||||
|
'details' => $details
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le cache
|
||||||
|
self::$cache[$ip] = [
|
||||||
|
'blocked_until' => time() + $durationSeconds,
|
||||||
|
'permanent' => false,
|
||||||
|
'reason' => $reason
|
||||||
|
];
|
||||||
|
|
||||||
|
// Logger l'action
|
||||||
|
error_log("IP blocked: $ip for {$durationSeconds}s - Reason: $reason");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to block IP: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bloquer une IP définitivement
|
||||||
|
*/
|
||||||
|
public static function blockPermanent(
|
||||||
|
string $ip,
|
||||||
|
string $reason = 'Security threat',
|
||||||
|
string $blockedBy = 'system'
|
||||||
|
): bool {
|
||||||
|
// Ne pas bloquer les IPs en whitelist
|
||||||
|
if (in_array($ip, self::WHITELIST)) {
|
||||||
|
error_log("Attempted to permanently block whitelisted IP: $ip");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Vérifier si l'IP existe déjà
|
||||||
|
$checkStmt = $db->prepare('
|
||||||
|
SELECT ip_address FROM sec_blocked_ips
|
||||||
|
WHERE ip_address = :ip
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
$checkStmt->execute(['ip' => $ip]);
|
||||||
|
$existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
// Mettre à jour en permanent
|
||||||
|
$updateStmt = $db->prepare('
|
||||||
|
UPDATE sec_blocked_ips
|
||||||
|
SET permanent = 1,
|
||||||
|
blocked_until = "2099-12-31 23:59:59",
|
||||||
|
reason = :reason,
|
||||||
|
blocked_at = NOW(),
|
||||||
|
blocked_by = :by,
|
||||||
|
unblocked_at = NULL
|
||||||
|
WHERE ip_address = :ip
|
||||||
|
');
|
||||||
|
|
||||||
|
$updateStmt->execute([
|
||||||
|
'ip' => $ip,
|
||||||
|
'reason' => $reason,
|
||||||
|
'by' => $blockedBy
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Créer un blocage permanent
|
||||||
|
$insertStmt = $db->prepare('
|
||||||
|
INSERT INTO sec_blocked_ips (
|
||||||
|
ip_address, reason, blocked_until, blocked_by,
|
||||||
|
permanent, details
|
||||||
|
) VALUES (
|
||||||
|
:ip, :reason, "2099-12-31 23:59:59", :by, 1, :details
|
||||||
|
)
|
||||||
|
');
|
||||||
|
|
||||||
|
$details = json_encode([
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'permanent' => true,
|
||||||
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||||
|
'endpoint' => $_SERVER['REQUEST_URI'] ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$insertStmt->execute([
|
||||||
|
'ip' => $ip,
|
||||||
|
'reason' => $reason,
|
||||||
|
'by' => $blockedBy,
|
||||||
|
'details' => $details
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le cache
|
||||||
|
self::$cache[$ip] = [
|
||||||
|
'blocked_until' => PHP_INT_MAX,
|
||||||
|
'permanent' => true,
|
||||||
|
'reason' => $reason
|
||||||
|
];
|
||||||
|
|
||||||
|
// Alerter pour blocage permanent
|
||||||
|
AlertService::trigger('IP_BLOCKED_PERMANENT', [
|
||||||
|
'ip' => $ip,
|
||||||
|
'reason' => $reason,
|
||||||
|
'blocked_by' => $blockedBy,
|
||||||
|
'message' => "IP bloquée définitivement : $ip"
|
||||||
|
], 'SECURITY');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to permanently block IP: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Débloquer une IP
|
||||||
|
*/
|
||||||
|
public static function unblock(string $ip, int $unblockedBy = null): bool {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
UPDATE sec_blocked_ips
|
||||||
|
SET unblocked_at = NOW(),
|
||||||
|
unblocked_by = :by
|
||||||
|
WHERE ip_address = :ip
|
||||||
|
AND unblocked_at IS NULL
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'ip' => $ip,
|
||||||
|
'by' => $unblockedBy
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Retirer du cache
|
||||||
|
unset(self::$cache[$ip]);
|
||||||
|
|
||||||
|
$affected = $stmt->rowCount();
|
||||||
|
|
||||||
|
if ($affected > 0) {
|
||||||
|
error_log("IP unblocked: $ip by user $unblockedBy");
|
||||||
|
|
||||||
|
// Logger l'action
|
||||||
|
AlertService::trigger('IP_UNBLOCKED', [
|
||||||
|
'ip' => $ip,
|
||||||
|
'unblocked_by' => $unblockedBy,
|
||||||
|
'message' => "IP débloquée manuellement : $ip"
|
||||||
|
], 'INFO');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $affected > 0;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to unblock IP: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir la liste des IPs bloquées
|
||||||
|
*/
|
||||||
|
public static function getBlockedIPs(bool $activeOnly = true): array {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
$whereClause = $activeOnly
|
||||||
|
? 'WHERE unblocked_at IS NULL AND (permanent = 1 OR blocked_until > NOW())'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
ip_address,
|
||||||
|
reason,
|
||||||
|
blocked_at,
|
||||||
|
blocked_until,
|
||||||
|
blocked_by,
|
||||||
|
permanent,
|
||||||
|
unblocked_at,
|
||||||
|
unblocked_by,
|
||||||
|
block_count,
|
||||||
|
details
|
||||||
|
FROM sec_blocked_ips
|
||||||
|
$whereClause
|
||||||
|
ORDER BY blocked_at DESC
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier si une IP est dans un range CIDR
|
||||||
|
*/
|
||||||
|
public static function isInRange(string $ip, string $cidr): bool {
|
||||||
|
list($subnet, $bits) = explode('/', $cidr);
|
||||||
|
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
|
||||||
|
filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
// IPv4
|
||||||
|
$ip = ip2long($ip);
|
||||||
|
$subnet = ip2long($subnet);
|
||||||
|
$mask = -1 << (32 - $bits);
|
||||||
|
$subnet &= $mask;
|
||||||
|
|
||||||
|
return ($ip & $mask) == $subnet;
|
||||||
|
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) &&
|
||||||
|
filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
// IPv6
|
||||||
|
$ipBin = inet_pton($ip);
|
||||||
|
$subnetBin = inet_pton($subnet);
|
||||||
|
|
||||||
|
$byteCount = (int)($bits / 8);
|
||||||
|
$bitCount = $bits % 8;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $byteCount; $i++) {
|
||||||
|
if ($ipBin[$i] !== $subnetBin[$i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bitCount > 0) {
|
||||||
|
$mask = 0xFF << (8 - $bitCount);
|
||||||
|
return (ord($ipBin[$byteCount]) & $mask) === (ord($subnetBin[$byteCount]) & $mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyer les blocages expirés
|
||||||
|
*/
|
||||||
|
public static function cleanupExpired(): int {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
UPDATE sec_blocked_ips
|
||||||
|
SET unblocked_at = NOW()
|
||||||
|
WHERE blocked_until <= NOW()
|
||||||
|
AND permanent = 0
|
||||||
|
AND unblocked_at IS NULL
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$cleaned = $stmt->rowCount();
|
||||||
|
|
||||||
|
if ($cleaned > 0) {
|
||||||
|
error_log("Cleaned up $cleaned expired IP blocks");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cleaned;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to cleanup expired blocks: " . $e->getMessage());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les statistiques de blocage
|
||||||
|
*/
|
||||||
|
public static function getStats(): array {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
// Total des IPs bloquées
|
||||||
|
$totalStmt = $db->prepare('
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN permanent = 1 THEN 1 ELSE 0 END) as permanent,
|
||||||
|
SUM(CASE WHEN permanent = 0 AND blocked_until > NOW() THEN 1 ELSE 0 END) as temporary,
|
||||||
|
SUM(CASE WHEN unblocked_at IS NOT NULL THEN 1 ELSE 0 END) as unblocked
|
||||||
|
FROM sec_blocked_ips
|
||||||
|
');
|
||||||
|
$totalStmt->execute();
|
||||||
|
$totals = $totalStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Blocages récents (24h)
|
||||||
|
$recentStmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM sec_blocked_ips
|
||||||
|
WHERE blocked_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||||
|
');
|
||||||
|
$recentStmt->execute();
|
||||||
|
$recent = $recentStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Top raisons de blocage
|
||||||
|
$reasonsStmt = $db->prepare('
|
||||||
|
SELECT
|
||||||
|
reason,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM sec_blocked_ips
|
||||||
|
WHERE unblocked_at IS NULL
|
||||||
|
GROUP BY reason
|
||||||
|
ORDER BY count DESC
|
||||||
|
LIMIT 10
|
||||||
|
');
|
||||||
|
$reasonsStmt->execute();
|
||||||
|
$reasons = $reasonsStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'totals' => $totals,
|
||||||
|
'recent_24h' => $recent['count'],
|
||||||
|
'top_reasons' => $reasons,
|
||||||
|
'cache_size' => count(self::$cache),
|
||||||
|
'whitelist' => self::WHITELIST
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyer le cache si nécessaire
|
||||||
|
*/
|
||||||
|
private static function cleanCacheIfNeeded(): void {
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
// Nettoyer toutes les 5 minutes
|
||||||
|
if (self::$lastCacheClean === null || $now - self::$lastCacheClean > 300) {
|
||||||
|
self::$cache = array_filter(self::$cache, function($item) use ($now) {
|
||||||
|
return $item['permanent'] || $item['blocked_until'] > $now;
|
||||||
|
});
|
||||||
|
|
||||||
|
self::$lastCacheClean = $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limiter la taille du cache
|
||||||
|
if (count(self::$cache) > 1000) {
|
||||||
|
self::$cache = array_slice(self::$cache, -500, null, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir l'instance de base de données
|
||||||
|
*/
|
||||||
|
private static function getDb(): PDO {
|
||||||
|
if (self::$db === null) {
|
||||||
|
self::$db = Database::getInstance();
|
||||||
|
}
|
||||||
|
return self::$db;
|
||||||
|
}
|
||||||
|
}
|
||||||
452
api/src/Services/Security/PerformanceMonitor.php
Normal file
452
api/src/Services/Security/PerformanceMonitor.php
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Security;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/AlertService.php';
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use Database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de monitoring des performances
|
||||||
|
* Mesure et alerte sur les temps de réponse, utilisation mémoire, etc.
|
||||||
|
*/
|
||||||
|
class PerformanceMonitor {
|
||||||
|
|
||||||
|
private static ?PDO $db = null;
|
||||||
|
private static ?float $requestStartTime = null;
|
||||||
|
private static ?float $requestStartMemory = null;
|
||||||
|
private static array $dbQueries = [];
|
||||||
|
private static float $dbTotalTime = 0;
|
||||||
|
private static int $dbQueryCount = 0;
|
||||||
|
|
||||||
|
// Seuils par défaut (en millisecondes)
|
||||||
|
const DEFAULT_THRESHOLDS = [
|
||||||
|
'response_time_warning' => 1000, // 1 seconde
|
||||||
|
'response_time_critical' => 3000, // 3 secondes
|
||||||
|
'db_time_warning' => 500, // 500ms
|
||||||
|
'db_time_critical' => 1000, // 1 seconde
|
||||||
|
'memory_warning' => 64, // 64 MB
|
||||||
|
'memory_critical' => 128, // 128 MB
|
||||||
|
'db_queries_warning' => 20, // 20 requêtes
|
||||||
|
'db_queries_critical' => 50 // 50 requêtes
|
||||||
|
];
|
||||||
|
|
||||||
|
// Seuils spécifiques par endpoint
|
||||||
|
const ENDPOINT_THRESHOLDS = [
|
||||||
|
'/api/operations/export' => [
|
||||||
|
'response_time_warning' => 5000,
|
||||||
|
'response_time_critical' => 10000,
|
||||||
|
'memory_warning' => 256,
|
||||||
|
'memory_critical' => 512
|
||||||
|
],
|
||||||
|
'/api/chat/rooms' => [
|
||||||
|
'response_time_warning' => 500,
|
||||||
|
'response_time_critical' => 1000
|
||||||
|
],
|
||||||
|
'/api/logs' => [
|
||||||
|
'response_time_warning' => 200,
|
||||||
|
'response_time_critical' => 500
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarrer le monitoring d'une requête
|
||||||
|
*/
|
||||||
|
public static function startRequest(): void {
|
||||||
|
self::$requestStartTime = microtime(true);
|
||||||
|
self::$requestStartMemory = memory_get_usage(true);
|
||||||
|
self::$dbQueries = [];
|
||||||
|
self::$dbTotalTime = 0;
|
||||||
|
self::$dbQueryCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarrer le monitoring d'une requête DB
|
||||||
|
*/
|
||||||
|
public static function startDbQuery(string $query): void {
|
||||||
|
self::$dbQueries[] = [
|
||||||
|
'query' => $query,
|
||||||
|
'start' => microtime(true),
|
||||||
|
'duration' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminer le monitoring d'une requête DB
|
||||||
|
*/
|
||||||
|
public static function endDbQuery(): void {
|
||||||
|
if (!empty(self::$dbQueries)) {
|
||||||
|
$lastIndex = count(self::$dbQueries) - 1;
|
||||||
|
$duration = (microtime(true) - self::$dbQueries[$lastIndex]['start']) * 1000;
|
||||||
|
self::$dbQueries[$lastIndex]['duration'] = $duration;
|
||||||
|
self::$dbTotalTime += $duration;
|
||||||
|
self::$dbQueryCount++;
|
||||||
|
|
||||||
|
// Alerter si requête très lente
|
||||||
|
if ($duration > 1000) { // Plus d'1 seconde
|
||||||
|
AlertService::trigger('SLOW_QUERY', [
|
||||||
|
'query' => substr(self::$dbQueries[$lastIndex]['query'], 0, 500),
|
||||||
|
'duration_ms' => $duration,
|
||||||
|
'message' => "Requête SQL lente détectée : {$duration}ms"
|
||||||
|
], 'WARNING');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminer le monitoring d'une requête HTTP
|
||||||
|
*/
|
||||||
|
public static function endRequest(string $endpoint, string $method, int $httpStatus): void {
|
||||||
|
if (self::$requestStartTime === null) {
|
||||||
|
return; // Monitoring non démarré
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer les métriques
|
||||||
|
$responseTime = (microtime(true) - self::$requestStartTime) * 1000;
|
||||||
|
$memoryPeak = memory_get_peak_usage(true) / 1024 / 1024; // En MB
|
||||||
|
$memoryStart = self::$requestStartMemory / 1024 / 1024;
|
||||||
|
$memoryUsed = $memoryPeak - $memoryStart;
|
||||||
|
|
||||||
|
// Enrichir avec les infos de requête
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'] ?? null;
|
||||||
|
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
$requestSize = strlen(file_get_contents('php://input'));
|
||||||
|
|
||||||
|
// ID utilisateur si connecté
|
||||||
|
$userId = null;
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION['user_id'])) {
|
||||||
|
$userId = $_SESSION['user_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enregistrer les métriques
|
||||||
|
self::saveMetrics([
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'method' => $method,
|
||||||
|
'response_time_ms' => (int)$responseTime,
|
||||||
|
'db_time_ms' => (int)self::$dbTotalTime,
|
||||||
|
'db_queries_count' => self::$dbQueryCount,
|
||||||
|
'memory_peak_mb' => $memoryPeak,
|
||||||
|
'memory_start_mb' => $memoryStart,
|
||||||
|
'http_status' => $httpStatus,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'ip_address' => $ip,
|
||||||
|
'user_agent' => $userAgent,
|
||||||
|
'request_size' => $requestSize,
|
||||||
|
'response_size' => ob_get_length() ?: 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Vérifier les seuils et alerter si nécessaire
|
||||||
|
self::checkThresholds($endpoint, $responseTime, self::$dbTotalTime, $memoryPeak);
|
||||||
|
|
||||||
|
// Vérifier la dégradation des performances
|
||||||
|
self::checkPerformanceDegradation($endpoint, $responseTime);
|
||||||
|
|
||||||
|
// Réinitialiser pour la prochaine requête
|
||||||
|
self::reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarder les métriques en base
|
||||||
|
*/
|
||||||
|
private static function saveMetrics(array $metrics): void {
|
||||||
|
try {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
INSERT INTO sec_performance_metrics (
|
||||||
|
endpoint, method, response_time_ms, db_time_ms, db_queries_count,
|
||||||
|
memory_peak_mb, memory_start_mb, http_status, user_id,
|
||||||
|
ip_address, user_agent, request_size, response_size
|
||||||
|
) VALUES (
|
||||||
|
:endpoint, :method, :response_time, :db_time, :db_queries,
|
||||||
|
:memory_peak, :memory_start, :status, :user_id,
|
||||||
|
:ip, :agent, :req_size, :resp_size
|
||||||
|
)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'endpoint' => $metrics['endpoint'],
|
||||||
|
'method' => $metrics['method'],
|
||||||
|
'response_time' => $metrics['response_time_ms'],
|
||||||
|
'db_time' => $metrics['db_time_ms'],
|
||||||
|
'db_queries' => $metrics['db_queries_count'],
|
||||||
|
'memory_peak' => $metrics['memory_peak_mb'],
|
||||||
|
'memory_start' => $metrics['memory_start_mb'],
|
||||||
|
'status' => $metrics['http_status'],
|
||||||
|
'user_id' => $metrics['user_id'],
|
||||||
|
'ip' => $metrics['ip_address'],
|
||||||
|
'agent' => $metrics['user_agent'],
|
||||||
|
'req_size' => $metrics['request_size'],
|
||||||
|
'resp_size' => $metrics['response_size']
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to save performance metrics: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier les seuils de performance
|
||||||
|
*/
|
||||||
|
private static function checkThresholds(
|
||||||
|
string $endpoint,
|
||||||
|
float $responseTime,
|
||||||
|
float $dbTime,
|
||||||
|
float $memoryPeak
|
||||||
|
): void {
|
||||||
|
// Obtenir les seuils pour cet endpoint
|
||||||
|
$thresholds = self::getThresholdsForEndpoint($endpoint);
|
||||||
|
|
||||||
|
// Vérifier temps de réponse
|
||||||
|
if ($responseTime > $thresholds['response_time_critical']) {
|
||||||
|
AlertService::trigger('PERFORMANCE_CRITICAL', [
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'response_time_ms' => $responseTime,
|
||||||
|
'threshold_ms' => $thresholds['response_time_critical'],
|
||||||
|
'db_time_ms' => $dbTime,
|
||||||
|
'message' => "Performance critique sur $endpoint : {$responseTime}ms"
|
||||||
|
], 'ERROR');
|
||||||
|
} elseif ($responseTime > $thresholds['response_time_warning']) {
|
||||||
|
AlertService::trigger('PERFORMANCE_SLOW', [
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'response_time_ms' => $responseTime,
|
||||||
|
'threshold_ms' => $thresholds['response_time_warning'],
|
||||||
|
'db_time_ms' => $dbTime,
|
||||||
|
'message' => "Performance lente sur $endpoint : {$responseTime}ms"
|
||||||
|
], 'WARNING');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier temps DB
|
||||||
|
if ($dbTime > $thresholds['db_time_critical']) {
|
||||||
|
AlertService::trigger('DB_PERFORMANCE_CRITICAL', [
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'db_time_ms' => $dbTime,
|
||||||
|
'threshold_ms' => $thresholds['db_time_critical'],
|
||||||
|
'queries_count' => self::$dbQueryCount,
|
||||||
|
'message' => "Temps DB critique : {$dbTime}ms pour " . self::$dbQueryCount . " requêtes"
|
||||||
|
], 'ERROR');
|
||||||
|
} elseif ($dbTime > $thresholds['db_time_warning']) {
|
||||||
|
AlertService::trigger('DB_PERFORMANCE_SLOW', [
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'db_time_ms' => $dbTime,
|
||||||
|
'threshold_ms' => $thresholds['db_time_warning'],
|
||||||
|
'queries_count' => self::$dbQueryCount,
|
||||||
|
'message' => "Temps DB lent : {$dbTime}ms"
|
||||||
|
], 'WARNING');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier mémoire
|
||||||
|
if ($memoryPeak > $thresholds['memory_critical']) {
|
||||||
|
AlertService::trigger('MEMORY_CRITICAL', [
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'memory_mb' => $memoryPeak,
|
||||||
|
'threshold_mb' => $thresholds['memory_critical'],
|
||||||
|
'message' => "Utilisation mémoire critique : {$memoryPeak}MB"
|
||||||
|
], 'ERROR');
|
||||||
|
} elseif ($memoryPeak > $thresholds['memory_warning']) {
|
||||||
|
AlertService::trigger('MEMORY_HIGH', [
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'memory_mb' => $memoryPeak,
|
||||||
|
'threshold_mb' => $thresholds['memory_warning'],
|
||||||
|
'message' => "Utilisation mémoire élevée : {$memoryPeak}MB"
|
||||||
|
], 'WARNING');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier nombre de requêtes DB
|
||||||
|
if (self::$dbQueryCount > $thresholds['db_queries_critical']) {
|
||||||
|
AlertService::trigger('DB_QUERIES_EXCESSIVE', [
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'queries_count' => self::$dbQueryCount,
|
||||||
|
'threshold' => $thresholds['db_queries_critical'],
|
||||||
|
'message' => "Trop de requêtes DB : " . self::$dbQueryCount
|
||||||
|
], 'ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier la dégradation des performances
|
||||||
|
*/
|
||||||
|
private static function checkPerformanceDegradation(string $endpoint, float $currentTime): void {
|
||||||
|
try {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
// Calculer la moyenne sur les 100 dernières requêtes
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT AVG(response_time_ms) as avg_time,
|
||||||
|
STDDEV(response_time_ms) as stddev_time,
|
||||||
|
COUNT(*) as sample_size
|
||||||
|
FROM (
|
||||||
|
SELECT response_time_ms
|
||||||
|
FROM sec_performance_metrics
|
||||||
|
WHERE endpoint = :endpoint
|
||||||
|
AND http_status < 500
|
||||||
|
AND created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
) as recent
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['endpoint' => $endpoint]);
|
||||||
|
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($stats && $stats['sample_size'] >= 10) {
|
||||||
|
$avgTime = (float)$stats['avg_time'];
|
||||||
|
$stdDev = (float)$stats['stddev_time'];
|
||||||
|
|
||||||
|
// Alerter si 2x plus lent que la moyenne
|
||||||
|
if ($currentTime > ($avgTime * 2) && $avgTime > 100) {
|
||||||
|
AlertService::trigger('PERFORMANCE_DEGRADATION', [
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'current_time_ms' => $currentTime,
|
||||||
|
'average_time_ms' => $avgTime,
|
||||||
|
'stddev_ms' => $stdDev,
|
||||||
|
'factor' => round($currentTime / $avgTime, 2),
|
||||||
|
'sample_size' => $stats['sample_size'],
|
||||||
|
'message' => "Performance dégradée : " . round($currentTime / $avgTime, 1) . "x plus lent que la moyenne"
|
||||||
|
], 'WARNING');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to check performance degradation: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les seuils pour un endpoint
|
||||||
|
*/
|
||||||
|
private static function getThresholdsForEndpoint(string $endpoint): array {
|
||||||
|
// Chercher des seuils spécifiques
|
||||||
|
foreach (self::ENDPOINT_THRESHOLDS as $pattern => $thresholds) {
|
||||||
|
if (strpos($endpoint, $pattern) === 0) {
|
||||||
|
return array_merge(self::DEFAULT_THRESHOLDS, $thresholds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retourner les seuils par défaut
|
||||||
|
return self::DEFAULT_THRESHOLDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les statistiques de performance
|
||||||
|
*/
|
||||||
|
public static function getStats(string $endpoint = null, int $hours = 24): array {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
$whereClause = 'WHERE created_at >= DATE_SUB(NOW(), INTERVAL :hours HOUR)';
|
||||||
|
$params = ['hours' => $hours];
|
||||||
|
|
||||||
|
if ($endpoint) {
|
||||||
|
$whereClause .= ' AND endpoint = :endpoint';
|
||||||
|
$params['endpoint'] = $endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistiques globales
|
||||||
|
$globalStmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_requests,
|
||||||
|
AVG(response_time_ms) as avg_response_time,
|
||||||
|
MIN(response_time_ms) as min_response_time,
|
||||||
|
MAX(response_time_ms) as max_response_time,
|
||||||
|
AVG(db_time_ms) as avg_db_time,
|
||||||
|
AVG(memory_peak_mb) as avg_memory,
|
||||||
|
SUM(CASE WHEN http_status >= 500 THEN 1 ELSE 0 END) as errors_5xx,
|
||||||
|
SUM(CASE WHEN http_status >= 400 AND http_status < 500 THEN 1 ELSE 0 END) as errors_4xx
|
||||||
|
FROM sec_performance_metrics
|
||||||
|
$whereClause
|
||||||
|
");
|
||||||
|
|
||||||
|
$globalStmt->execute($params);
|
||||||
|
$globalStats = $globalStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Top endpoints lents
|
||||||
|
$slowStmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
endpoint,
|
||||||
|
method,
|
||||||
|
COUNT(*) as requests,
|
||||||
|
AVG(response_time_ms) as avg_time,
|
||||||
|
MAX(response_time_ms) as max_time
|
||||||
|
FROM sec_performance_metrics
|
||||||
|
$whereClause
|
||||||
|
GROUP BY endpoint, method
|
||||||
|
ORDER BY avg_time DESC
|
||||||
|
LIMIT 10
|
||||||
|
");
|
||||||
|
|
||||||
|
$slowStmt->execute($params);
|
||||||
|
$slowEndpoints = $slowStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Requêtes DB lentes
|
||||||
|
$slowDbStmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
endpoint,
|
||||||
|
MAX(db_time_ms) as max_db_time,
|
||||||
|
AVG(db_time_ms) as avg_db_time,
|
||||||
|
AVG(db_queries_count) as avg_queries
|
||||||
|
FROM sec_performance_metrics
|
||||||
|
WHERE db_time_ms > 500
|
||||||
|
AND created_at >= DATE_SUB(NOW(), INTERVAL :hours HOUR)
|
||||||
|
GROUP BY endpoint
|
||||||
|
ORDER BY max_db_time DESC
|
||||||
|
LIMIT 10
|
||||||
|
");
|
||||||
|
|
||||||
|
$slowDbStmt->execute(['hours' => $hours]);
|
||||||
|
$slowDbQueries = $slowDbStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'timeframe_hours' => $hours,
|
||||||
|
'global' => $globalStats,
|
||||||
|
'slow_endpoints' => $slowEndpoints,
|
||||||
|
'slow_db_operations' => $slowDbQueries,
|
||||||
|
'current_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
|
||||||
|
'current_load' => sys_getloadavg()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyer les anciennes métriques
|
||||||
|
*/
|
||||||
|
public static function cleanup(int $daysToKeep = 30): int {
|
||||||
|
try {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
DELETE FROM sec_performance_metrics
|
||||||
|
WHERE created_at < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['days' => $daysToKeep]);
|
||||||
|
|
||||||
|
return $stmt->rowCount();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to cleanup performance metrics: " . $e->getMessage());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialiser le monitoring
|
||||||
|
*/
|
||||||
|
private static function reset(): void {
|
||||||
|
self::$requestStartTime = null;
|
||||||
|
self::$requestStartMemory = null;
|
||||||
|
self::$dbQueries = [];
|
||||||
|
self::$dbTotalTime = 0;
|
||||||
|
self::$dbQueryCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir l'instance de base de données
|
||||||
|
*/
|
||||||
|
private static function getDb(): PDO {
|
||||||
|
if (self::$db === null) {
|
||||||
|
self::$db = Database::getInstance();
|
||||||
|
}
|
||||||
|
return self::$db;
|
||||||
|
}
|
||||||
|
}
|
||||||
440
api/src/Services/Security/SecurityMonitor.php
Normal file
440
api/src/Services/Security/SecurityMonitor.php
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Security;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/AlertService.php';
|
||||||
|
require_once __DIR__ . '/IPBlocker.php';
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use Database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de monitoring de sécurité
|
||||||
|
* Détecte les patterns d'attaque et les comportements suspects
|
||||||
|
*/
|
||||||
|
class SecurityMonitor {
|
||||||
|
|
||||||
|
private static ?PDO $db = null;
|
||||||
|
|
||||||
|
// Patterns suspects dans les paramètres
|
||||||
|
const SQL_INJECTION_PATTERNS = [
|
||||||
|
'/\bunion\b.*\bselect\b/i',
|
||||||
|
'/\bselect\b.*\bfrom\b.*\bwhere\b/i',
|
||||||
|
'/\bdrop\b.*\btable\b/i',
|
||||||
|
'/\binsert\b.*\binto\b/i',
|
||||||
|
'/\bupdate\b.*\bset\b/i',
|
||||||
|
'/\bdelete\b.*\bfrom\b/i',
|
||||||
|
'/\bexec(\s|\()/i',
|
||||||
|
'/\bscript\b.*\b\/script\b/i',
|
||||||
|
'/\b(char|nchar|varchar|nvarchar)\s*\(/i',
|
||||||
|
'/\bconvert\s*\(/i',
|
||||||
|
'/\bcast\s*\(/i',
|
||||||
|
'/\bwaitfor\s+delay\b/i',
|
||||||
|
'/\bsleep\s*\(/i',
|
||||||
|
'/\bbenchmark\s*\(/i',
|
||||||
|
'/\;.*\-\-/i',
|
||||||
|
'/\bor\b.*\=.*\bor\b/i',
|
||||||
|
'/\b1\s*\=\s*1\b/i',
|
||||||
|
'/\b\'\s*or\s*\'\s*\=\s*\'/i'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Patterns de scan/exploration
|
||||||
|
const SCAN_PATTERNS = [
|
||||||
|
'admin', 'administrator', 'wp-admin', 'phpmyadmin',
|
||||||
|
'.git', '.env', 'config.php', 'wp-config.php',
|
||||||
|
'backup', '.sql', '.bak', '.zip', '.tar',
|
||||||
|
'shell.php', 'c99.php', 'r57.php', 'eval.php'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier les tentatives de brute force
|
||||||
|
*/
|
||||||
|
public static function checkBruteForce(string $ip, string $username = null): bool {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
// Compter les tentatives récentes depuis cette IP
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as attempts,
|
||||||
|
COUNT(DISTINCT username) as unique_users,
|
||||||
|
MIN(attempt_time) as first_attempt,
|
||||||
|
MAX(attempt_time) as last_attempt
|
||||||
|
FROM sec_failed_login_attempts
|
||||||
|
WHERE ip_address = :ip
|
||||||
|
AND attempt_time >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['ip' => $ip]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$attempts = (int)$result['attempts'];
|
||||||
|
$uniqueUsers = (int)$result['unique_users'];
|
||||||
|
|
||||||
|
// Critères de détection
|
||||||
|
$isBruteForce = false;
|
||||||
|
$reason = '';
|
||||||
|
|
||||||
|
if ($attempts >= 5) {
|
||||||
|
$isBruteForce = true;
|
||||||
|
$reason = "Plus de 5 tentatives en 5 minutes";
|
||||||
|
} elseif ($uniqueUsers >= 3) {
|
||||||
|
$isBruteForce = true;
|
||||||
|
$reason = "Tentatives sur 3 usernames différents";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isBruteForce) {
|
||||||
|
// Déclencher l'alerte
|
||||||
|
AlertService::trigger('BRUTE_FORCE', [
|
||||||
|
'ip' => $ip,
|
||||||
|
'username' => $username,
|
||||||
|
'attempts' => $attempts,
|
||||||
|
'unique_usernames' => $uniqueUsers,
|
||||||
|
'timeframe' => '5 minutes',
|
||||||
|
'reason' => $reason,
|
||||||
|
'message' => "Attaque brute force détectée depuis $ip : $reason"
|
||||||
|
], 'SECURITY');
|
||||||
|
|
||||||
|
return false; // Bloquer
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Autoriser
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistrer une tentative de login échouée
|
||||||
|
*/
|
||||||
|
public static function recordFailedLogin(
|
||||||
|
string $ip,
|
||||||
|
string $username = null,
|
||||||
|
string $errorType = 'invalid_credentials',
|
||||||
|
string $userAgent = null
|
||||||
|
): void {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Chercher si le username existe (pour stocker la version chiffrée)
|
||||||
|
$encryptedUsername = null;
|
||||||
|
if ($username) {
|
||||||
|
$userStmt = $db->prepare('
|
||||||
|
SELECT encrypted_user_name
|
||||||
|
FROM users
|
||||||
|
WHERE username = :username
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
$userStmt->execute(['username' => $username]);
|
||||||
|
$user = $userStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($user) {
|
||||||
|
$encryptedUsername = $user['encrypted_user_name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enregistrer la tentative
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
INSERT INTO sec_failed_login_attempts (
|
||||||
|
username, encrypted_username, ip_address,
|
||||||
|
user_agent, error_type
|
||||||
|
) VALUES (
|
||||||
|
:username, :encrypted, :ip, :agent, :error
|
||||||
|
)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'username' => $username,
|
||||||
|
'encrypted' => $encryptedUsername,
|
||||||
|
'ip' => $ip,
|
||||||
|
'agent' => $userAgent,
|
||||||
|
'error' => $errorType
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Vérifier si on doit bloquer l'IP
|
||||||
|
if (!self::checkBruteForce($ip, $username)) {
|
||||||
|
// L'IP sera bloquée par AlertService via les actions automatiques
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to record login attempt: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier les patterns d'injection SQL
|
||||||
|
*/
|
||||||
|
public static function checkSQLInjection(string $value): bool {
|
||||||
|
// Nettoyer la valeur pour l'analyse
|
||||||
|
$decoded = urldecode($value);
|
||||||
|
|
||||||
|
foreach (self::SQL_INJECTION_PATTERNS as $pattern) {
|
||||||
|
if (preg_match($pattern, $decoded)) {
|
||||||
|
// Injection SQL détectée !
|
||||||
|
$context = [
|
||||||
|
'pattern_matched' => $pattern,
|
||||||
|
'value' => substr($value, 0, 500), // Limiter la taille
|
||||||
|
'decoded_value' => substr($decoded, 0, 500),
|
||||||
|
'message' => 'Tentative d\'injection SQL détectée'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Enrichir avec le contexte de requête
|
||||||
|
if (isset($_SERVER['REQUEST_URI'])) {
|
||||||
|
$context['endpoint'] = $_SERVER['REQUEST_URI'];
|
||||||
|
$context['method'] = $_SERVER['REQUEST_METHOD'] ?? 'unknown';
|
||||||
|
$context['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertService::trigger('SQL_INJECTION', $context, 'SECURITY');
|
||||||
|
|
||||||
|
return false; // Bloquer la requête
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Pas d'injection détectée
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier tous les paramètres d'une requête
|
||||||
|
*/
|
||||||
|
public static function checkRequestParameters(array $params): bool {
|
||||||
|
$suspicious = [];
|
||||||
|
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
if (is_string($value)) {
|
||||||
|
if (!self::checkSQLInjection($value)) {
|
||||||
|
$suspicious[$key] = $value;
|
||||||
|
}
|
||||||
|
} elseif (is_array($value)) {
|
||||||
|
// Vérifier récursivement
|
||||||
|
if (!self::checkRequestParameters($value)) {
|
||||||
|
$suspicious[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($suspicious)) {
|
||||||
|
// Paramètres suspects détectés
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détecter les patterns de scan/exploration
|
||||||
|
*/
|
||||||
|
public static function checkScanPattern(string $uri): bool {
|
||||||
|
$lowerUri = strtolower($uri);
|
||||||
|
|
||||||
|
foreach (self::SCAN_PATTERNS as $pattern) {
|
||||||
|
if (strpos($lowerUri, $pattern) !== false) {
|
||||||
|
// Pattern de scan détecté
|
||||||
|
AlertService::trigger('SUSPICIOUS_PATTERN', [
|
||||||
|
'pattern' => $pattern,
|
||||||
|
'uri' => $uri,
|
||||||
|
'message' => "Tentative d'accès à un fichier sensible : $pattern"
|
||||||
|
], 'WARNING');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier les accès non autorisés
|
||||||
|
*/
|
||||||
|
public static function checkUnauthorizedAccess(string $endpoint, bool $hasAuth): bool {
|
||||||
|
// Endpoints publics qui n'ont pas besoin d'auth
|
||||||
|
$publicEndpoints = [
|
||||||
|
'/api/login',
|
||||||
|
'/api/users/check-username',
|
||||||
|
'/api/logs',
|
||||||
|
'/api/health',
|
||||||
|
'/api/status'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Vérifier si l'endpoint nécessite une auth
|
||||||
|
$isPublic = false;
|
||||||
|
foreach ($publicEndpoints as $public) {
|
||||||
|
if (strpos($endpoint, $public) === 0) {
|
||||||
|
$isPublic = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isPublic && !$hasAuth) {
|
||||||
|
// Accès non autorisé
|
||||||
|
AlertService::trigger('UNAUTHORIZED_ACCESS', [
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'message' => "Tentative d'accès sans authentification à : $endpoint"
|
||||||
|
], 'WARNING');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyser les patterns 404
|
||||||
|
*/
|
||||||
|
public static function check404Pattern(string $ip): void {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
// Compter les 404 récents de cette IP
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as count,
|
||||||
|
COUNT(DISTINCT endpoint) as unique_endpoints
|
||||||
|
FROM sec_performance_metrics
|
||||||
|
WHERE ip_address = :ip
|
||||||
|
AND http_status = 404
|
||||||
|
AND created_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['ip' => $ip]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$count404 = (int)$result['count'];
|
||||||
|
$uniqueEndpoints = (int)$result['unique_endpoints'];
|
||||||
|
|
||||||
|
// Si trop de 404, c'est suspect
|
||||||
|
if ($count404 >= 10 || $uniqueEndpoints >= 5) {
|
||||||
|
AlertService::trigger('HTTP_404_PATTERN', [
|
||||||
|
'ip' => $ip,
|
||||||
|
'count_404' => $count404,
|
||||||
|
'unique_endpoints' => $uniqueEndpoints,
|
||||||
|
'timeframe' => '10 minutes',
|
||||||
|
'message' => "Pattern de scan détecté : $count404 erreurs 404 sur $uniqueEndpoints endpoints"
|
||||||
|
], 'WARNING');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier la vitesse des requêtes (rate limiting)
|
||||||
|
*/
|
||||||
|
public static function checkRateLimit(string $ip): bool {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
// Compter les requêtes de la dernière minute
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM sec_performance_metrics
|
||||||
|
WHERE ip_address = :ip
|
||||||
|
AND created_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute(['ip' => $ip]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$requestsPerMinute = (int)$result['count'];
|
||||||
|
|
||||||
|
// Limite : 60 requêtes par minute
|
||||||
|
if ($requestsPerMinute > 60) {
|
||||||
|
AlertService::trigger('RATE_LIMIT_EXCEEDED', [
|
||||||
|
'ip' => $ip,
|
||||||
|
'requests_per_minute' => $requestsPerMinute,
|
||||||
|
'limit' => 60,
|
||||||
|
'message' => "Limite de taux dépassée : $requestsPerMinute requêtes/minute"
|
||||||
|
], 'WARNING');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyser une erreur SQL pour détecter les problèmes
|
||||||
|
*/
|
||||||
|
public static function analyzeSQLError(\PDOException $e, string $query = null): void {
|
||||||
|
$message = $e->getMessage();
|
||||||
|
$code = $e->getCode();
|
||||||
|
|
||||||
|
// Classifier l'erreur
|
||||||
|
$level = 'ERROR';
|
||||||
|
$type = 'SQL_ERROR';
|
||||||
|
|
||||||
|
if (strpos($message, 'Unknown column') !== false) {
|
||||||
|
$type = 'SQL_ERROR';
|
||||||
|
$level = 'ERROR';
|
||||||
|
} elseif (strpos($message, 'Table') !== false && strpos($message, 'doesn\'t exist') !== false) {
|
||||||
|
$type = 'SQL_ERROR';
|
||||||
|
$level = 'CRITICAL';
|
||||||
|
} elseif (strpos($message, 'Connection refused') !== false || strpos($message, 'Can\'t connect') !== false) {
|
||||||
|
$type = 'DB_CONNECTION';
|
||||||
|
$level = 'CRITICAL';
|
||||||
|
} elseif (strpos($message, 'Too many connections') !== false) {
|
||||||
|
$type = 'DB_CONNECTION';
|
||||||
|
$level = 'CRITICAL';
|
||||||
|
} elseif (strpos($message, 'Deadlock') !== false) {
|
||||||
|
$type = 'DB_DEADLOCK';
|
||||||
|
$level = 'ERROR';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer l'alerte
|
||||||
|
AlertService::trigger($type, [
|
||||||
|
'sql_error' => $message,
|
||||||
|
'sql_code' => $code,
|
||||||
|
'query' => $query ? substr($query, 0, 500) : null,
|
||||||
|
'message' => "Erreur SQL : $message"
|
||||||
|
], $level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les statistiques de sécurité
|
||||||
|
*/
|
||||||
|
public static function getSecurityStats(): array {
|
||||||
|
$db = self::getDb();
|
||||||
|
|
||||||
|
// Statistiques des dernières 24h
|
||||||
|
$stats = [];
|
||||||
|
|
||||||
|
// Tentatives de login
|
||||||
|
$loginStmt = $db->prepare('
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_attempts,
|
||||||
|
COUNT(DISTINCT ip_address) as unique_ips,
|
||||||
|
COUNT(DISTINCT username) as unique_usernames
|
||||||
|
FROM sec_failed_login_attempts
|
||||||
|
WHERE attempt_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||||
|
');
|
||||||
|
$loginStmt->execute();
|
||||||
|
$stats['failed_logins'] = $loginStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// IPs bloquées
|
||||||
|
$blockedStmt = $db->prepare('
|
||||||
|
SELECT COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN permanent = 1 THEN 1 ELSE 0 END) as permanent,
|
||||||
|
SUM(CASE WHEN blocked_until > NOW() THEN 1 ELSE 0 END) as active
|
||||||
|
FROM sec_blocked_ips
|
||||||
|
');
|
||||||
|
$blockedStmt->execute();
|
||||||
|
$stats['blocked_ips'] = $blockedStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Alertes par type
|
||||||
|
$alertsStmt = $db->prepare('
|
||||||
|
SELECT
|
||||||
|
alert_type,
|
||||||
|
alert_level,
|
||||||
|
COUNT(*) as count,
|
||||||
|
MAX(last_seen) as last_seen
|
||||||
|
FROM sec_alerts
|
||||||
|
WHERE last_seen >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||||
|
GROUP BY alert_type, alert_level
|
||||||
|
ORDER BY count DESC
|
||||||
|
');
|
||||||
|
$alertsStmt->execute();
|
||||||
|
$stats['alerts_by_type'] = $alertsStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir l'instance de base de données
|
||||||
|
*/
|
||||||
|
private static function getDb(): PDO {
|
||||||
|
if (self::$db === null) {
|
||||||
|
self::$db = Database::getInstance();
|
||||||
|
}
|
||||||
|
return self::$db;
|
||||||
|
}
|
||||||
|
}
|
||||||
178
api/src/Services/SimplePDF.php
Normal file
178
api/src/Services/SimplePDF.php
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Générateur de PDF simple avec support d'images
|
||||||
|
* Génère des PDF légers avec logo
|
||||||
|
*/
|
||||||
|
class SimplePDF {
|
||||||
|
private string $content = '';
|
||||||
|
private array $objects = [];
|
||||||
|
private int $objectCount = 0;
|
||||||
|
private array $xref = [];
|
||||||
|
private float $pageWidth = 595.0; // A4 width in points
|
||||||
|
private float $pageHeight = 842.0; // A4 height in points
|
||||||
|
private float $margin = 50.0;
|
||||||
|
private float $currentY = 0;
|
||||||
|
private int $fontObject = 0;
|
||||||
|
private int $pageObject = 0;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->currentY = $this->pageHeight - $this->margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute du texte au PDF
|
||||||
|
*/
|
||||||
|
public function addText(string $text, float $x, float $y, int $fontSize = 12): void {
|
||||||
|
$this->content .= "BT\n";
|
||||||
|
$this->content .= "/F1 $fontSize Tf\n";
|
||||||
|
$this->content .= "$x $y Td\n";
|
||||||
|
$this->content .= "(" . $this->escapeString($text) . ") Tj\n";
|
||||||
|
$this->content .= "ET\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une ligne de texte avec positionnement automatique
|
||||||
|
*/
|
||||||
|
public function addLine(string $text, int $fontSize = 11, string $align = 'left'): void {
|
||||||
|
$x = $this->margin;
|
||||||
|
|
||||||
|
if ($align === 'center') {
|
||||||
|
// Estimation approximative de la largeur du texte
|
||||||
|
$textWidth = strlen($text) * $fontSize * 0.5;
|
||||||
|
$x = ($this->pageWidth - $textWidth) / 2;
|
||||||
|
} elseif ($align === 'right') {
|
||||||
|
$textWidth = strlen($text) * $fontSize * 0.5;
|
||||||
|
$x = $this->pageWidth - $this->margin - $textWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addText($text, $x, $this->currentY, $fontSize);
|
||||||
|
$this->currentY -= ($fontSize + 8); // Line height
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un espace vertical
|
||||||
|
*/
|
||||||
|
public function addSpace(float $space = 20): void {
|
||||||
|
$this->currentY -= $space;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une ligne horizontale
|
||||||
|
*/
|
||||||
|
public function addHorizontalLine(): void {
|
||||||
|
$y = $this->currentY;
|
||||||
|
$this->content .= "q\n"; // Save state
|
||||||
|
$this->content .= "0.5 w\n"; // Line width
|
||||||
|
$this->content .= $this->margin . " $y m\n"; // Move to start
|
||||||
|
$this->content .= ($this->pageWidth - $this->margin) . " $y l\n"; // Line to end
|
||||||
|
$this->content .= "S\n"; // Stroke
|
||||||
|
$this->content .= "Q\n"; // Restore state
|
||||||
|
$this->currentY -= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un rectangle (pour encadrer)
|
||||||
|
*/
|
||||||
|
public function addRectangle(float $x, float $y, float $width, float $height, bool $fill = false): void {
|
||||||
|
$this->content .= "q\n";
|
||||||
|
$this->content .= "0.8 w\n"; // Line width
|
||||||
|
$this->content .= "$x $y $width $height re\n"; // Rectangle
|
||||||
|
$this->content .= $fill ? "f\n" : "S\n"; // Fill or Stroke
|
||||||
|
$this->content .= "Q\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Échappe les caractères spéciaux pour le PDF
|
||||||
|
*/
|
||||||
|
private function escapeString(string $str): string {
|
||||||
|
// Échapper les caractères spéciaux PDF
|
||||||
|
$str = str_replace('\\', '\\\\', $str);
|
||||||
|
$str = str_replace('(', '\\(', $str);
|
||||||
|
$str = str_replace(')', '\\)', $str);
|
||||||
|
|
||||||
|
// Convertir les caractères accentués
|
||||||
|
$accents = [
|
||||||
|
'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A',
|
||||||
|
'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a',
|
||||||
|
'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E',
|
||||||
|
'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
|
||||||
|
'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
|
||||||
|
'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
|
||||||
|
'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O',
|
||||||
|
'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o',
|
||||||
|
'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U',
|
||||||
|
'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u',
|
||||||
|
'Ñ' => 'N', 'ñ' => 'n',
|
||||||
|
'Ç' => 'C', 'ç' => 'c',
|
||||||
|
'€' => 'EUR',
|
||||||
|
'Œ' => 'OE', 'œ' => 'oe',
|
||||||
|
'Æ' => 'AE', 'æ' => 'ae'
|
||||||
|
];
|
||||||
|
|
||||||
|
$str = strtr($str, $accents);
|
||||||
|
|
||||||
|
// Supprimer tout caractère non-ASCII restant
|
||||||
|
$str = preg_replace('/[^\x20-\x7E]/', '', $str);
|
||||||
|
|
||||||
|
return $str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le PDF final
|
||||||
|
*/
|
||||||
|
public function generate(): string {
|
||||||
|
// Début du PDF
|
||||||
|
$pdf = "%PDF-1.4\n";
|
||||||
|
$pdf .= "%âãÏÓ\n"; // Binary marker
|
||||||
|
|
||||||
|
// Object 1 - Catalog
|
||||||
|
$this->objects[1] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
|
||||||
|
|
||||||
|
// Object 2 - Pages
|
||||||
|
$this->objects[2] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
|
||||||
|
|
||||||
|
// Object 3 - Page
|
||||||
|
$this->objects[3] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " .
|
||||||
|
$this->pageWidth . " " . $this->pageHeight .
|
||||||
|
"] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n";
|
||||||
|
|
||||||
|
// Object 4 - Font (Helvetica)
|
||||||
|
$this->objects[4] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\nendobj\n";
|
||||||
|
|
||||||
|
// Object 5 - Content stream
|
||||||
|
$contentLength = strlen($this->content);
|
||||||
|
$this->objects[5] = "5 0 obj\n<< /Length $contentLength >>\nstream\n" .
|
||||||
|
$this->content . "\nendstream\nendobj\n";
|
||||||
|
|
||||||
|
// Construction du PDF final
|
||||||
|
$offset = strlen($pdf);
|
||||||
|
foreach ($this->objects as $obj) {
|
||||||
|
$this->xref[] = $offset;
|
||||||
|
$pdf .= $obj;
|
||||||
|
$offset += strlen($obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table xref
|
||||||
|
$xrefStart = $offset;
|
||||||
|
$pdf .= "xref\n";
|
||||||
|
$pdf .= "0 " . (count($this->objects) + 1) . "\n";
|
||||||
|
$pdf .= "0000000000 65535 f \n";
|
||||||
|
foreach ($this->xref as $off) {
|
||||||
|
$pdf .= sprintf("%010d 00000 n \n", $off);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailer
|
||||||
|
$pdf .= "trailer\n";
|
||||||
|
$pdf .= "<< /Size " . (count($this->objects) + 1) . " /Root 1 0 R >>\n";
|
||||||
|
$pdf .= "startxref\n";
|
||||||
|
$pdf .= "$xrefStart\n";
|
||||||
|
$pdf .= "%%EOF\n";
|
||||||
|
|
||||||
|
return $pdf;
|
||||||
|
}
|
||||||
|
}
|
||||||
156
api/test_chat_temp_id.php
Normal file
156
api/test_chat_temp_id.php
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script de test pour vérifier le support des temp_id dans l'API chat
|
||||||
|
*
|
||||||
|
* Usage: php test_chat_temp_id.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
$apiUrl = 'http://localhost/api';
|
||||||
|
$sessionId = 'test_session_id'; // À remplacer par un session_id valide
|
||||||
|
|
||||||
|
// Fonction pour faire une requête HTTP
|
||||||
|
function makeRequest($method, $endpoint, $data = null, $sessionId = null) {
|
||||||
|
global $apiUrl;
|
||||||
|
|
||||||
|
$ch = curl_init($apiUrl . $endpoint);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||||
|
|
||||||
|
$headers = ['Content-Type: application/json'];
|
||||||
|
if ($sessionId) {
|
||||||
|
$headers[] = 'Authorization: Bearer ' . $sessionId;
|
||||||
|
}
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
|
||||||
|
if ($data !== null) {
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => $httpCode,
|
||||||
|
'response' => json_decode($response, true)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
echo "=== Tests de support des temp_id dans l'API Chat ===\n\n";
|
||||||
|
|
||||||
|
// Test 1: Création d'une room avec temp_id
|
||||||
|
echo "Test 1: Création d'une room avec temp_id\n";
|
||||||
|
$roomData = [
|
||||||
|
'type' => 'private',
|
||||||
|
'participants' => [2], // ID d'un autre utilisateur
|
||||||
|
'title' => 'Test Room Offline',
|
||||||
|
'temp_id' => 'temp_room_' . uniqid()
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "Requête POST /chat/rooms avec temp_id: {$roomData['temp_id']}\n";
|
||||||
|
$result = makeRequest('POST', '/chat/rooms', $roomData, $sessionId);
|
||||||
|
|
||||||
|
if ($result['code'] === 201 && isset($result['response']['room']['temp_id'])) {
|
||||||
|
echo "✅ Succès: Room créée avec temp_id retourné\n";
|
||||||
|
echo " - ID réel: " . $result['response']['room']['id'] . "\n";
|
||||||
|
echo " - temp_id: " . $result['response']['room']['temp_id'] . "\n";
|
||||||
|
$roomId = $result['response']['room']['id'];
|
||||||
|
} else {
|
||||||
|
echo "❌ Échec: temp_id non retourné dans la réponse\n";
|
||||||
|
echo " Response: " . json_encode($result['response']) . "\n";
|
||||||
|
$roomId = null;
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 2: Envoi d'un message avec temp_id
|
||||||
|
if ($roomId) {
|
||||||
|
echo "Test 2: Envoi d'un message avec temp_id\n";
|
||||||
|
$messageData = [
|
||||||
|
'content' => 'Message créé hors ligne',
|
||||||
|
'temp_id' => 'temp_msg_' . uniqid()
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "Requête POST /chat/rooms/{$roomId}/messages avec temp_id: {$messageData['temp_id']}\n";
|
||||||
|
$result = makeRequest('POST', "/chat/rooms/{$roomId}/messages", $messageData, $sessionId);
|
||||||
|
|
||||||
|
if ($result['code'] === 201 && isset($result['response']['message']['temp_id'])) {
|
||||||
|
echo "✅ Succès: Message créé avec temp_id retourné\n";
|
||||||
|
echo " - ID réel: " . $result['response']['message']['id'] . "\n";
|
||||||
|
echo " - temp_id: " . $result['response']['message']['temp_id'] . "\n";
|
||||||
|
$messageId = $result['response']['message']['id'];
|
||||||
|
} else {
|
||||||
|
echo "❌ Échec: temp_id non retourné dans la réponse\n";
|
||||||
|
echo " Response: " . json_encode($result['response']) . "\n";
|
||||||
|
$messageId = null;
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Mise à jour d'une room avec temp_id
|
||||||
|
if ($roomId) {
|
||||||
|
echo "Test 3: Mise à jour d'une room avec temp_id\n";
|
||||||
|
$updateData = [
|
||||||
|
'title' => 'Room mise à jour',
|
||||||
|
'temp_id' => 'temp_update_room_' . uniqid()
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "Requête PUT /chat/rooms/{$roomId} avec temp_id: {$updateData['temp_id']}\n";
|
||||||
|
$result = makeRequest('PUT', "/chat/rooms/{$roomId}", $updateData, $sessionId);
|
||||||
|
|
||||||
|
if ($result['code'] === 200 && isset($result['response']['room']['temp_id'])) {
|
||||||
|
echo "✅ Succès: Room mise à jour avec temp_id retourné\n";
|
||||||
|
echo " - temp_id: " . $result['response']['room']['temp_id'] . "\n";
|
||||||
|
} else {
|
||||||
|
echo "❌ Échec: temp_id non retourné dans la réponse\n";
|
||||||
|
echo " Response: " . json_encode($result['response']) . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Mise à jour d'un message avec temp_id
|
||||||
|
if ($messageId) {
|
||||||
|
echo "Test 4: Mise à jour d'un message avec temp_id\n";
|
||||||
|
$updateData = [
|
||||||
|
'content' => 'Message modifié',
|
||||||
|
'temp_id' => 'temp_update_msg_' . uniqid()
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "Requête PUT /chat/messages/{$messageId} avec temp_id: {$updateData['temp_id']}\n";
|
||||||
|
$result = makeRequest('PUT', "/chat/messages/{$messageId}", $updateData, $sessionId);
|
||||||
|
|
||||||
|
if ($result['code'] === 200 && isset($result['response']['message']['temp_id'])) {
|
||||||
|
echo "✅ Succès: Message mis à jour avec temp_id retourné\n";
|
||||||
|
echo " - temp_id: " . $result['response']['message']['temp_id'] . "\n";
|
||||||
|
} else {
|
||||||
|
echo "❌ Échec: temp_id non retourné dans la réponse\n";
|
||||||
|
echo " Response: " . json_encode($result['response']) . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 5: Création sans temp_id (doit continuer à fonctionner)
|
||||||
|
echo "Test 5: Création d'une room SANS temp_id (compatibilité)\n";
|
||||||
|
$roomData = [
|
||||||
|
'type' => 'private',
|
||||||
|
'participants' => [2],
|
||||||
|
'title' => 'Test Room Normal'
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "Requête POST /chat/rooms sans temp_id\n";
|
||||||
|
$result = makeRequest('POST', '/chat/rooms', $roomData, $sessionId);
|
||||||
|
|
||||||
|
if ($result['code'] === 201 && !isset($result['response']['room']['temp_id'])) {
|
||||||
|
echo "✅ Succès: Room créée normalement sans temp_id\n";
|
||||||
|
echo " - ID: " . $result['response']['room']['id'] . "\n";
|
||||||
|
} else if ($result['code'] === 201) {
|
||||||
|
echo "⚠️ Attention: Room créée mais temp_id présent alors qu'il ne devrait pas\n";
|
||||||
|
} else {
|
||||||
|
echo "❌ Échec: Erreur lors de la création\n";
|
||||||
|
echo " Response: " . json_encode($result['response']) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== Fin des tests ===\n";
|
||||||
|
echo "\nNote: Assurez-vous d'avoir un session_id valide et que l'API est accessible.\n";
|
||||||
|
echo "Pour obtenir un session_id valide, connectez-vous d'abord via POST /api/login\n";
|
||||||
254
api/test_security.php
Normal file
254
api/test_security.php
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script de test pour le système de sécurité et monitoring
|
||||||
|
* Usage: php test_security.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';
|
||||||
|
|
||||||
|
// Services de sécurité
|
||||||
|
require_once __DIR__ . '/src/Services/Security/AlertService.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/SecurityMonitor.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/PerformanceMonitor.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/IPBlocker.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/EmailThrottler.php';
|
||||||
|
|
||||||
|
use App\Services\Security\AlertService;
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
use App\Services\Security\PerformanceMonitor;
|
||||||
|
use App\Services\Security\IPBlocker;
|
||||||
|
use App\Services\Security\EmailThrottler;
|
||||||
|
|
||||||
|
// Initialiser la configuration
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
$config = $appConfig->getFullConfig();
|
||||||
|
|
||||||
|
// Initialiser la base de données
|
||||||
|
Database::init($config['database']);
|
||||||
|
|
||||||
|
echo "\n========================================\n";
|
||||||
|
echo " TEST DU SYSTÈME DE SÉCURITÉ\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
|
||||||
|
// Test 1: Vérifier les tables
|
||||||
|
echo "1. Vérification des tables de sécurité...\n";
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
$tables = [
|
||||||
|
'sec_alerts',
|
||||||
|
'sec_performance_metrics',
|
||||||
|
'sec_failed_login_attempts',
|
||||||
|
'sec_blocked_ips'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$stmt = $db->query("SELECT COUNT(*) as count FROM $table");
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
echo " ✓ Table $table : {$result['count']} enregistrements\n";
|
||||||
|
}
|
||||||
|
echo " [OK] Toutes les tables existent\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ✗ Erreur : " . $e->getMessage() . "\n";
|
||||||
|
echo " Assurez-vous d'avoir exécuté le script SQL de création des tables.\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Test du monitoring de performance
|
||||||
|
echo "2. Test du monitoring de performance...\n";
|
||||||
|
try {
|
||||||
|
// Simuler une requête
|
||||||
|
PerformanceMonitor::startRequest();
|
||||||
|
|
||||||
|
// Simuler une requête DB
|
||||||
|
PerformanceMonitor::startDbQuery("SELECT * FROM users WHERE id = 1");
|
||||||
|
usleep(50000); // 50ms
|
||||||
|
PerformanceMonitor::endDbQuery();
|
||||||
|
|
||||||
|
// Terminer la requête
|
||||||
|
PerformanceMonitor::endRequest('/api/test', 'GET', 200);
|
||||||
|
|
||||||
|
echo " ✓ Monitoring de performance fonctionnel\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ✗ Erreur : " . $e->getMessage() . "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Test de détection SQL Injection
|
||||||
|
echo "3. Test de détection d'injection SQL...\n";
|
||||||
|
$testQueries = [
|
||||||
|
"normal_query" => true,
|
||||||
|
"'; DROP TABLE users; --" => false,
|
||||||
|
"1' OR '1'='1" => false,
|
||||||
|
"admin' UNION SELECT * FROM users--" => false
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($testQueries as $query => $shouldPass) {
|
||||||
|
$result = SecurityMonitor::checkSQLInjection($query);
|
||||||
|
$status = ($result === $shouldPass) ? '✓' : '✗';
|
||||||
|
$expected = $shouldPass ? 'autorisé' : 'bloqué';
|
||||||
|
echo " $status '$query' : $expected\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 4: Test du blocage d'IP
|
||||||
|
echo "4. Test du système de blocage d'IP...\n";
|
||||||
|
try {
|
||||||
|
$testIP = '192.168.1.99';
|
||||||
|
|
||||||
|
// Vérifier que l'IP n'est pas bloquée
|
||||||
|
if (!IPBlocker::isBlocked($testIP)) {
|
||||||
|
echo " ✓ IP $testIP non bloquée initialement\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bloquer l'IP temporairement (10 secondes)
|
||||||
|
IPBlocker::block($testIP, 10, 'Test temporaire');
|
||||||
|
|
||||||
|
// Vérifier qu'elle est bloquée
|
||||||
|
if (IPBlocker::isBlocked($testIP)) {
|
||||||
|
echo " ✓ IP $testIP bloquée avec succès\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Débloquer l'IP
|
||||||
|
IPBlocker::unblock($testIP);
|
||||||
|
|
||||||
|
// Vérifier qu'elle est débloquée
|
||||||
|
if (!IPBlocker::isBlocked($testIP)) {
|
||||||
|
echo " ✓ IP $testIP débloquée avec succès\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo " [OK] Système de blocage IP fonctionnel\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ✗ Erreur : " . $e->getMessage() . "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 5: Test des alertes
|
||||||
|
echo "5. Test du système d'alertes...\n";
|
||||||
|
try {
|
||||||
|
// Créer une alerte de test
|
||||||
|
AlertService::trigger('TEST', [
|
||||||
|
'message' => 'Ceci est une alerte de test',
|
||||||
|
'test_script' => true
|
||||||
|
], 'INFO');
|
||||||
|
|
||||||
|
echo " ✓ Alerte créée avec succès\n";
|
||||||
|
|
||||||
|
// Récupérer les alertes actives
|
||||||
|
$alerts = AlertService::getActiveAlerts(1);
|
||||||
|
if (!empty($alerts)) {
|
||||||
|
echo " ✓ " . count($alerts) . " alerte(s) active(s) trouvée(s)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo " [OK] Système d'alertes fonctionnel\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ✗ Erreur : " . $e->getMessage() . "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 6: Test de brute force
|
||||||
|
echo "6. Simulation d'attaque brute force...\n";
|
||||||
|
try {
|
||||||
|
$attackerIP = '10.0.0.1';
|
||||||
|
|
||||||
|
// Simuler plusieurs tentatives échouées
|
||||||
|
for ($i = 1; $i <= 6; $i++) {
|
||||||
|
SecurityMonitor::recordFailedLogin(
|
||||||
|
$attackerIP,
|
||||||
|
'user' . $i,
|
||||||
|
'invalid_password',
|
||||||
|
'Mozilla/5.0 Test'
|
||||||
|
);
|
||||||
|
echo " - Tentative $i enregistrée\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier la détection
|
||||||
|
$canLogin = SecurityMonitor::checkBruteForce($attackerIP, 'testuser');
|
||||||
|
|
||||||
|
if (!$canLogin) {
|
||||||
|
echo " ✓ Attaque brute force détectée et bloquée\n";
|
||||||
|
} else {
|
||||||
|
echo " ✗ L'attaque aurait dû être détectée\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer
|
||||||
|
$db->exec("DELETE FROM sec_failed_login_attempts WHERE ip_address = '$attackerIP'");
|
||||||
|
IPBlocker::unblock($attackerIP);
|
||||||
|
|
||||||
|
echo " [OK] Détection brute force fonctionnelle\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ✗ Erreur : " . $e->getMessage() . "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 7: Test du throttling email
|
||||||
|
echo "7. Test du throttling d'emails...\n";
|
||||||
|
try {
|
||||||
|
$throttler = new EmailThrottler();
|
||||||
|
|
||||||
|
// Premier email devrait passer
|
||||||
|
if ($throttler->canSend('TEST_TYPE', 60)) {
|
||||||
|
echo " ✓ Premier email autorisé\n";
|
||||||
|
$throttler->recordSent('TEST_TYPE');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deuxième email immédiat devrait être bloqué
|
||||||
|
if (!$throttler->canSend('TEST_TYPE', 60)) {
|
||||||
|
echo " ✓ Deuxième email bloqué (throttling)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtenir les stats
|
||||||
|
$stats = $throttler->getStats();
|
||||||
|
echo " ✓ Stats throttling : {$stats['hourly']['sent']} emails/heure\n";
|
||||||
|
|
||||||
|
echo " [OK] Throttling email fonctionnel\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ✗ Erreur : " . $e->getMessage() . "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 8: Statistiques globales
|
||||||
|
echo "8. Récupération des statistiques...\n";
|
||||||
|
try {
|
||||||
|
// Stats de sécurité
|
||||||
|
$securityStats = SecurityMonitor::getSecurityStats();
|
||||||
|
echo " ✓ Tentatives de login échouées (24h) : " .
|
||||||
|
($securityStats['failed_logins']['total_attempts'] ?? 0) . "\n";
|
||||||
|
|
||||||
|
// Stats de performance
|
||||||
|
$perfStats = PerformanceMonitor::getStats(null, 24);
|
||||||
|
echo " ✓ Requêtes totales (24h) : " .
|
||||||
|
($perfStats['global']['total_requests'] ?? 0) . "\n";
|
||||||
|
|
||||||
|
// Stats de blocage IP
|
||||||
|
$ipStats = IPBlocker::getStats();
|
||||||
|
echo " ✓ IPs bloquées actives : " .
|
||||||
|
(($ipStats['totals']['temporary'] ?? 0) + ($ipStats['totals']['permanent'] ?? 0)) . "\n";
|
||||||
|
|
||||||
|
echo " [OK] Statistiques disponibles\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ✗ Erreur : " . $e->getMessage() . "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Résumé
|
||||||
|
echo "========================================\n";
|
||||||
|
echo " RÉSUMÉ DES TESTS\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
echo "✓ Tables de sécurité créées\n";
|
||||||
|
echo "✓ Monitoring de performance actif\n";
|
||||||
|
echo "✓ Détection SQL injection fonctionnelle\n";
|
||||||
|
echo "✓ Blocage d'IP opérationnel\n";
|
||||||
|
echo "✓ Système d'alertes configuré\n";
|
||||||
|
echo "✓ Détection brute force active\n";
|
||||||
|
echo "✓ Throttling email en place\n";
|
||||||
|
echo "✓ Statistiques disponibles\n\n";
|
||||||
|
|
||||||
|
echo "🔒 Le système de sécurité est opérationnel !\n\n";
|
||||||
|
|
||||||
|
echo "PROCHAINES ÉTAPES :\n";
|
||||||
|
echo "1. Configurer l'email dans AppConfig pour recevoir les alertes\n";
|
||||||
|
echo "2. Personnaliser les seuils dans les constantes des services\n";
|
||||||
|
echo "3. Tester avec de vraies requêtes API\n";
|
||||||
|
echo "4. Surveiller les logs et ajuster les règles\n\n";
|
||||||
150
api/tests/test_user_creation.php
Executable file
150
api/tests/test_user_creation.php
Executable file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script de test pour la création d'utilisateurs
|
||||||
|
* Teste les différents cas qui peuvent causer une erreur 400
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
$apiUrl = 'http://localhost/api';
|
||||||
|
$sessionId = ''; // À remplir avec un session_id valide
|
||||||
|
|
||||||
|
// Couleurs pour la sortie
|
||||||
|
$red = "\033[31m";
|
||||||
|
$green = "\033[32m";
|
||||||
|
$yellow = "\033[33m";
|
||||||
|
$blue = "\033[34m";
|
||||||
|
$reset = "\033[0m";
|
||||||
|
|
||||||
|
function testRequest($endpoint, $method, $data = null, $sessionId = '') {
|
||||||
|
$ch = curl_init($endpoint);
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||||
|
|
||||||
|
if ($sessionId) {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $sessionId
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => $httpCode,
|
||||||
|
'body' => json_decode($response, true) ?: $response
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $blue . "=== Test de création d'utilisateur et vérification username ===" . $reset . "\n\n";
|
||||||
|
|
||||||
|
// Test 1: Vérification username sans données
|
||||||
|
echo $yellow . "Test 1: POST /api/users/check-username sans données" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', [], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 2: Vérification username avec format invalide (trop court)
|
||||||
|
echo $yellow . "Test 2: POST /api/users/check-username avec username trop court" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => 'abc'], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Format attendu: " . ($result['body']['format'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 3: Vérification username avec format invalide (commence par un chiffre)
|
||||||
|
echo $yellow . "Test 3: POST /api/users/check-username avec username commençant par un chiffre" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => '123test.user'], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Valeur testée: " . ($result['body']['value'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 4: Vérification username valide
|
||||||
|
echo $yellow . "Test 4: POST /api/users/check-username avec username valide" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => 'test.user123'], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 200 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Disponible: " . ($result['body']['available'] ? 'Oui' : 'Non') . "\n";
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 5: Création utilisateur sans email
|
||||||
|
echo $yellow . "Test 5: POST /api/users sans email" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users', 'POST', [
|
||||||
|
'name' => 'Test User',
|
||||||
|
'fk_entite' => 1
|
||||||
|
], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 6: Création utilisateur sans nom
|
||||||
|
echo $yellow . "Test 6: POST /api/users sans nom" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users', 'POST', [
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'fk_entite' => 1
|
||||||
|
], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 7: Création utilisateur avec email invalide
|
||||||
|
echo $yellow . "Test 7: POST /api/users avec email invalide" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users', 'POST', [
|
||||||
|
'email' => 'invalid-email',
|
||||||
|
'name' => 'Test User',
|
||||||
|
'fk_entite' => 1
|
||||||
|
], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
echo "Valeur testée: " . ($result['body']['value'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 8: Création utilisateur pour entité avec username manuel mais sans username
|
||||||
|
echo $yellow . "Test 8: POST /api/users pour entité avec chk_username_manuel=1 sans username" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users', 'POST', [
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'name' => 'Test User',
|
||||||
|
'fk_entite' => 5 // Supposons que l'entité 5 a chk_username_manuel=1
|
||||||
|
], $sessionId);
|
||||||
|
echo "Code HTTP: " . $result['code'] . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
echo "Reason: " . ($result['body']['reason'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
echo $blue . "=== Fin des tests ===" . $reset . "\n";
|
||||||
|
echo "\nRappel: Pour que ces tests fonctionnent, vous devez:\n";
|
||||||
|
echo "1. Avoir un serveur local en cours d'exécution\n";
|
||||||
|
echo "2. Remplir la variable \$sessionId avec un token de session valide\n";
|
||||||
|
echo "3. Vérifier les logs dans /logs/ pour voir les détails\n";
|
||||||
132
api/tests/test_username_validation.php
Executable file
132
api/tests/test_username_validation.php
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script de test pour la validation ultra-souple des usernames
|
||||||
|
* Teste les nouvelles règles : 8-30 caractères UTF-8, tous caractères acceptés
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
$apiUrl = 'http://localhost/api';
|
||||||
|
$sessionId = ''; // À remplir avec un session_id valide
|
||||||
|
|
||||||
|
// Couleurs pour la sortie
|
||||||
|
$red = "\033[31m";
|
||||||
|
$green = "\033[32m";
|
||||||
|
$yellow = "\033[33m";
|
||||||
|
$blue = "\033[34m";
|
||||||
|
$reset = "\033[0m";
|
||||||
|
|
||||||
|
function testRequest($endpoint, $method, $data = null, $sessionId = '') {
|
||||||
|
$ch = curl_init($endpoint);
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||||
|
|
||||||
|
if ($sessionId) {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json; charset=UTF-8',
|
||||||
|
'Authorization: Bearer ' . $sessionId
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json; charset=UTF-8']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => $httpCode,
|
||||||
|
'body' => json_decode($response, true) ?: $response
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $blue . "=== Test de validation ultra-souple des usernames ===" . $reset . "\n";
|
||||||
|
echo "Nouvelles règles : 8-30 caractères UTF-8, tous caractères acceptés\n\n";
|
||||||
|
|
||||||
|
// Cas qui doivent ÉCHOUER (trop court ou trop long)
|
||||||
|
$testsFail = [
|
||||||
|
['username' => 'abc', 'desc' => 'Trop court (3 car.)'],
|
||||||
|
['username' => '1234567', 'desc' => 'Trop court (7 car.)'],
|
||||||
|
['username' => str_repeat('a', 31), 'desc' => 'Trop long (31 car.)'],
|
||||||
|
['username' => str_repeat('😀', 31), 'desc' => 'Trop long (31 émojis)'],
|
||||||
|
['username' => ' ', 'desc' => 'Espaces seulement (devient vide après trim)'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Cas qui doivent RÉUSSIR
|
||||||
|
$testsSuccess = [
|
||||||
|
['username' => 'Jean-Pierre', 'desc' => 'Nom avec tiret et majuscules'],
|
||||||
|
['username' => '12345678', 'desc' => 'Chiffres seulement (8 car.)'],
|
||||||
|
['username' => 'user@company', 'desc' => 'Avec arobase'],
|
||||||
|
['username' => 'Marie 2024', 'desc' => 'Avec espace'],
|
||||||
|
['username' => 'josé.garcía', 'desc' => 'Avec accents'],
|
||||||
|
['username' => '用户2024', 'desc' => 'Caractères chinois'],
|
||||||
|
['username' => 'مستخدم123', 'desc' => 'Caractères arabes'],
|
||||||
|
['username' => 'Admin_#123!', 'desc' => 'Caractères spéciaux'],
|
||||||
|
['username' => '😀🎉Party2024', 'desc' => 'Avec émojis'],
|
||||||
|
['username' => 'Пользователь', 'desc' => 'Cyrillique'],
|
||||||
|
['username' => ' espacé ', 'desc' => 'Espaces (seront trimés)'],
|
||||||
|
['username' => 'a' . str_repeat('b', 28) . 'c', 'desc' => 'Exactement 30 car.'],
|
||||||
|
];
|
||||||
|
|
||||||
|
echo $yellow . "Tests qui doivent ÉCHOUER :" . $reset . "\n\n";
|
||||||
|
|
||||||
|
foreach ($testsFail as $index => $test) {
|
||||||
|
echo "Test " . ($index + 1) . ": " . $test['desc'] . "\n";
|
||||||
|
echo "Username: \"" . $test['username'] . "\"\n";
|
||||||
|
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => $test['username']], $sessionId);
|
||||||
|
|
||||||
|
$isExpectedFailure = ($result['code'] == 400);
|
||||||
|
echo "Code HTTP: " . ($isExpectedFailure ? $green : $red) . $result['code'] . $reset;
|
||||||
|
echo " - " . ($isExpectedFailure ? $green . "✓ OK" : $red . "✗ ERREUR") . $reset . "\n";
|
||||||
|
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
if (isset($result['body']['details'])) {
|
||||||
|
echo "Détails: " . $result['body']['details'] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $yellow . "Tests qui doivent RÉUSSIR :" . $reset . "\n\n";
|
||||||
|
|
||||||
|
foreach ($testsSuccess as $index => $test) {
|
||||||
|
echo "Test " . ($index + 1) . ": " . $test['desc'] . "\n";
|
||||||
|
echo "Username: \"" . $test['username'] . "\"\n";
|
||||||
|
echo "Longueur: " . mb_strlen(trim($test['username']), 'UTF-8') . " caractères\n";
|
||||||
|
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => $test['username']], $sessionId);
|
||||||
|
|
||||||
|
$isExpectedSuccess = ($result['code'] == 200);
|
||||||
|
echo "Code HTTP: " . ($isExpectedSuccess ? $green : $red) . $result['code'] . $reset;
|
||||||
|
echo " - " . ($isExpectedSuccess ? $green . "✓ OK" : $red . "✗ ERREUR") . $reset . "\n";
|
||||||
|
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
if ($result['code'] == 200) {
|
||||||
|
echo "Disponible: " . ($result['body']['available'] ? $green . "Oui" : $yellow . "Non (déjà pris)") . $reset . "\n";
|
||||||
|
} else {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
if (isset($result['body']['details'])) {
|
||||||
|
echo "Détails: " . $result['body']['details'] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $blue . "=== Résumé des nouvelles règles ===" . $reset . "\n";
|
||||||
|
echo "✓ Minimum : 8 caractères UTF-8\n";
|
||||||
|
echo "✓ Maximum : 30 caractères UTF-8\n";
|
||||||
|
echo "✓ Tous caractères acceptés (lettres, chiffres, espaces, émojis, accents, etc.)\n";
|
||||||
|
echo "✓ Trim automatique des espaces début/fin\n";
|
||||||
|
echo "✓ Sensible à la casse (Jean ≠ jean)\n";
|
||||||
|
echo "✓ Unicité vérifiée dans toute la base\n\n";
|
||||||
|
|
||||||
|
echo $yellow . "Note: N'oubliez pas d'exécuter le script SQL de migration !" . $reset . "\n";
|
||||||
|
echo "scripts/sql/migration_username_utf8_support.sql\n";
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/build_daemon-4.0.4/lib/fake.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/build_daemon-4.0.4/lib/fake.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/build_runner-2.5.4/lib/fake.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/build_runner-2.4.13/lib/fake.dart
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
// @dart=3.6
|
|
||||||
// ignore_for_file: directives_ordering
|
// ignore_for_file: directives_ordering
|
||||||
// build_runner >=2.4.16
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
import 'package:build_runner_core/build_runner_core.dart' as _i1;
|
import 'package:build_runner_core/build_runner_core.dart' as _i1;
|
||||||
import 'package:hive_generator/hive_generator.dart' as _i2;
|
import 'package:hive_generator/hive_generator.dart' as _i2;
|
||||||
import 'package:source_gen/builder.dart' as _i3;
|
import 'package:source_gen/builder.dart' as _i3;
|
||||||
import 'dart:isolate' as _i4;
|
import 'package:build_resolvers/builder.dart' as _i4;
|
||||||
import 'package:build_runner/src/build_script_generate/build_process_state.dart'
|
import 'dart:isolate' as _i5;
|
||||||
as _i5;
|
|
||||||
import 'package:build_runner/build_runner.dart' as _i6;
|
import 'package:build_runner/build_runner.dart' as _i6;
|
||||||
import 'dart:io' as _i7;
|
import 'dart:io' as _i7;
|
||||||
|
|
||||||
@@ -26,6 +23,18 @@ final _builders = <_i1.BuilderApplication>[
|
|||||||
hideOutput: false,
|
hideOutput: false,
|
||||||
appliesBuilders: const [r'source_gen:part_cleanup'],
|
appliesBuilders: const [r'source_gen:part_cleanup'],
|
||||||
),
|
),
|
||||||
|
_i1.apply(
|
||||||
|
r'build_resolvers:transitive_digests',
|
||||||
|
[_i4.transitiveDigestsBuilder],
|
||||||
|
_i1.toAllPackages(),
|
||||||
|
isOptional: true,
|
||||||
|
hideOutput: true,
|
||||||
|
appliesBuilders: const [r'build_resolvers:transitive_digest_cleanup'],
|
||||||
|
),
|
||||||
|
_i1.applyPostProcess(
|
||||||
|
r'build_resolvers:transitive_digest_cleanup',
|
||||||
|
_i4.transitiveDigestCleanup,
|
||||||
|
),
|
||||||
_i1.applyPostProcess(
|
_i1.applyPostProcess(
|
||||||
r'source_gen:part_cleanup',
|
r'source_gen:part_cleanup',
|
||||||
_i3.partCleanup,
|
_i3.partCleanup,
|
||||||
@@ -33,13 +42,12 @@ final _builders = <_i1.BuilderApplication>[
|
|||||||
];
|
];
|
||||||
void main(
|
void main(
|
||||||
List<String> args, [
|
List<String> args, [
|
||||||
_i4.SendPort? sendPort,
|
_i5.SendPort? sendPort,
|
||||||
]) async {
|
]) async {
|
||||||
await _i5.buildProcessState.receive(sendPort);
|
var result = await _i6.run(
|
||||||
_i5.buildProcessState.isolateExitCode = await _i6.run(
|
|
||||||
args,
|
args,
|
||||||
_builders,
|
_builders,
|
||||||
);
|
);
|
||||||
_i7.exitCode = _i5.buildProcessState.isolateExitCode!;
|
sendPort?.send(result);
|
||||||
await _i5.buildProcessState.send(sendPort);
|
_i7.exitCode = result;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>5z<EFBFBD><EFBFBD><EFBFBD>k<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<EFBFBD>ũ<EFBFBD><EFBFBD><0C><><EFBFBD>U/!<21><>W<EFBFBD>
|
||||||
Binary file not shown.
@@ -1,55 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class AnonymousUserModelAdapter extends TypeAdapter<AnonymousUserModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 23;
|
|
||||||
|
|
||||||
@override
|
|
||||||
AnonymousUserModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return AnonymousUserModel(
|
|
||||||
id: fields[0] as String,
|
|
||||||
deviceId: fields[1] as String,
|
|
||||||
name: fields[2] as String?,
|
|
||||||
email: fields[3] as String?,
|
|
||||||
createdAt: fields[4] as DateTime,
|
|
||||||
convertedToUserId: fields[5] as String?,
|
|
||||||
metadata: (fields[6] as Map?)?.cast<String, dynamic>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, AnonymousUserModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(7)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.deviceId)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.name)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.email)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.createdAt)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.convertedToUserId)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is AnonymousUserModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class AudienceTargetModelAdapter extends TypeAdapter<AudienceTargetModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 24;
|
|
||||||
|
|
||||||
@override
|
|
||||||
AudienceTargetModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return AudienceTargetModel(
|
|
||||||
id: fields[0] as String,
|
|
||||||
conversationId: fields[1] as String,
|
|
||||||
targetType: fields[2] as String,
|
|
||||||
targetId: fields[3] as String?,
|
|
||||||
createdAt: fields[4] as DateTime,
|
|
||||||
roleFilter: fields[5] as String?,
|
|
||||||
entityFilter: fields[6] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, AudienceTargetModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(7)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.conversationId)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.targetType)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.targetId)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.createdAt)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.roleFilter)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.entityFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is AudienceTargetModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -2,57 +2,57 @@
|
|||||||
// TypeAdapterGenerator
|
// TypeAdapterGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
class MessageModelAdapter extends TypeAdapter<MessageModel> {
|
class MessageAdapter extends TypeAdapter<Message> {
|
||||||
@override
|
@override
|
||||||
final int typeId = 21;
|
final int typeId = 51;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MessageModel read(BinaryReader reader) {
|
Message read(BinaryReader reader) {
|
||||||
final numOfFields = reader.readByte();
|
final numOfFields = reader.readByte();
|
||||||
final fields = <int, dynamic>{
|
final fields = <int, dynamic>{
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
};
|
};
|
||||||
return MessageModel(
|
return Message(
|
||||||
id: fields[0] as String,
|
id: fields[0] as String,
|
||||||
conversationId: fields[1] as String,
|
roomId: fields[1] as String,
|
||||||
senderId: fields[2] as String?,
|
content: fields[2] as String,
|
||||||
senderType: fields[3] as String,
|
senderId: fields[3] as int,
|
||||||
content: fields[4] as String,
|
senderName: fields[4] as String,
|
||||||
contentType: fields[5] as String,
|
sentAt: fields[5] as DateTime,
|
||||||
createdAt: fields[6] as DateTime,
|
isMe: fields[6] as bool,
|
||||||
deliveredAt: fields[7] as DateTime?,
|
isRead: fields[7] as bool,
|
||||||
readAt: fields[8] as DateTime?,
|
senderFirstName: fields[8] as String?,
|
||||||
status: fields[9] as String,
|
readCount: fields[9] as int?,
|
||||||
isAnnouncement: fields[10] as bool,
|
isSynced: fields[10] as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, MessageModel obj) {
|
void write(BinaryWriter writer, Message obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(11)
|
..writeByte(11)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.id)
|
..write(obj.id)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
..write(obj.conversationId)
|
..write(obj.roomId)
|
||||||
..writeByte(2)
|
..writeByte(2)
|
||||||
..write(obj.senderId)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.senderType)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.content)
|
..write(obj.content)
|
||||||
|
..writeByte(3)
|
||||||
|
..write(obj.senderId)
|
||||||
|
..writeByte(4)
|
||||||
|
..write(obj.senderName)
|
||||||
..writeByte(5)
|
..writeByte(5)
|
||||||
..write(obj.contentType)
|
..write(obj.sentAt)
|
||||||
..writeByte(6)
|
..writeByte(6)
|
||||||
..write(obj.createdAt)
|
..write(obj.isMe)
|
||||||
..writeByte(7)
|
..writeByte(7)
|
||||||
..write(obj.deliveredAt)
|
..write(obj.isRead)
|
||||||
..writeByte(8)
|
..writeByte(8)
|
||||||
..write(obj.readAt)
|
..write(obj.senderFirstName)
|
||||||
..writeByte(9)
|
..writeByte(9)
|
||||||
..write(obj.status)
|
..write(obj.readCount)
|
||||||
..writeByte(10)
|
..writeByte(10)
|
||||||
..write(obj.isAnnouncement);
|
..write(obj.isSynced);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -61,7 +61,7 @@ class MessageModelAdapter extends TypeAdapter<MessageModel> {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is MessageModelAdapter &&
|
other is MessageAdapter &&
|
||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
typeId == other.typeId;
|
typeId == other.typeId;
|
||||||
}
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class NotificationSettingsAdapter extends TypeAdapter<NotificationSettings> {
|
|
||||||
@override
|
|
||||||
final int typeId = 25;
|
|
||||||
|
|
||||||
@override
|
|
||||||
NotificationSettings read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return NotificationSettings(
|
|
||||||
enableNotifications: fields[0] as bool,
|
|
||||||
soundEnabled: fields[1] as bool,
|
|
||||||
vibrationEnabled: fields[2] as bool,
|
|
||||||
mutedConversations: (fields[3] as List).cast<String>(),
|
|
||||||
showPreview: fields[4] as bool,
|
|
||||||
conversationNotifications: (fields[5] as Map).cast<String, bool>(),
|
|
||||||
doNotDisturb: fields[6] as bool,
|
|
||||||
doNotDisturbStart: fields[7] as DateTime?,
|
|
||||||
doNotDisturbEnd: fields[8] as DateTime?,
|
|
||||||
deviceToken: fields[9] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, NotificationSettings obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(10)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.enableNotifications)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.soundEnabled)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.vibrationEnabled)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.mutedConversations)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.showPreview)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.conversationNotifications)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.doNotDisturb)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.doNotDisturbStart)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.doNotDisturbEnd)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.deviceToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is NotificationSettingsAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class ParticipantModelAdapter extends TypeAdapter<ParticipantModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 22;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ParticipantModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return ParticipantModel(
|
|
||||||
id: fields[0] as String,
|
|
||||||
conversationId: fields[1] as String,
|
|
||||||
userId: fields[2] as String?,
|
|
||||||
anonymousId: fields[3] as String?,
|
|
||||||
role: fields[4] as String,
|
|
||||||
joinedAt: fields[5] as DateTime,
|
|
||||||
lastReadMessageId: fields[6] as String?,
|
|
||||||
viaTarget: fields[7] as bool,
|
|
||||||
canReply: fields[8] as bool?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, ParticipantModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(9)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.conversationId)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.userId)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.anonymousId)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.role)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.joinedAt)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.lastReadMessageId)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.viaTarget)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.canReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is ParticipantModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -2,54 +2,59 @@
|
|||||||
// TypeAdapterGenerator
|
// TypeAdapterGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
class ConversationModelAdapter extends TypeAdapter<ConversationModel> {
|
class RoomAdapter extends TypeAdapter<Room> {
|
||||||
@override
|
@override
|
||||||
final int typeId = 20;
|
final int typeId = 50;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConversationModel read(BinaryReader reader) {
|
Room read(BinaryReader reader) {
|
||||||
final numOfFields = reader.readByte();
|
final numOfFields = reader.readByte();
|
||||||
final fields = <int, dynamic>{
|
final fields = <int, dynamic>{
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
};
|
};
|
||||||
return ConversationModel(
|
return Room(
|
||||||
id: fields[0] as String,
|
id: fields[0] as String,
|
||||||
type: fields[1] as String,
|
title: fields[1] as String,
|
||||||
title: fields[2] as String?,
|
type: fields[2] as String,
|
||||||
createdAt: fields[3] as DateTime,
|
createdAt: fields[3] as DateTime,
|
||||||
updatedAt: fields[4] as DateTime,
|
lastMessage: fields[4] as String?,
|
||||||
participants: (fields[5] as List).cast<ParticipantModel>(),
|
lastMessageAt: fields[5] as DateTime?,
|
||||||
isSynced: fields[6] as bool,
|
unreadCount: fields[6] as int,
|
||||||
replyPermission: fields[7] as String,
|
recentMessages: (fields[7] as List?)
|
||||||
isPinned: fields[8] as bool,
|
?.map((dynamic e) => (e as Map).cast<String, dynamic>())
|
||||||
expiryDate: fields[9] as DateTime?,
|
?.toList(),
|
||||||
|
updatedAt: fields[8] as DateTime?,
|
||||||
|
createdBy: fields[9] as int?,
|
||||||
|
isSynced: fields[10] as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, ConversationModel obj) {
|
void write(BinaryWriter writer, Room obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(10)
|
..writeByte(11)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.id)
|
..write(obj.id)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
..write(obj.type)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.title)
|
..write(obj.title)
|
||||||
|
..writeByte(2)
|
||||||
|
..write(obj.type)
|
||||||
..writeByte(3)
|
..writeByte(3)
|
||||||
..write(obj.createdAt)
|
..write(obj.createdAt)
|
||||||
..writeByte(4)
|
..writeByte(4)
|
||||||
..write(obj.updatedAt)
|
..write(obj.lastMessage)
|
||||||
..writeByte(5)
|
..writeByte(5)
|
||||||
..write(obj.participants)
|
..write(obj.lastMessageAt)
|
||||||
..writeByte(6)
|
..writeByte(6)
|
||||||
..write(obj.isSynced)
|
..write(obj.unreadCount)
|
||||||
..writeByte(7)
|
..writeByte(7)
|
||||||
..write(obj.replyPermission)
|
..write(obj.recentMessages)
|
||||||
..writeByte(8)
|
..writeByte(8)
|
||||||
..write(obj.isPinned)
|
..write(obj.updatedAt)
|
||||||
..writeByte(9)
|
..writeByte(9)
|
||||||
..write(obj.expiryDate);
|
..write(obj.createdBy)
|
||||||
|
..writeByte(10)
|
||||||
|
..write(obj.isSynced);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -58,7 +63,7 @@ class ConversationModelAdapter extends TypeAdapter<ConversationModel> {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is ConversationModelAdapter &&
|
other is RoomAdapter &&
|
||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
typeId == other.typeId;
|
typeId == other.typeId;
|
||||||
}
|
}
|
||||||
@@ -38,13 +38,14 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
|
|||||||
chkMdpManuel: fields[22] as bool,
|
chkMdpManuel: fields[22] as bool,
|
||||||
chkUsernameManuel: fields[23] as bool,
|
chkUsernameManuel: fields[23] as bool,
|
||||||
logoBase64: fields[24] as String?,
|
logoBase64: fields[24] as String?,
|
||||||
|
chkUserDeletePass: fields[25] as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, AmicaleModel obj) {
|
void write(BinaryWriter writer, AmicaleModel obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(25)
|
..writeByte(26)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.id)
|
..write(obj.id)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -94,7 +95,9 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
|
|||||||
..writeByte(23)
|
..writeByte(23)
|
||||||
..write(obj.chkUsernameManuel)
|
..write(obj.chkUsernameManuel)
|
||||||
..writeByte(24)
|
..writeByte(24)
|
||||||
..write(obj.logoBase64);
|
..write(obj.logoBase64)
|
||||||
|
..writeByte(25)
|
||||||
|
..write(obj.chkUserDeletePass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -37,13 +37,14 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
|
|||||||
updatedAt: fields[21] as DateTime?,
|
updatedAt: fields[21] as DateTime?,
|
||||||
chkMdpManuel: fields[22] as bool?,
|
chkMdpManuel: fields[22] as bool?,
|
||||||
chkUsernameManuel: fields[23] as bool?,
|
chkUsernameManuel: fields[23] as bool?,
|
||||||
|
chkUserDeletePass: fields[24] as bool?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, ClientModel obj) {
|
void write(BinaryWriter writer, ClientModel obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(24)
|
..writeByte(25)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.id)
|
..write(obj.id)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -91,7 +92,9 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
|
|||||||
..writeByte(22)
|
..writeByte(22)
|
||||||
..write(obj.chkMdpManuel)
|
..write(obj.chkMdpManuel)
|
||||||
..writeByte(23)
|
..writeByte(23)
|
||||||
..write(obj.chkUsernameManuel);
|
..write(obj.chkUsernameManuel)
|
||||||
|
..writeByte(24)
|
||||||
|
..write(obj.chkUserDeletePass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// **************************************************************************
|
||||||
|
// TypeAdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class PendingRequestAdapter extends TypeAdapter<PendingRequest> {
|
||||||
|
@override
|
||||||
|
final int typeId = 100;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PendingRequest read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return PendingRequest(
|
||||||
|
id: fields[0] as String,
|
||||||
|
method: fields[1] as String,
|
||||||
|
path: fields[2] as String,
|
||||||
|
data: (fields[3] as Map?)?.cast<String, dynamic>(),
|
||||||
|
queryParams: (fields[4] as Map?)?.cast<String, dynamic>(),
|
||||||
|
createdAt: fields[5] as DateTime,
|
||||||
|
tempId: fields[6] as String?,
|
||||||
|
context: fields[7] as String,
|
||||||
|
retryCount: fields[8] as int,
|
||||||
|
errorMessage: fields[9] as String?,
|
||||||
|
metadata: (fields[10] as Map?)?.cast<String, dynamic>(),
|
||||||
|
priority: fields[11] as int,
|
||||||
|
headers: (fields[12] as Map?)?.cast<String, String>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, PendingRequest obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(13)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.id)
|
||||||
|
..writeByte(1)
|
||||||
|
..write(obj.method)
|
||||||
|
..writeByte(2)
|
||||||
|
..write(obj.path)
|
||||||
|
..writeByte(3)
|
||||||
|
..write(obj.data)
|
||||||
|
..writeByte(4)
|
||||||
|
..write(obj.queryParams)
|
||||||
|
..writeByte(5)
|
||||||
|
..write(obj.createdAt)
|
||||||
|
..writeByte(6)
|
||||||
|
..write(obj.tempId)
|
||||||
|
..writeByte(7)
|
||||||
|
..write(obj.context)
|
||||||
|
..writeByte(8)
|
||||||
|
..write(obj.retryCount)
|
||||||
|
..writeByte(9)
|
||||||
|
..write(obj.errorMessage)
|
||||||
|
..writeByte(10)
|
||||||
|
..write(obj.metadata)
|
||||||
|
..writeByte(11)
|
||||||
|
..write(obj.priority)
|
||||||
|
..writeByte(12)
|
||||||
|
..write(obj.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is PendingRequestAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
C<EFBFBD><EFBFBD><EFBFBD>F}<7D><EFBFBD>7<><37><EFBFBD><EFBFBD>9
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<EFBFBD><EFBFBD>E>`<60>e0<65>sl<73><6C><0C>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Q<EFBFBD>;<14><><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
<EFBFBD>)<29>j<EFBFBD>
|
||||||
Binary file not shown.
@@ -1 +1 @@
|
|||||||
{"sdk":"3.8.1 (stable) (Wed May 28 00:47:25 2025 -0700) on \"linux_x64\"","analyzer":"/home/pierre/.pub-cache/hosted/pub.dev/analyzer-6.11.0","build_resolvers":"/home/pierre/.pub-cache/hosted/pub.dev/build_resolvers-2.5.4"}
|
{"sdk":"3.9.0 (stable) (Mon Aug 11 07:58:10 2025 -0700) on \"linux_x64\"","analyzer":"/home/pierre/.pub-cache/hosted/pub.dev/analyzer-6.4.1","build_resolvers":"/home/pierre/.pub-cache/hosted/pub.dev/build_resolvers-2.4.2"}
|
||||||
@@ -10,6 +10,7 @@ import 'package:connectivity_plus/src/connectivity_plus_web.dart';
|
|||||||
import 'package:geolocator_web/geolocator_web.dart';
|
import 'package:geolocator_web/geolocator_web.dart';
|
||||||
import 'package:image_picker_for_web/image_picker_for_web.dart';
|
import 'package:image_picker_for_web/image_picker_for_web.dart';
|
||||||
import 'package:package_info_plus/src/package_info_plus_web.dart';
|
import 'package:package_info_plus/src/package_info_plus_web.dart';
|
||||||
|
import 'package:sensors_plus/src/sensors_plus_web.dart';
|
||||||
import 'package:shared_preferences_web/shared_preferences_web.dart';
|
import 'package:shared_preferences_web/shared_preferences_web.dart';
|
||||||
import 'package:url_launcher_web/url_launcher_web.dart';
|
import 'package:url_launcher_web/url_launcher_web.dart';
|
||||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||||
@@ -20,6 +21,7 @@ void registerPlugins([final Registrar? pluginRegistrar]) {
|
|||||||
GeolocatorPlugin.registerWith(registrar);
|
GeolocatorPlugin.registerWith(registrar);
|
||||||
ImagePickerPlugin.registerWith(registrar);
|
ImagePickerPlugin.registerWith(registrar);
|
||||||
PackageInfoPlusWebPlugin.registerWith(registrar);
|
PackageInfoPlusWebPlugin.registerWith(registrar);
|
||||||
|
WebSensorsPlugin.registerWith(registrar);
|
||||||
SharedPreferencesPlugin.registerWith(registrar);
|
SharedPreferencesPlugin.registerWith(registrar);
|
||||||
UrlLauncherPlugin.registerWith(registrar);
|
UrlLauncherPlugin.registerWith(registrar);
|
||||||
registrar.registerMessageHandler();
|
registrar.registerMessageHandler();
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -79,9 +79,9 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_se
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/unmodifiable_wrappers.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/unmodifiable_wrappers.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/wrappers.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/wrappers.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.4/lib/connectivity_plus.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/lib/connectivity_plus.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.4/lib/src/connectivity_plus_web.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/lib/src/connectivity_plus_web.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.4/lib/src/web/dart_html_connectivity_plugin.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/lib/src/web/dart_html_connectivity_plugin.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/connectivity_plus_platform_interface.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/connectivity_plus_platform_interface.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/method_channel_connectivity.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/method_channel_connectivity.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/src/enums.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/src/enums.dart
|
||||||
@@ -164,70 +164,70 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/int32.dart
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/int64.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/int64.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/intx.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/intx.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/utilities.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/utilities.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/fl_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/fl_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_extensions.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_extensions.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_widgets.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_widgets.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/scale_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/scale_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/side_titles/side_titles_flex.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/side_titles/side_titles_flex.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/side_titles/side_titles_widget.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/side_titles/side_titles_widget.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/transformation_config.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/transformation_config.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/base_chart/base_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/base_chart/base_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/base_chart/base_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/base_chart/base_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/base_chart/fl_touch_event.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/base_chart/fl_touch_event.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/base_chart/render_base_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/base_chart/render_base_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/custom_interactive_viewer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/custom_interactive_viewer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/line.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/line.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/bar_chart_data_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/bar_chart_data_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/border_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/border_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/color_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/color_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/edge_insets_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/edge_insets_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/fl_border_data_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/fl_border_data_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/fl_titles_data_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/fl_titles_data_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/gradient_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/gradient_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/paint_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/paint_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/path_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/path_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/rrect_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/rrect_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/side_titles_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/side_titles_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/size_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/size_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/text_align_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/text_align_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/utils/canvas_wrapper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/utils/canvas_wrapper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/utils/lerp.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/utils/lerp.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/utils/path_drawing/dash_path.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/utils/path_drawing/dash_path.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/utils/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/utils/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/flutter_map.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/flutter_map.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/src/geo/crs.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/src/geo/crs.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/src/geo/latlng_bounds.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/src/geo/latlng_bounds.dart
|
||||||
@@ -369,27 +369,27 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/html_
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/permissions_manager.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/permissions_manager.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/web_settings.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/web_settings.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/go_router.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/go_router.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/builder.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/builder.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/configuration.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/configuration.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/delegate.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/delegate.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/information_provider.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/information_provider.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/logging.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/logging.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/match.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/match.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/custom_parameter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/custom_parameter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/error_screen.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/error_screen.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/errors.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/errors.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/extensions.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/extensions.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/inherited_router.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/inherited_router.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/pages/cupertino.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/pages/cupertino.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/pages/custom_transition_page.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/pages/custom_transition_page.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/pages/material.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/pages/material.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/parser.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/parser.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/path_utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/path_utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/route.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/route.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/route_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/route_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/router.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/router.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/state.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/state.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/hive.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/hive.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/big_int_adapter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/big_int_adapter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/date_time_adapter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/date_time_adapter.dart
|
||||||
@@ -502,28 +502,29 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/http_dat
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/media_type.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/media_type.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/lib/image_picker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.2.0/lib/image_picker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/image_picker_for_web.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/image_picker_for_web.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/image_resizer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer_utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/image_resizer_utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/pkg_web_tweaks.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/pkg_web_tweaks.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/image_picker_platform_interface.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/image_picker_platform_interface.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/method_channel/method_channel_image_picker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/method_channel/method_channel_image_picker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/platform_interface/image_picker_platform.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/platform_interface/image_picker_platform.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/camera_delegate.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/camera_delegate.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/camera_device.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/camera_device.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/image_options.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/image_options.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/image_source.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/image_source.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/lost_data_response.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/lost_data_response.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/media_options.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/media_options.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/media_selection_type.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/media_selection_type.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/multi_image_picker_options.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/multi_image_picker_options.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/base.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/multi_video_picker_options.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/html.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/picked_file/base.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/lost_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/picked_file/html.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/picked_file.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/picked_file/lost_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/retrieve_type.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/picked_file/picked_file.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/types.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/retrieve_type.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/types.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbol_data_custom.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbol_data_custom.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbols.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbols.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/intl.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/intl.dart
|
||||||
@@ -652,12 +653,12 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/magic_number.da
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_multipart_transformer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_multipart_transformer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_shared.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_shared.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_type.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_type.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/package_info_plus.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/lib/package_info_plus.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_linux.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/lib/src/package_info_plus_linux.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_web.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/lib/src/package_info_plus_web.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.0/lib/method_channel_package_info.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.1/lib/method_channel_package_info.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.0/lib/package_info_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.1/lib/package_info_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.0/lib/package_info_platform_interface.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.1/lib/package_info_platform_interface.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/path.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/path.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/characters.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/characters.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/context.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/context.dart
|
||||||
@@ -736,6 +737,19 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projection
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/utm.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/utm.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/sensors_plus.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/sensors_plus_web.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/web_sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/web_sensors_interop.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/sensors_plus_platform_interface.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/accelerometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/barometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/gyroscope_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/magnetometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/method_channel_sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/sensor_interval.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/user_accelerometer_event.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/shared_preferences.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/shared_preferences.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_async.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_async.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_devtools_extension_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_devtools_extension_data.dart
|
||||||
@@ -773,128 +787,128 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/span_
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/charts.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/charts.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/category_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/category_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/datetime_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/datetime_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/datetime_category_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/datetime_category_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/logarithmic_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/logarithmic_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/multi_level_labels.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/multi_level_labels.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/numeric_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/numeric_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/plot_band.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/plot_band.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/base.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/base.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/crosshair.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/behaviors/crosshair.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/trackball.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/behaviors/trackball.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/zooming.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/behaviors/zooming.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/cartesian_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/cartesian_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/circular_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/circular_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/annotation.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/annotation.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/callbacks.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/callbacks.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/chart_point.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/chart_point.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/circular_data_label.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/circular_data_label.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/circular_data_label_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/circular_data_label_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/connector_line.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/connector_line.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/core_legend.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/core_legend.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/core_tooltip.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/core_tooltip.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/data_label.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/data_label.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/element_widget.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/element_widget.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/empty_points.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/empty_points.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/funnel_data_label.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/funnel_data_label.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/interactive_tooltip.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/interactive_tooltip.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/layout_handler.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/layout_handler.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/legend.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/legend.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/marker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/marker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/pyramid_data_label.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/pyramid_data_label.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/title.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/title.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/funnel_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/funnel_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/accumulation_distribution_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/accumulation_distribution_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/atr_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/atr_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/bollinger_bands_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/bollinger_bands_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/ema_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/ema_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/macd_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/macd_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/momentum_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/momentum_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/roc_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/roc_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/rsi_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/rsi_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/sma_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/sma_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/stochastic_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/stochastic_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/technical_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/technical_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/tma_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/tma_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/wma_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/wma_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/behavior.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/interactions/behavior.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/selection.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/interactions/selection.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/tooltip.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/interactions/tooltip.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/pyramid_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/pyramid_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/area_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/area_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/bar_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/bar_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/box_and_whisker_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/box_and_whisker_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/bubble_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/bubble_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/candle_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/candle_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/chart_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/chart_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/column_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/column_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/doughnut_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/doughnut_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/error_bar_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/error_bar_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/fast_line_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/fast_line_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/funnel_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/funnel_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/hilo_open_close_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/hilo_open_close_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/hilo_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/hilo_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/histogram_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/histogram_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/line_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/line_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/pie_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/pie_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/pyramid_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/pyramid_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/radial_bar_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/radial_bar_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/range_area_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/range_area_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/range_column_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/range_column_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/scatter_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/scatter_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/spline_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/spline_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_area100_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_area100_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_area_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_area_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_bar100_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_bar100_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_bar_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_bar_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_column100_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_column100_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_column_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_column_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_line100_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_line100_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_line_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_line_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/step_area_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/step_area_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stepline_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stepline_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/waterfall_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/waterfall_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/trendline/trendline.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/trendline/trendline.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/constants.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/constants.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/enum.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/enum.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/renderer_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/renderer_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/typedef.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/typedef.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/zooming_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/zooming_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/marker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/sparkline/marker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/utils/enum.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/sparkline/utils/enum.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/utils/helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/sparkline/utils/helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/core.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/core.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/localizations.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/localizations.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/calendar/calendar_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/calendar/calendar_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/calendar/hijri_date_time.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/calendar/hijri_date_time.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/localizations/global_localizations.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/localizations/global_localizations.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/slider_controller.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/slider_controller.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/assistview_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/assistview_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/barcodes_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/barcodes_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/calendar_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/calendar_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/charts_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/charts_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/chat_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/chat_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/color_scheme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/color_scheme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/datagrid_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/datagrid_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/datapager_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/datapager_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/daterangepicker_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/daterangepicker_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/gauges_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/gauges_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/maps_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/maps_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/pdfviewer_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/pdfviewer_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/range_selector_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/range_selector_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/range_slider_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/range_slider_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/slider_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/slider_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/spark_charts_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/spark_charts_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/theme_widget.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/theme_widget.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/treemap_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/treemap_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/utils/helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/utils/helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/utils/shape_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/utils/shape_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
|
||||||
@@ -937,57 +951,56 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v7.dart
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v8.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v8.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v8generic.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v8generic.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/validation.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/validation.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/aabb2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/aabb2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/aabb3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/aabb3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/colors.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/colors.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/constants.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/constants.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/error_helpers.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/error_helpers.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/frustum.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/frustum.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/intersection_result.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/intersection_result.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/matrix2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/matrix2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/matrix3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/matrix3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/matrix4.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/matrix4.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/noise.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/noise.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/obb3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/obb3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/opengl.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/opengl.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/plane.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/plane.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/quad.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/quad.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/quaternion.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/quaternion.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/ray.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/ray.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/sphere.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/sphere.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/triangle.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/triangle.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/utilities.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/utilities.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/vector.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/vector.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/vector2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/vector2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/vector3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/vector3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/vector4.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/vector4.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/aabb2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/aabb2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/aabb3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/aabb3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/colors.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/colors.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/constants.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/constants.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/error_helpers.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/error_helpers.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/frustum.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/frustum.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/intersection_result.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/intersection_result.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/matrix2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/matrix2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/matrix3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/matrix3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/matrix4.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/matrix4.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/noise.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/noise.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/obb3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/obb3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/opengl.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/opengl.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/plane.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/plane.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/quad.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/quad.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/quaternion.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/quaternion.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/ray.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/ray.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/sphere.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/sphere.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/triangle.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/triangle.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/utilities.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/utilities.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/vector.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/vector2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/vector3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector4.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/vector4.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/vector_math.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math_64.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/vector_math_64.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/helpers.dart
|
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
|
||||||
@@ -1182,6 +1195,22 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/parser.da
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/process.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/process.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/proj_wkt.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/proj_wkt.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/wkt_parser.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/wkt_parser.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/charcodes.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/equality.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/error_listener.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/loader.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/null_span.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/parser.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/scanner.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/style.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/token.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/utils.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_document.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_exception.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_node.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_node_wrapper.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/yaml.dart
|
||||||
file:///home/pierre/dev/flutter/bin/cache/dart-sdk/lib/libraries.json
|
file:///home/pierre/dev/flutter/bin/cache/dart-sdk/lib/libraries.json
|
||||||
file:///home/pierre/dev/flutter/bin/cache/flutter_web_sdk/kernel/dart2js_platform.dill
|
file:///home/pierre/dev/flutter/bin/cache/flutter_web_sdk/kernel/dart2js_platform.dill
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/animation.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/animation.dart
|
||||||
@@ -1219,6 +1248,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/dialog.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/dialog.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/expansion_tile.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/form_row.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/form_row.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/form_section.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/form_section.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/icon_theme_data.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/icon_theme_data.dart
|
||||||
@@ -1295,6 +1325,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/drag_details.d
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/eager.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/eager.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/events.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/events.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/force_press.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/force_press.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/gesture_details.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/gesture_settings.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/gesture_settings.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/hit_test.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/hit_test.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/long_press.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/long_press.dart
|
||||||
@@ -1359,6 +1390,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/calendar_date_
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/card.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/card.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/card_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/card_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/carousel.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/carousel.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/carousel_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox_list_tile.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox_list_tile.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox_theme.dart
|
||||||
@@ -1389,6 +1421,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/drawer_header.
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/drawer_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/drawer_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu_form_field.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/elevated_button.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/elevated_button.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/elevated_button_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/elevated_button_theme.dart
|
||||||
@@ -1455,6 +1488,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio.dart
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio_list_tile.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio_list_tile.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/range_slider.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/range_slider.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/range_slider_parts.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/refresh_indicator.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/refresh_indicator.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/reorderable_list.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/reorderable_list.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/scaffold.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/scaffold.dart
|
||||||
@@ -1470,6 +1504,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/selectable_tex
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/selection_area.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/selection_area.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/shadows.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/shadows.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_parts.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_value_indicator_shape.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_value_indicator_shape.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/snack_bar.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/snack_bar.dart
|
||||||
@@ -1652,6 +1687,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/raw_keyboard_w
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/raw_keyboard_windows.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/raw_keyboard_windows.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/restoration.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/restoration.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/scribe.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/scribe.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/sensitive_content.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/service_extensions.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/service_extensions.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/spell_check.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/spell_check.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/system_channels.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/system_channels.dart
|
||||||
@@ -1668,6 +1704,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/undo_manager.d
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/web.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/web.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_html_element_view_web.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_html_element_view_web.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_web_browser_detection_web.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_web_image_web.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_web_image_web.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/actions.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/actions.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/adapter.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/adapter.dart
|
||||||
@@ -1757,8 +1794,10 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/platform_view.d
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/pop_scope.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/pop_scope.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/preferred_size.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/preferred_size.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/primary_scroll_controller.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/primary_scroll_controller.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/radio_group.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_menu_anchor.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_menu_anchor.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_radio.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/reorderable_list.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/reorderable_list.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/restoration.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/restoration.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/restoration_properties.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/restoration_properties.dart
|
||||||
@@ -1785,6 +1824,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/scrollbar.dart
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/selectable_region.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/selectable_region.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/selection_container.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/selection_container.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/semantics_debugger.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/semantics_debugger.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/sensitive_content.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/service_extensions.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/service_extensions.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/shared_app_data.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/shared_app_data.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/shortcuts.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/shortcuts.dart
|
||||||
@@ -1827,7 +1867,6 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/view.dart
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/viewport.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/viewport.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/visibility.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/visibility.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_inspector.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_inspector.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_preview.dart
|
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_span.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_span.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_state.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_state.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/will_pop_scope.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/will_pop_scope.dart
|
||||||
@@ -1847,25 +1886,22 @@ file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/navigation/
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart
|
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/plugin_registry.dart
|
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/plugin_registry.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/url_strategy.dart
|
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/url_strategy.dart
|
||||||
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/main.dart
|
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/main.dart
|
||||||
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/web_plugin_registrant.dart
|
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/web_plugin_registrant.dart
|
||||||
file:///home/pierre/dev/geosector/app/.dart_tool/package_config.json
|
file:///home/pierre/dev/geosector/app/.dart_tool/package_config.json
|
||||||
file:///home/pierre/dev/geosector/app/lib/app.dart
|
file:///home/pierre/dev/geosector/app/lib/app.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/anonymous_user_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/chat_module.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/anonymous_user_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/message.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/message.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/room.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/chat_adapters.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/room.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/chat_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page_embedded.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_config_loader.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_info_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/widgets/recipient_selector.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.g.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/widgets/chat_screen.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/widgets/conversations_list.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/constants/app_keys.dart
|
file:///home/pierre/dev/geosector/app/lib/core/constants/app_keys.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.g.dart
|
||||||
@@ -1877,6 +1913,8 @@ file:///home/pierre/dev/geosector/app/lib/core/data/models/operation_model.dart
|
|||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/operation_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/operation_model.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/passage_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/passage_model.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/passage_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/passage_model.g.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/pending_request.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/pending_request.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/region_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/region_model.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/region_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/region_model.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/sector_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/sector_model.dart
|
||||||
@@ -1895,6 +1933,7 @@ file:///home/pierre/dev/geosector/app/lib/core/repositories/sector_repository.da
|
|||||||
file:///home/pierre/dev/geosector/app/lib/core/repositories/user_repository.dart
|
file:///home/pierre/dev/geosector/app/lib/core/repositories/user_repository.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/api_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/api_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/app_info_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/app_info_service.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/core/services/chat_manager.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/connectivity_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/connectivity_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/current_amicale_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/current_amicale_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/current_user_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/current_user_service.dart
|
||||||
@@ -1911,7 +1950,6 @@ file:///home/pierre/dev/geosector/app/lib/core/theme/app_theme.dart
|
|||||||
file:///home/pierre/dev/geosector/app/lib/core/utils/api_exception.dart
|
file:///home/pierre/dev/geosector/app/lib/core/utils/api_exception.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/main.dart
|
file:///home/pierre/dev/geosector/app/lib/main.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_amicale_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_amicale_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_communication_page.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_home_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_home_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_history_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_history_page.dart
|
||||||
@@ -1921,16 +1959,18 @@ file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_statistics_pa
|
|||||||
file:///home/pierre/dev/geosector/app/lib/presentation/auth/login_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/auth/login_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/auth/register_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/auth/register_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/auth/splash_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/auth/splash_page.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/chat/chat_communication_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/dialogs/sector_dialog.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/dialogs/sector_dialog.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_communication_page.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_home_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_home_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_page.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_field_mode_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_history_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_history_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_map_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_map_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_statistics_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_statistics_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_form.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_form.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_row_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_row_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_table_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_table_widget.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/badged_navigation_destination.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/activity_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/activity_chart.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/charts.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/charts.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
|
||||||
@@ -1941,9 +1981,6 @@ file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/passage_ut
|
|||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_data.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_data.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_pie_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_pie_chart.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_summary_card.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_summary_card.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_input.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_messages.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_sidebar.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/connectivity_indicator.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/connectivity_indicator.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_button.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_button.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart
|
||||||
@@ -1951,13 +1988,14 @@ file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_app_bar
|
|||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_layout.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_layout.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/form_section.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/form_section.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/help_dialog.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/help_dialog.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_overlay.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_spin_overlay.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_spin_overlay.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/mapbox_map.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/mapbox_map.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_row_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_row_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_table_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_table_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/operation_form_dialog.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/operation_form_dialog.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passage_form_dialog.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passage_form_dialog.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passages/passage_form.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passage_map_dialog.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passages/passages_list_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passages/passages_list_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/responsive_navigation.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/responsive_navigation.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/sector_distribution_card.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/sector_distribution_card.dart
|
||||||
@@ -2177,6 +2215,7 @@ org-dartlang-sdk:///dart-sdk/lib/web_gl/dart2js/web_gl_dart2js.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine.dart
|
org-dartlang-sdk:///lib/_engine/engine.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/alarm_clock.dart
|
org-dartlang-sdk:///lib/_engine/engine/alarm_clock.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/app_bootstrap.dart
|
org-dartlang-sdk:///lib/_engine/engine/app_bootstrap.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/arena.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/browser_detection.dart
|
org-dartlang-sdk:///lib/_engine/engine/browser_detection.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/canvaskit/canvas.dart
|
org-dartlang-sdk:///lib/_engine/engine/canvaskit/canvas.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/canvaskit/canvaskit_api.dart
|
org-dartlang-sdk:///lib/_engine/engine/canvaskit/canvaskit_api.dart
|
||||||
@@ -2237,6 +2276,7 @@ org-dartlang-sdk:///lib/_engine/engine/js_interop/js_typed_data.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine/key_map.g.dart
|
org-dartlang-sdk:///lib/_engine/engine/key_map.g.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/keyboard_binding.dart
|
org-dartlang-sdk:///lib/_engine/engine/keyboard_binding.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/layers.dart
|
org-dartlang-sdk:///lib/_engine/engine/layers.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/lazy_path.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/mouse/context_menu.dart
|
org-dartlang-sdk:///lib/_engine/engine/mouse/context_menu.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/mouse/cursor.dart
|
org-dartlang-sdk:///lib/_engine/engine/mouse/cursor.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/mouse/prevent_default.dart
|
org-dartlang-sdk:///lib/_engine/engine/mouse/prevent_default.dart
|
||||||
@@ -2257,7 +2297,6 @@ org-dartlang-sdk:///lib/_engine/engine/pointer_converter.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine/profiler.dart
|
org-dartlang-sdk:///lib/_engine/engine/profiler.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/raw_keyboard.dart
|
org-dartlang-sdk:///lib/_engine/engine/raw_keyboard.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/renderer.dart
|
org-dartlang-sdk:///lib/_engine/engine/renderer.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/rrect_renderer.dart
|
|
||||||
org-dartlang-sdk:///lib/_engine/engine/safe_browser_api.dart
|
org-dartlang-sdk:///lib/_engine/engine/safe_browser_api.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/scene_builder.dart
|
org-dartlang-sdk:///lib/_engine/engine/scene_builder.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/scene_painting.dart
|
org-dartlang-sdk:///lib/_engine/engine/scene_painting.dart
|
||||||
@@ -2268,11 +2307,13 @@ org-dartlang-sdk:///lib/_engine/engine/semantics/checkable.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/disable.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/disable.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/expandable.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/expandable.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/focusable.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/focusable.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/semantics/form.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/header.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/header.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/heading.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/heading.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/image.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/image.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/incrementable.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/incrementable.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/label_and_value.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/label_and_value.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/semantics/landmarks.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/link.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/link.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/list.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/list.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/live_region.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/live_region.dart
|
||||||
@@ -2318,6 +2359,7 @@ org-dartlang-sdk:///lib/_engine/engine/view_embedder/flutter_view_manager.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine/view_embedder/global_html_attributes.dart
|
org-dartlang-sdk:///lib/_engine/engine/view_embedder/global_html_attributes.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/view_embedder/hot_restart_cache_handler.dart
|
org-dartlang-sdk:///lib/_engine/engine/view_embedder/hot_restart_cache_handler.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/view_embedder/style_manager.dart
|
org-dartlang-sdk:///lib/_engine/engine/view_embedder/style_manager.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/web_paragraph/paragraph.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/window.dart
|
org-dartlang-sdk:///lib/_engine/engine/window.dart
|
||||||
org-dartlang-sdk:///lib/_skwasm_stub/skwasm_stub.dart
|
org-dartlang-sdk:///lib/_skwasm_stub/skwasm_stub.dart
|
||||||
org-dartlang-sdk:///lib/_skwasm_stub/skwasm_stub/renderer.dart
|
org-dartlang-sdk:///lib/_skwasm_stub/skwasm_stub/renderer.dart
|
||||||
@@ -2344,6 +2386,7 @@ org-dartlang-sdk:///lib/ui/path_metrics.dart
|
|||||||
org-dartlang-sdk:///lib/ui/platform_dispatcher.dart
|
org-dartlang-sdk:///lib/ui/platform_dispatcher.dart
|
||||||
org-dartlang-sdk:///lib/ui/platform_isolate.dart
|
org-dartlang-sdk:///lib/ui/platform_isolate.dart
|
||||||
org-dartlang-sdk:///lib/ui/pointer.dart
|
org-dartlang-sdk:///lib/ui/pointer.dart
|
||||||
|
org-dartlang-sdk:///lib/ui/rsuperellipse_param.dart
|
||||||
org-dartlang-sdk:///lib/ui/semantics.dart
|
org-dartlang-sdk:///lib/ui/semantics.dart
|
||||||
org-dartlang-sdk:///lib/ui/text.dart
|
org-dartlang-sdk:///lib/ui/text.dart
|
||||||
org-dartlang-sdk:///lib/ui/tile_mode.dart
|
org-dartlang-sdk:///lib/ui/tile_mode.dart
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"inputs":["/home/pierre/dev/flutter/bin/cache/engine.stamp","/home/pierre/dev/flutter/bin/cache/engine.stamp","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/main.dart","/home/pierre/dev/geosector/app/.dart_tool/package_config.json"],"outputs":[],"buildKey":"{\"optimizationLevel\":null,\"webRenderer\":\"skwasm\",\"StripWasm\":true,\"minify\":null,\"dryRun\":true,\"SourceMaps\":false}"}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -79,9 +79,9 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_se
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/unmodifiable_wrappers.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/unmodifiable_wrappers.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/wrappers.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/wrappers.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.4/lib/connectivity_plus.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/lib/connectivity_plus.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.4/lib/src/connectivity_plus_web.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/lib/src/connectivity_plus_web.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.4/lib/src/web/dart_html_connectivity_plugin.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/lib/src/web/dart_html_connectivity_plugin.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/connectivity_plus_platform_interface.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/connectivity_plus_platform_interface.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/method_channel_connectivity.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/method_channel_connectivity.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/src/enums.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/src/enums.dart
|
||||||
@@ -164,70 +164,70 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/int32.dart
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/int64.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/int64.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/intx.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/intx.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/utilities.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/src/utilities.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/fl_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/fl_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/bar_chart/bar_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/bar_chart/bar_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_extensions.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_extensions.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/axis_chart_widgets.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/axis_chart_widgets.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/scale_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/scale_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/side_titles/side_titles_flex.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/side_titles/side_titles_flex.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/side_titles/side_titles_widget.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/side_titles/side_titles_widget.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/axis_chart/transformation_config.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/axis_chart/transformation_config.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/base_chart/base_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/base_chart/base_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/base_chart/base_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/base_chart/base_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/base_chart/fl_touch_event.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/base_chart/fl_touch_event.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/base_chart/render_base_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/base_chart/render_base_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/custom_interactive_viewer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/custom_interactive_viewer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/base/line.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/base/line.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/candlestick_chart/candlestick_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/candlestick_chart/candlestick_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/line_chart/line_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/line_chart/line_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/pie_chart/pie_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/pie_chart/pie_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/radar_chart/radar_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/radar_chart/radar_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart_painter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart_painter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/chart/scatter_chart/scatter_chart_renderer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/chart/scatter_chart/scatter_chart_renderer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/bar_chart_data_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/bar_chart_data_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/border_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/border_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/color_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/color_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/edge_insets_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/edge_insets_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/fl_border_data_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/fl_border_data_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/fl_titles_data_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/fl_titles_data_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/gradient_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/gradient_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/paint_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/paint_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/path_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/path_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/rrect_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/rrect_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/side_titles_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/side_titles_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/size_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/size_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/extensions/text_align_extension.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/extensions/text_align_extension.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/utils/canvas_wrapper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/utils/canvas_wrapper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/utils/lerp.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/utils/lerp.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/utils/path_drawing/dash_path.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/utils/path_drawing/dash_path.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.0.0/lib/src/utils/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/fl_chart-1.1.0/lib/src/utils/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/flutter_map.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/flutter_map.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/src/geo/crs.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/src/geo/crs.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/src/geo/latlng_bounds.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1/lib/src/geo/latlng_bounds.dart
|
||||||
@@ -369,27 +369,27 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/html_
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/permissions_manager.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/permissions_manager.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/web_settings.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/web_settings.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/go_router.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/go_router.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/builder.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/builder.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/configuration.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/configuration.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/delegate.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/delegate.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/information_provider.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/information_provider.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/logging.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/logging.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/match.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/match.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/custom_parameter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/custom_parameter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/error_screen.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/error_screen.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/errors.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/errors.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/extensions.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/extensions.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/misc/inherited_router.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/misc/inherited_router.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/pages/cupertino.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/pages/cupertino.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/pages/custom_transition_page.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/pages/custom_transition_page.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/pages/material.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/pages/material.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/parser.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/parser.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/path_utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/path_utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/route.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/route.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/route_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/route_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/router.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/router.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/src/state.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.1/lib/src/state.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/hive.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/hive.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/big_int_adapter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/big_int_adapter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/date_time_adapter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/date_time_adapter.dart
|
||||||
@@ -502,28 +502,29 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/http_dat
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/media_type.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/media_type.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/lib/image_picker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.2.0/lib/image_picker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/image_picker_for_web.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/image_picker_for_web.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/image_resizer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer_utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/image_resizer_utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/pkg_web_tweaks.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/pkg_web_tweaks.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/image_picker_platform_interface.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/image_picker_platform_interface.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/method_channel/method_channel_image_picker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/method_channel/method_channel_image_picker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/platform_interface/image_picker_platform.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/platform_interface/image_picker_platform.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/camera_delegate.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/camera_delegate.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/camera_device.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/camera_device.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/image_options.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/image_options.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/image_source.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/image_source.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/lost_data_response.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/lost_data_response.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/media_options.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/media_options.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/media_selection_type.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/media_selection_type.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/multi_image_picker_options.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/multi_image_picker_options.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/base.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/multi_video_picker_options.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/html.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/picked_file/base.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/lost_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/picked_file/html.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/picked_file.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/picked_file/lost_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/retrieve_type.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/picked_file/picked_file.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/types.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/retrieve_type.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/src/types/types.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbol_data_custom.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbol_data_custom.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbols.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbols.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/intl.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/intl.dart
|
||||||
@@ -652,12 +653,12 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/magic_number.da
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_multipart_transformer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_multipart_transformer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_shared.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_shared.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_type.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_type.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/package_info_plus.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/lib/package_info_plus.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_linux.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/lib/src/package_info_plus_linux.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_web.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/lib/src/package_info_plus_web.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.0/lib/method_channel_package_info.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.1/lib/method_channel_package_info.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.0/lib/package_info_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.1/lib/package_info_data.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.0/lib/package_info_platform_interface.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus_platform_interface-3.2.1/lib/package_info_platform_interface.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/path.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/path.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/characters.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/characters.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/context.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/context.dart
|
||||||
@@ -736,6 +737,19 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projection
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/utm.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/utm.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/sensors_plus.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/sensors_plus_web.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/web_sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/web_sensors_interop.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/sensors_plus_platform_interface.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/accelerometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/barometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/gyroscope_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/magnetometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/method_channel_sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/sensor_interval.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/user_accelerometer_event.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/shared_preferences.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/shared_preferences.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_async.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_async.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_devtools_extension_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_devtools_extension_data.dart
|
||||||
@@ -773,128 +787,128 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/span_
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/charts.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/charts.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/category_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/category_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/datetime_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/datetime_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/datetime_category_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/datetime_category_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/logarithmic_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/logarithmic_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/multi_level_labels.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/multi_level_labels.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/numeric_axis.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/numeric_axis.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/plot_band.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/axis/plot_band.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/base.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/base.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/crosshair.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/behaviors/crosshair.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/trackball.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/behaviors/trackball.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/zooming.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/behaviors/zooming.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/cartesian_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/cartesian_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/circular_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/circular_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/annotation.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/annotation.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/callbacks.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/callbacks.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/chart_point.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/chart_point.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/circular_data_label.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/circular_data_label.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/circular_data_label_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/circular_data_label_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/connector_line.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/connector_line.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/core_legend.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/core_legend.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/core_tooltip.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/core_tooltip.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/data_label.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/data_label.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/element_widget.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/element_widget.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/empty_points.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/empty_points.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/funnel_data_label.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/funnel_data_label.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/interactive_tooltip.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/interactive_tooltip.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/layout_handler.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/layout_handler.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/legend.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/legend.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/marker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/marker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/pyramid_data_label.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/pyramid_data_label.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/title.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/common/title.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/funnel_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/funnel_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/accumulation_distribution_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/accumulation_distribution_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/atr_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/atr_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/bollinger_bands_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/bollinger_bands_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/ema_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/ema_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/macd_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/macd_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/momentum_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/momentum_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/roc_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/roc_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/rsi_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/rsi_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/sma_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/sma_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/stochastic_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/stochastic_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/technical_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/technical_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/tma_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/tma_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/wma_indicator.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/indicators/wma_indicator.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/behavior.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/interactions/behavior.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/selection.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/interactions/selection.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/tooltip.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/interactions/tooltip.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/pyramid_chart.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/pyramid_chart.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/area_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/area_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/bar_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/bar_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/box_and_whisker_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/box_and_whisker_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/bubble_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/bubble_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/candle_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/candle_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/chart_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/chart_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/column_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/column_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/doughnut_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/doughnut_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/error_bar_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/error_bar_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/fast_line_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/fast_line_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/funnel_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/funnel_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/hilo_open_close_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/hilo_open_close_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/hilo_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/hilo_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/histogram_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/histogram_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/line_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/line_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/pie_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/pie_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/pyramid_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/pyramid_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/radial_bar_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/radial_bar_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/range_area_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/range_area_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/range_column_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/range_column_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/scatter_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/scatter_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/spline_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/spline_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_area100_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_area100_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_area_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_area_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_bar100_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_bar100_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_bar_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_bar_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_column100_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_column100_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_column_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_column_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_line100_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_line100_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_line_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stacked_line_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/step_area_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/step_area_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stepline_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/stepline_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/waterfall_series.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/series/waterfall_series.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/trendline/trendline.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/trendline/trendline.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/constants.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/constants.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/enum.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/enum.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/renderer_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/renderer_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/typedef.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/typedef.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/zooming_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/charts/utils/zooming_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/marker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/sparkline/marker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/utils/enum.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/sparkline/utils/enum.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/utils/helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.7/lib/src/sparkline/utils/helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/core.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/core.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/localizations.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/localizations.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/calendar/calendar_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/calendar/calendar_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/calendar/hijri_date_time.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/calendar/hijri_date_time.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/localizations/global_localizations.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/localizations/global_localizations.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/slider_controller.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/slider_controller.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/assistview_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/assistview_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/barcodes_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/barcodes_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/calendar_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/calendar_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/charts_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/charts_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/chat_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/chat_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/color_scheme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/color_scheme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/datagrid_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/datagrid_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/datapager_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/datapager_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/daterangepicker_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/daterangepicker_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/gauges_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/gauges_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/maps_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/maps_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/pdfviewer_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/pdfviewer_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/range_selector_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/range_selector_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/range_slider_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/range_slider_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/slider_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/slider_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/spark_charts_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/spark_charts_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/theme_widget.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/theme_widget.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/treemap_theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/theme/treemap_theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/utils/helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/utils/helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/utils/shape_helper.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/src/utils/shape_helper.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/theme.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.7/lib/theme.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
|
||||||
@@ -937,57 +951,56 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v7.dart
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v8.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v8.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v8generic.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/v8generic.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/validation.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.1/lib/validation.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/aabb2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/aabb2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/aabb3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/aabb3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/colors.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/colors.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/constants.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/constants.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/error_helpers.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/error_helpers.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/frustum.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/frustum.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/intersection_result.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/intersection_result.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/matrix2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/matrix2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/matrix3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/matrix3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/matrix4.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/matrix4.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/noise.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/noise.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/obb3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/obb3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/opengl.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/opengl.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/plane.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/plane.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/quad.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/quad.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/quaternion.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/quaternion.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/ray.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/ray.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/sphere.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/sphere.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/triangle.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/triangle.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/utilities.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/utilities.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/vector.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/vector.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/vector2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/vector2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/vector3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/vector3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math/vector4.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math/vector4.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/aabb2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/aabb2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/aabb3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/aabb3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/colors.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/colors.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/constants.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/constants.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/error_helpers.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/error_helpers.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/frustum.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/frustum.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/intersection_result.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/intersection_result.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/matrix2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/matrix2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/matrix3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/matrix3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/matrix4.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/matrix4.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/noise.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/noise.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/obb3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/obb3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/opengl.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/opengl.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/plane.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/plane.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/quad.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/quad.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/quaternion.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/quaternion.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/ray.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/ray.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/sphere.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/sphere.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/triangle.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/triangle.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/utilities.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/utilities.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/vector.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector2.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/vector2.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector3.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/vector3.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector4.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/src/vector_math_64/vector4.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/vector_math.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math_64.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.2.0/lib/vector_math_64.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/helpers.dart
|
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
|
||||||
@@ -1182,6 +1195,22 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/parser.da
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/process.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/process.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/proj_wkt.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/proj_wkt.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/wkt_parser.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/wkt_parser.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/charcodes.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/equality.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/error_listener.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/loader.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/null_span.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/parser.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/scanner.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/style.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/token.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/utils.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_document.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_exception.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_node.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_node_wrapper.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/yaml.dart
|
||||||
file:///home/pierre/dev/flutter/bin/cache/flutter_web_sdk/kernel/dart2js_platform.dill
|
file:///home/pierre/dev/flutter/bin/cache/flutter_web_sdk/kernel/dart2js_platform.dill
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/animation.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/animation.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/cupertino.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/cupertino.dart
|
||||||
@@ -1218,6 +1247,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/dialog.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/dialog.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/expansion_tile.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/form_row.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/form_row.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/form_section.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/form_section.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/icon_theme_data.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/cupertino/icon_theme_data.dart
|
||||||
@@ -1294,6 +1324,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/drag_details.d
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/eager.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/eager.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/events.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/events.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/force_press.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/force_press.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/gesture_details.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/gesture_settings.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/gesture_settings.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/hit_test.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/hit_test.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/long_press.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/gestures/long_press.dart
|
||||||
@@ -1358,6 +1389,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/calendar_date_
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/card.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/card.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/card_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/card_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/carousel.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/carousel.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/carousel_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox_list_tile.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox_list_tile.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/checkbox_theme.dart
|
||||||
@@ -1388,6 +1420,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/drawer_header.
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/drawer_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/drawer_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu_form_field.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/dropdown_menu_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/elevated_button.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/elevated_button.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/elevated_button_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/elevated_button_theme.dart
|
||||||
@@ -1454,6 +1487,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio.dart
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio_list_tile.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio_list_tile.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/radio_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/range_slider.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/range_slider.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/range_slider_parts.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/refresh_indicator.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/refresh_indicator.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/reorderable_list.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/reorderable_list.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/scaffold.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/scaffold.dart
|
||||||
@@ -1469,6 +1503,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/selectable_tex
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/selection_area.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/selection_area.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/shadows.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/shadows.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_parts.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_theme.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_theme.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_value_indicator_shape.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/slider_value_indicator_shape.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/snack_bar.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/material/snack_bar.dart
|
||||||
@@ -1651,6 +1686,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/raw_keyboard_w
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/raw_keyboard_windows.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/raw_keyboard_windows.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/restoration.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/restoration.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/scribe.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/scribe.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/sensitive_content.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/service_extensions.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/service_extensions.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/spell_check.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/spell_check.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/system_channels.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/system_channels.dart
|
||||||
@@ -1667,6 +1703,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/services/undo_manager.d
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/web.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/web.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_html_element_view_web.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_html_element_view_web.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_web_browser_detection_web.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_web_image_web.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/_web_image_web.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/actions.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/actions.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/adapter.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/adapter.dart
|
||||||
@@ -1756,8 +1793,10 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/platform_view.d
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/pop_scope.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/pop_scope.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/preferred_size.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/preferred_size.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/primary_scroll_controller.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/primary_scroll_controller.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/radio_group.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_menu_anchor.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_menu_anchor.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/raw_radio.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/reorderable_list.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/reorderable_list.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/restoration.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/restoration.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/restoration_properties.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/restoration_properties.dart
|
||||||
@@ -1784,6 +1823,7 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/scrollbar.dart
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/selectable_region.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/selectable_region.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/selection_container.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/selection_container.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/semantics_debugger.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/semantics_debugger.dart
|
||||||
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/sensitive_content.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/service_extensions.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/service_extensions.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/shared_app_data.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/shared_app_data.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/shortcuts.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/shortcuts.dart
|
||||||
@@ -1826,7 +1866,6 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/view.dart
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/viewport.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/viewport.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/visibility.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/visibility.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_inspector.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_inspector.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_preview.dart
|
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_span.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_span.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_state.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_state.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/will_pop_scope.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/will_pop_scope.dart
|
||||||
@@ -1846,25 +1885,22 @@ file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/navigation/
|
|||||||
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart
|
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/plugin_registry.dart
|
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/plugin_registry.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/url_strategy.dart
|
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/url_strategy.dart
|
||||||
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/app.dill
|
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/app.dill
|
||||||
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/main.dart
|
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/main.dart
|
||||||
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/web_plugin_registrant.dart
|
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/web_plugin_registrant.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/app.dart
|
file:///home/pierre/dev/geosector/app/lib/app.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/anonymous_user_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/chat_module.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/anonymous_user_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/message.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/message.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/room.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/chat_adapters.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/room.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/chat_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page_embedded.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_config_loader.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_info_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/widgets/recipient_selector.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.g.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/widgets/chat_screen.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/widgets/conversations_list.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/constants/app_keys.dart
|
file:///home/pierre/dev/geosector/app/lib/core/constants/app_keys.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.g.dart
|
||||||
@@ -1876,6 +1912,8 @@ file:///home/pierre/dev/geosector/app/lib/core/data/models/operation_model.dart
|
|||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/operation_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/operation_model.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/passage_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/passage_model.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/passage_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/passage_model.g.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/pending_request.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/pending_request.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/region_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/region_model.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/region_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/region_model.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/sector_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/sector_model.dart
|
||||||
@@ -1894,6 +1932,7 @@ file:///home/pierre/dev/geosector/app/lib/core/repositories/sector_repository.da
|
|||||||
file:///home/pierre/dev/geosector/app/lib/core/repositories/user_repository.dart
|
file:///home/pierre/dev/geosector/app/lib/core/repositories/user_repository.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/api_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/api_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/app_info_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/app_info_service.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/core/services/chat_manager.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/connectivity_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/connectivity_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/current_amicale_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/current_amicale_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/services/current_user_service.dart
|
file:///home/pierre/dev/geosector/app/lib/core/services/current_user_service.dart
|
||||||
@@ -1910,7 +1949,6 @@ file:///home/pierre/dev/geosector/app/lib/core/theme/app_theme.dart
|
|||||||
file:///home/pierre/dev/geosector/app/lib/core/utils/api_exception.dart
|
file:///home/pierre/dev/geosector/app/lib/core/utils/api_exception.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/main.dart
|
file:///home/pierre/dev/geosector/app/lib/main.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_amicale_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_amicale_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_communication_page.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_home_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_home_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_history_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_history_page.dart
|
||||||
@@ -1920,16 +1958,18 @@ file:///home/pierre/dev/geosector/app/lib/presentation/admin/admin_statistics_pa
|
|||||||
file:///home/pierre/dev/geosector/app/lib/presentation/auth/login_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/auth/login_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/auth/register_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/auth/register_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/auth/splash_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/auth/splash_page.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/chat/chat_communication_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/dialogs/sector_dialog.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/dialogs/sector_dialog.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_communication_page.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_home_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_home_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_page.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_field_mode_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_history_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_history_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_map_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_map_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_statistics_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_statistics_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_form.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_form.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_row_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_row_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_table_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_table_widget.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/badged_navigation_destination.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/activity_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/activity_chart.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/charts.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/charts.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
|
||||||
@@ -1940,9 +1980,6 @@ file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/passage_ut
|
|||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_data.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_data.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_pie_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_pie_chart.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_summary_card.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_summary_card.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_input.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_messages.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_sidebar.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/connectivity_indicator.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/connectivity_indicator.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_button.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_button.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart
|
||||||
@@ -1950,13 +1987,14 @@ file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_app_bar
|
|||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_layout.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_layout.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/form_section.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/form_section.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/help_dialog.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/help_dialog.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_overlay.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_spin_overlay.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_spin_overlay.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/mapbox_map.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/mapbox_map.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_row_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_row_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_table_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_table_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/operation_form_dialog.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/operation_form_dialog.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passage_form_dialog.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passage_form_dialog.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passages/passage_form.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passage_map_dialog.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passages/passages_list_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/passages/passages_list_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/responsive_navigation.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/responsive_navigation.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/sector_distribution_card.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/sector_distribution_card.dart
|
||||||
@@ -2176,6 +2214,7 @@ org-dartlang-sdk:///dart-sdk/lib/web_gl/dart2js/web_gl_dart2js.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine.dart
|
org-dartlang-sdk:///lib/_engine/engine.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/alarm_clock.dart
|
org-dartlang-sdk:///lib/_engine/engine/alarm_clock.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/app_bootstrap.dart
|
org-dartlang-sdk:///lib/_engine/engine/app_bootstrap.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/arena.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/browser_detection.dart
|
org-dartlang-sdk:///lib/_engine/engine/browser_detection.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/canvaskit/canvas.dart
|
org-dartlang-sdk:///lib/_engine/engine/canvaskit/canvas.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/canvaskit/canvaskit_api.dart
|
org-dartlang-sdk:///lib/_engine/engine/canvaskit/canvaskit_api.dart
|
||||||
@@ -2236,6 +2275,7 @@ org-dartlang-sdk:///lib/_engine/engine/js_interop/js_typed_data.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine/key_map.g.dart
|
org-dartlang-sdk:///lib/_engine/engine/key_map.g.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/keyboard_binding.dart
|
org-dartlang-sdk:///lib/_engine/engine/keyboard_binding.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/layers.dart
|
org-dartlang-sdk:///lib/_engine/engine/layers.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/lazy_path.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/mouse/context_menu.dart
|
org-dartlang-sdk:///lib/_engine/engine/mouse/context_menu.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/mouse/cursor.dart
|
org-dartlang-sdk:///lib/_engine/engine/mouse/cursor.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/mouse/prevent_default.dart
|
org-dartlang-sdk:///lib/_engine/engine/mouse/prevent_default.dart
|
||||||
@@ -2256,7 +2296,6 @@ org-dartlang-sdk:///lib/_engine/engine/pointer_converter.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine/profiler.dart
|
org-dartlang-sdk:///lib/_engine/engine/profiler.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/raw_keyboard.dart
|
org-dartlang-sdk:///lib/_engine/engine/raw_keyboard.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/renderer.dart
|
org-dartlang-sdk:///lib/_engine/engine/renderer.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/rrect_renderer.dart
|
|
||||||
org-dartlang-sdk:///lib/_engine/engine/safe_browser_api.dart
|
org-dartlang-sdk:///lib/_engine/engine/safe_browser_api.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/scene_builder.dart
|
org-dartlang-sdk:///lib/_engine/engine/scene_builder.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/scene_painting.dart
|
org-dartlang-sdk:///lib/_engine/engine/scene_painting.dart
|
||||||
@@ -2267,11 +2306,13 @@ org-dartlang-sdk:///lib/_engine/engine/semantics/checkable.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/disable.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/disable.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/expandable.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/expandable.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/focusable.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/focusable.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/semantics/form.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/header.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/header.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/heading.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/heading.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/image.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/image.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/incrementable.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/incrementable.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/label_and_value.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/label_and_value.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/semantics/landmarks.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/link.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/link.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/list.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/list.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/semantics/live_region.dart
|
org-dartlang-sdk:///lib/_engine/engine/semantics/live_region.dart
|
||||||
@@ -2317,6 +2358,7 @@ org-dartlang-sdk:///lib/_engine/engine/view_embedder/flutter_view_manager.dart
|
|||||||
org-dartlang-sdk:///lib/_engine/engine/view_embedder/global_html_attributes.dart
|
org-dartlang-sdk:///lib/_engine/engine/view_embedder/global_html_attributes.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/view_embedder/hot_restart_cache_handler.dart
|
org-dartlang-sdk:///lib/_engine/engine/view_embedder/hot_restart_cache_handler.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/view_embedder/style_manager.dart
|
org-dartlang-sdk:///lib/_engine/engine/view_embedder/style_manager.dart
|
||||||
|
org-dartlang-sdk:///lib/_engine/engine/web_paragraph/paragraph.dart
|
||||||
org-dartlang-sdk:///lib/_engine/engine/window.dart
|
org-dartlang-sdk:///lib/_engine/engine/window.dart
|
||||||
org-dartlang-sdk:///lib/_skwasm_stub/skwasm_stub.dart
|
org-dartlang-sdk:///lib/_skwasm_stub/skwasm_stub.dart
|
||||||
org-dartlang-sdk:///lib/_skwasm_stub/skwasm_stub/renderer.dart
|
org-dartlang-sdk:///lib/_skwasm_stub/skwasm_stub/renderer.dart
|
||||||
@@ -2343,6 +2385,7 @@ org-dartlang-sdk:///lib/ui/path_metrics.dart
|
|||||||
org-dartlang-sdk:///lib/ui/platform_dispatcher.dart
|
org-dartlang-sdk:///lib/ui/platform_dispatcher.dart
|
||||||
org-dartlang-sdk:///lib/ui/platform_isolate.dart
|
org-dartlang-sdk:///lib/ui/platform_isolate.dart
|
||||||
org-dartlang-sdk:///lib/ui/pointer.dart
|
org-dartlang-sdk:///lib/ui/pointer.dart
|
||||||
|
org-dartlang-sdk:///lib/ui/rsuperellipse_param.dart
|
||||||
org-dartlang-sdk:///lib/ui/semantics.dart
|
org-dartlang-sdk:///lib/ui/semantics.dart
|
||||||
org-dartlang-sdk:///lib/ui/text.dart
|
org-dartlang-sdk:///lib/ui/text.dart
|
||||||
org-dartlang-sdk:///lib/ui/tile_mode.dart
|
org-dartlang-sdk:///lib/ui/tile_mode.dart
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user