Merge pull request 'feat: Release version 3.1.4 - Mode terrain et génération PDF' (#8) from release/v3.1.4 into main

Reviewed-on: #8
This commit is contained in:
2025-08-19 19:45:44 +02:00
185 changed files with 109354 additions and 102937 deletions

334
api/docs/API-SECURITY.md Normal file
View 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
View 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

View 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.

View File

@@ -779,7 +779,7 @@ fetch('/api/endpoint', {
## Changements récents
### Version 3.0.7 (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
@@ -797,7 +797,7 @@ fetch('/api/endpoint', {
- **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 (Janvier 2025)
### Version 3.0.6 (Août 2025)
#### 1. Correction des rôles administrateurs
- **Avant :** Les administrateurs d'amicale devaient avoir `fk_role > 2`
@@ -836,3 +836,28 @@ fetch('/api/endpoint', {
- **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
- **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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
api/docs/_recu_template.pdf Normal file

Binary file not shown.

View File

@@ -18,168 +18,39 @@
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE TABLE `chat_anonymous_users` (
`id` varchar(50) NOT NULL,
`device_id` varchar(100) NOT NULL,
`name` varchar(100) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`converted_to_user_id` int(10) unsigned DEFAULT NULL,
`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';
-- Tables préfixées "chat_"
CREATE TABLE chat_rooms (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255),
type ENUM('private', 'group', 'broadcast'),
created_at TIMESTAMP,
created_by INT
);
CREATE TABLE `chat_attachments` (
`id` varchar(50) NOT NULL,
`fk_message` varchar(50) NOT NULL,
`file_name` varchar(255) NOT NULL,
`file_path` varchar(500) NOT NULL,
`file_type` varchar(100) NOT NULL,
`file_size` int(10) unsigned NOT NULL,
`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_messages (
id VARCHAR(36) PRIMARY KEY,
room_id VARCHAR(36),
content TEXT,
sender_id INT,
sent_at TIMESTAMP,
FOREIGN KEY (room_id) REFERENCES chat_rooms(id)
);
CREATE TABLE `chat_audience_targets` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_room` varchar(50) NOT NULL,
`target_type` enum('role','entity','all','combined') NOT NULL DEFAULT 'all',
`target_id` varchar(50) DEFAULT NULL,
`role_filter` varchar(20) DEFAULT NULL,
`entity_filter` varchar(50) DEFAULT NULL,
`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_participants (
room_id VARCHAR(36),
user_id INT,
role INT,
entite_id INT,
joined_at TIMESTAMP,
PRIMARY KEY (room_id, user_id)
);
CREATE TABLE `chat_broadcast_lists` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_room` varchar(50) NOT NULL,
`name` varchar(100) NOT NULL,
`description` text DEFAULT NULL,
`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 chat_read_receipts (
message_id VARCHAR(36),
user_id INT,
read_at TIMESTAMP,
PRIMARY KEY (message_id, user_id)
);
CREATE TABLE `email_counter` (
`id` int(10) unsigned NOT NULL DEFAULT 1,

BIN
api/docs/recu_537254062.pdf Normal file

Binary file not shown.

BIN
api/docs/recu_972506460.pdf Normal file

Binary file not shown.

View File

@@ -15,6 +15,12 @@ require_once __DIR__ . '/src/Core/Response.php';
require_once __DIR__ . '/src/Utils/ClientDetector.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
require_once __DIR__ . '/src/Controllers/LogController.php';
require_once __DIR__ . '/src/Controllers/LoginController.php';
@@ -26,6 +32,8 @@ require_once __DIR__ . '/src/Controllers/VilleController.php';
require_once __DIR__ . '/src/Controllers/FileController.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
$appConfig = AppConfig::getInstance();
@@ -57,8 +65,132 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
// Initialiser la session
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([
'success' => false,
'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([
'success' => false,
'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([
'success' => false,
'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([
'success' => false,
'message' => 'Bad request'
], 400);
exit;
}
// Créer l'instance de routeur
$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([
'success' => false,
'message' => 'Internal server error'
], 500);
});
// Gérer la requête
try {
$router->handle();
} catch (Exception $e) {
// Les exceptions sont gérées par le handler ci-dessus
throw $e;
}

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

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

View 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;

View 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

View 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

View File

@@ -0,0 +1,875 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
require_once __DIR__ . '/../Services/LogService.php';
require_once __DIR__ . '/../Services/ApiService.php';
use PDO;
use PDOException;
use Database;
use Request;
use Response;
use Session;
use LogService;
use ApiService;
class ChatController {
private PDO $db;
public function __construct() {
$this->db = Database::getInstance();
}
/**
* GET /api/chat/rooms
* Liste des conversations filtrées par rôle et entité
*/
public function getRooms(): void {
Session::requireAuth();
try {
$userId = Session::getUserId();
$entityId = Session::getEntityId();
// Récupérer le rôle de l'utilisateur
$userRole = $this->getUserRole($userId);
// Construction de la requête selon le rôle
$sql = '
SELECT DISTINCT
r.id,
r.title,
r.type,
r.created_at,
r.created_by,
r.updated_at,
-- Dernier message
(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,
-- Nombre de messages non lus
(SELECT COUNT(*)
FROM chat_messages m
WHERE m.room_id = r.id
AND m.sent_at > COALESCE(p.last_read_at, p.joined_at)
AND m.sender_id != :user_id_count) as unread_count,
-- Participants
(SELECT COUNT(*)
FROM chat_participants cp
WHERE cp.room_id = r.id
AND cp.left_at IS NULL) as participant_count
FROM chat_rooms r
INNER JOIN chat_participants p ON r.id = p.room_id
WHERE r.is_active = 1
AND p.user_id = :user_id
AND p.left_at IS NULL
';
// Filtrage supplémentaire selon le rôle
if ($userRole == 1) {
// Utilisateur simple : seulement ses conversations privées et de groupe
$sql .= ' AND r.type IN ("private", "group")';
} elseif ($userRole == 2) {
// Admin d'entité : toutes les conversations de son entité
$sql .= ' AND (p.entite_id = :entity_id OR r.type = "broadcast")';
}
// Rôle > 2 : accès à toutes les conversations
$sql .= ' ORDER BY COALESCE(last_message_at, r.created_at) DESC';
$stmt = $this->db->prepare($sql);
$params = [
'user_id' => $userId,
'user_id_count' => $userId
];
if ($userRole == 2) {
$params['entity_id'] = $entityId;
}
$stmt->execute($params);
$rooms = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Pour chaque room, récupérer les participants
foreach ($rooms as &$room) {
$room['participants'] = $this->getRoomParticipants($room['id']);
}
LogService::log('Récupération des conversations', [
'level' => 'debug',
'user_id' => $userId,
'room_count' => count($rooms)
]);
Response::json([
'status' => 'success',
'rooms' => $rooms
]);
} catch (PDOException $e) {
LogService::log('Erreur lors de la récupération des conversations', [
'level' => 'error',
'error' => $e->getMessage()
]);
Response::json([
'status' => 'error',
'message' => 'Erreur serveur'
], 500);
}
}
/**
* POST /api/chat/rooms
* Créer une nouvelle conversation
*/
public function createRoom(): void {
Session::requireAuth();
try {
$data = Request::getJson();
$userId = Session::getUserId();
$entityId = Session::getEntityId();
$userRole = $this->getUserRole($userId);
// Validation des données
if (!isset($data['type']) || !in_array($data['type'], ['private', 'group', 'broadcast'])) {
Response::json([
'status' => 'error',
'message' => 'Type de conversation invalide'
], 400);
return;
}
// Vérification des permissions pour broadcast
if ($data['type'] === 'broadcast' && $userRole < 2) {
Response::json([
'status' => 'error',
'message' => 'Permissions insuffisantes pour créer une diffusion'
], 403);
return;
}
// Validation des participants
if (!isset($data['participants']) || !is_array($data['participants']) || empty($data['participants'])) {
Response::json([
'status' => 'error',
'message' => 'Au moins un participant requis'
], 400);
return;
}
// Pour une conversation privée, limiter à 2 participants (incluant le créateur)
if ($data['type'] === 'private' && count($data['participants']) > 1) {
Response::json([
'status' => 'error',
'message' => 'Une conversation privée ne peut avoir que 2 participants'
], 400);
return;
}
// Vérifier que tous les participants existent et sont accessibles
$participantIds = array_map('intval', $data['participants']);
if (!in_array($userId, $participantIds)) {
$participantIds[] = $userId; // Ajouter le créateur
}
// Vérifier l'existence d'une conversation privée existante
if ($data['type'] === 'private' && count($participantIds) === 2) {
$existingRoom = $this->findExistingPrivateRoom($participantIds[0], $participantIds[1]);
if ($existingRoom) {
Response::json([
'status' => 'success',
'room' => $existingRoom,
'existing' => true
]);
return;
}
}
// Générer un UUID pour la room
$roomId = $this->generateUUID();
// Titre de la conversation
$title = $data['title'] ?? null;
if (!$title && $data['type'] === 'private') {
// Pour une conversation privée, pas de titre par défaut
$title = null;
}
$this->db->beginTransaction();
try {
// Créer la room
$stmt = $this->db->prepare('
INSERT INTO chat_rooms (id, title, type, created_by, created_at)
VALUES (:id, :title, :type, :created_by, NOW())
');
$stmt->execute([
'id' => $roomId,
'title' => $title,
'type' => $data['type'],
'created_by' => $userId
]);
// Ajouter les participants
$participantStmt = $this->db->prepare('
INSERT INTO chat_participants (room_id, user_id, role, entite_id, is_admin)
VALUES (:room_id, :user_id, :role, :entite_id, :is_admin)
');
foreach ($participantIds as $participantId) {
$participantData = $this->getUserData($participantId);
if (!$participantData) {
throw new \Exception("Participant invalide: $participantId");
}
$participantStmt->execute([
'room_id' => $roomId,
'user_id' => $participantId,
'role' => $participantData['fk_role'],
'entite_id' => $participantData['fk_entite'],
'is_admin' => ($participantId === $userId) ? 1 : 0
]);
}
// Si un message initial est fourni, l'envoyer
if (isset($data['initial_message']) && !empty($data['initial_message'])) {
$messageId = $this->generateUUID();
$msgStmt = $this->db->prepare('
INSERT INTO chat_messages (id, room_id, content, sender_id, sent_at)
VALUES (:id, :room_id, :content, :sender_id, NOW())
');
$msgStmt->execute([
'id' => $messageId,
'room_id' => $roomId,
'content' => $data['initial_message'],
'sender_id' => $userId
]);
}
$this->db->commit();
LogService::log('Conversation créée', [
'level' => 'info',
'room_id' => $roomId,
'type' => $data['type'],
'created_by' => $userId,
'participant_count' => count($participantIds)
]);
// Récupérer la room créée avec ses détails
$room = $this->getRoomDetails($roomId);
Response::json([
'status' => 'success',
'room' => $room
], 201);
} catch (\Exception $e) {
$this->db->rollBack();
throw $e;
}
} catch (PDOException $e) {
LogService::log('Erreur lors de la création de la conversation', [
'level' => 'error',
'error' => $e->getMessage()
]);
Response::json([
'status' => 'error',
'message' => 'Erreur serveur'
], 500);
} catch (\Exception $e) {
LogService::log('Erreur lors de la création de la conversation', [
'level' => 'error',
'error' => $e->getMessage()
]);
Response::json([
'status' => 'error',
'message' => $e->getMessage()
], 400);
}
}
/**
* GET /api/chat/rooms/{id}/messages
* Récupérer les messages d'une conversation
*/
public function getRoomMessages(string $roomId): void {
Session::requireAuth();
try {
$userId = Session::getUserId();
// Vérifier que l'utilisateur est participant
if (!$this->isUserInRoom($userId, $roomId)) {
Response::json([
'status' => 'error',
'message' => 'Accès non autorisé à cette conversation'
], 403);
return;
}
// Paramètres de pagination
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 50;
$before = $_GET['before'] ?? null; // Message ID pour pagination
$sql = '
SELECT
m.id,
m.content,
m.sender_id,
m.sent_at,
m.edited_at,
m.is_deleted,
u.encrypted_name as sender_name,
u.first_name as sender_first_name,
-- Statut de lecture
(SELECT COUNT(*)
FROM chat_read_receipts r
WHERE r.message_id = m.id) as read_count,
(SELECT COUNT(*)
FROM chat_read_receipts r
WHERE r.message_id = m.id
AND r.user_id = :user_id) as is_read
FROM chat_messages m
INNER JOIN users u ON m.sender_id = u.id
WHERE m.room_id = :room_id
';
$params = [
'room_id' => $roomId,
'user_id' => $userId
];
if ($before) {
$sql .= ' AND m.sent_at < (SELECT sent_at FROM chat_messages WHERE id = :before)';
$params['before'] = $before;
}
$sql .= ' ORDER BY m.sent_at DESC LIMIT :limit';
$stmt = $this->db->prepare($sql);
foreach ($params as $key => $value) {
if ($key === 'limit') {
$stmt->bindValue($key, $value, PDO::PARAM_INT);
} else {
$stmt->bindValue($key, $value);
}
}
$stmt->bindValue('limit', $limit, PDO::PARAM_INT);
$stmt->execute();
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Déchiffrer les noms
foreach ($messages as &$message) {
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
$message['is_read'] = (bool)$message['is_read'];
$message['is_mine'] = ($message['sender_id'] == $userId);
}
// Inverser pour avoir l'ordre chronologique
$messages = array_reverse($messages);
// Mettre à jour last_read_at pour ce participant
$this->updateLastRead($roomId, $userId);
Response::json([
'status' => 'success',
'messages' => $messages,
'has_more' => count($messages) === $limit
]);
} catch (PDOException $e) {
LogService::log('Erreur lors de la récupération des messages', [
'level' => 'error',
'room_id' => $roomId,
'error' => $e->getMessage()
]);
Response::json([
'status' => 'error',
'message' => 'Erreur serveur'
], 500);
}
}
/**
* POST /api/chat/rooms/{id}/messages
* Envoyer un message dans une conversation
*/
public function sendMessage(string $roomId): void {
Session::requireAuth();
try {
$data = Request::getJson();
$userId = Session::getUserId();
// Vérifier que l'utilisateur est participant
if (!$this->isUserInRoom($userId, $roomId)) {
Response::json([
'status' => 'error',
'message' => 'Accès non autorisé à cette conversation'
], 403);
return;
}
// Validation du contenu
if (!isset($data['content']) || empty(trim($data['content']))) {
Response::json([
'status' => 'error',
'message' => 'Le message ne peut pas être vide'
], 400);
return;
}
$content = trim($data['content']);
// Limiter la longueur du message
if (mb_strlen($content, 'UTF-8') > 5000) {
Response::json([
'status' => 'error',
'message' => 'Message trop long (max 5000 caractères)'
], 400);
return;
}
$messageId = $this->generateUUID();
// Insérer le message
$stmt = $this->db->prepare('
INSERT INTO chat_messages (id, room_id, content, sender_id, sent_at)
VALUES (:id, :room_id, :content, :sender_id, NOW())
');
$stmt->execute([
'id' => $messageId,
'room_id' => $roomId,
'content' => $content,
'sender_id' => $userId
]);
// Mettre à jour la date de dernière modification de la room
$updateStmt = $this->db->prepare('
UPDATE chat_rooms
SET updated_at = NOW()
WHERE id = :room_id
');
$updateStmt->execute(['room_id' => $roomId]);
// Récupérer le message créé avec les infos du sender
$msgStmt = $this->db->prepare('
SELECT
m.id,
m.content,
m.sender_id,
m.sent_at,
u.encrypted_name as sender_name,
u.first_name as sender_first_name
FROM chat_messages m
INNER JOIN users u ON m.sender_id = u.id
WHERE m.id = :id
');
$msgStmt->execute(['id' => $messageId]);
$message = $msgStmt->fetch(PDO::FETCH_ASSOC);
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
$message['is_mine'] = true;
$message['is_read'] = false;
$message['read_count'] = 0;
LogService::log('Message envoyé', [
'level' => 'debug',
'room_id' => $roomId,
'message_id' => $messageId,
'sender_id' => $userId
]);
Response::json([
'status' => 'success',
'message' => $message
], 201);
} catch (PDOException $e) {
LogService::log('Erreur lors de l\'envoi du message', [
'level' => 'error',
'room_id' => $roomId,
'error' => $e->getMessage()
]);
Response::json([
'status' => 'error',
'message' => 'Erreur serveur'
], 500);
}
}
/**
* POST /api/chat/rooms/{id}/read
* Marquer les messages comme lus
*/
public function markAsRead(string $roomId): void {
Session::requireAuth();
try {
$data = Request::getJson();
$userId = Session::getUserId();
// Vérifier que l'utilisateur est participant
if (!$this->isUserInRoom($userId, $roomId)) {
Response::json([
'status' => 'error',
'message' => 'Accès non autorisé à cette conversation'
], 403);
return;
}
// Si des message_ids spécifiques sont fournis
if (isset($data['message_ids']) && is_array($data['message_ids'])) {
$messageIds = $data['message_ids'];
// Marquer ces messages spécifiques comme lus
$stmt = $this->db->prepare('
INSERT IGNORE INTO chat_read_receipts (message_id, user_id, read_at)
VALUES (:message_id, :user_id, NOW())
');
foreach ($messageIds as $messageId) {
$stmt->execute([
'message_id' => $messageId,
'user_id' => $userId
]);
}
} else {
// Marquer tous les messages non lus de la room comme lus
$stmt = $this->db->prepare('
INSERT IGNORE INTO chat_read_receipts (message_id, user_id, read_at)
SELECT m.id, :user_id, NOW()
FROM chat_messages m
WHERE m.room_id = :room_id
AND m.id NOT IN (
SELECT message_id
FROM chat_read_receipts
WHERE user_id = :user_id_check
)
');
$stmt->execute([
'user_id' => $userId,
'user_id_check' => $userId,
'room_id' => $roomId
]);
}
// Mettre à jour last_read_at
$this->updateLastRead($roomId, $userId);
// Compter les messages non lus restants
$countStmt = $this->db->prepare('
SELECT COUNT(*) as unread_count
FROM chat_messages m
WHERE m.room_id = :room_id
AND m.sender_id != :user_id
AND m.id NOT IN (
SELECT message_id
FROM chat_read_receipts
WHERE user_id = :user_id_check
)
');
$countStmt->execute([
'room_id' => $roomId,
'user_id' => $userId,
'user_id_check' => $userId
]);
$result = $countStmt->fetch(PDO::FETCH_ASSOC);
Response::json([
'status' => 'success',
'unread_count' => (int)$result['unread_count']
]);
} catch (PDOException $e) {
LogService::log('Erreur lors du marquage comme lu', [
'level' => 'error',
'room_id' => $roomId,
'error' => $e->getMessage()
]);
Response::json([
'status' => 'error',
'message' => 'Erreur serveur'
], 500);
}
}
/**
* GET /api/chat/recipients
* Liste des destinataires possibles selon le rôle
*/
public function getRecipients(): void {
Session::requireAuth();
try {
$userId = Session::getUserId();
$entityId = Session::getEntityId();
$userRole = $this->getUserRole($userId);
$sql = '
SELECT
u.id,
u.encrypted_name as name,
u.first_name,
u.sect_name,
u.fk_role as role,
u.fk_entite as entite_id,
e.encrypted_name as entite_name
FROM users u
LEFT JOIN entites e ON u.fk_entite = e.id
WHERE u.chk_active = 1
AND u.id != :user_id
';
$params = ['user_id' => $userId];
// Filtrage selon le rôle
if ($userRole == 1) {
// Utilisateur simple : seulement les utilisateurs de son entité
$sql .= ' AND u.fk_entite = :entity_id';
$params['entity_id'] = $entityId;
} elseif ($userRole == 2) {
// Admin d'entité :
// - Tous les membres actifs de son amicale (même entité)
// - Les super-admins (fk_role=9) de l'entité 1
$sql .= ' AND (
u.fk_entite = :entity_id
OR (u.fk_role = 9 AND u.fk_entite = 1)
)';
$params['entity_id'] = $entityId;
} elseif ($userRole == 9) {
// Super-administrateur :
// - Seulement les administrateurs actifs des amicales (fk_role=2)
// - Et les autres super-admins (fk_role=9)
$sql .= ' AND (u.fk_role = 2 OR u.fk_role = 9)';
}
// Autres rôles (3-8) : pas de filtrage spécifique pour le moment
$sql .= ' ORDER BY u.fk_entite, u.encrypted_name';
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$recipients = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Déchiffrer les données et organiser par entité
$recipientsByEntity = [];
$recipientsDecrypted = [];
foreach ($recipients as &$recipient) {
// Déchiffrer le nom
$recipient['name'] = ApiService::decryptData($recipient['name']);
// Déchiffrer le nom de l'entité
$entiteName = $recipient['entite_name'] ?
ApiService::decryptData($recipient['entite_name']) :
'Sans entité';
// Créer une copie pour recipients_by_entity
$recipientCopy = $recipient;
unset($recipientCopy['entite_name']);
// Organiser par entité
if (!isset($recipientsByEntity[$entiteName])) {
$recipientsByEntity[$entiteName] = [];
}
$recipientsByEntity[$entiteName][] = $recipientCopy;
// Remplacer entite_name chiffré par la version déchiffrée
$recipient['entite_name'] = $entiteName;
// Ajouter à la liste déchiffrée
$recipientsDecrypted[] = $recipient;
}
Response::json([
'status' => 'success',
'recipients' => $recipientsDecrypted,
'recipients_by_entity' => $recipientsByEntity
]);
} catch (PDOException $e) {
LogService::log('Erreur lors de la récupération des destinataires', [
'level' => 'error',
'error' => $e->getMessage()
]);
Response::json([
'status' => 'error',
'message' => 'Erreur serveur'
], 500);
}
}
// ===== Méthodes utilitaires privées =====
/**
* Récupérer le rôle d'un utilisateur
*/
private function getUserRole(int $userId): int {
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
$stmt->execute([$userId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? (int)$result['fk_role'] : 1;
}
/**
* Récupérer les données d'un utilisateur
*/
private function getUserData(int $userId): ?array {
$stmt = $this->db->prepare('
SELECT id, fk_role, fk_entite, encrypted_name, first_name
FROM users
WHERE id = ? AND chk_active = 1
');
$stmt->execute([$userId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ?: null;
}
/**
* Vérifier si un utilisateur est dans une room
*/
private function isUserInRoom(int $userId, string $roomId): bool {
$stmt = $this->db->prepare('
SELECT COUNT(*) as count
FROM chat_participants
WHERE room_id = ?
AND user_id = ?
AND left_at IS NULL
');
$stmt->execute([$roomId, $userId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result && $result['count'] > 0;
}
/**
* Récupérer les participants d'une room
*/
private function getRoomParticipants(string $roomId): array {
$stmt = $this->db->prepare('
SELECT
p.user_id,
p.is_admin,
u.encrypted_name as name,
u.first_name
FROM chat_participants p
INNER JOIN users u ON p.user_id = u.id
WHERE p.room_id = ?
AND p.left_at IS NULL
');
$stmt->execute([$roomId]);
$participants = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($participants as &$participant) {
$participant['name'] = ApiService::decryptData($participant['name']);
}
return $participants;
}
/**
* Récupérer les détails d'une room
*/
private function getRoomDetails(string $roomId): array {
$stmt = $this->db->prepare('
SELECT
r.id,
r.title,
r.type,
r.created_at,
r.created_by,
r.updated_at
FROM chat_rooms r
WHERE r.id = ?
');
$stmt->execute([$roomId]);
$room = $stmt->fetch(PDO::FETCH_ASSOC);
if ($room) {
$room['participants'] = $this->getRoomParticipants($roomId);
}
return $room;
}
/**
* Trouver une conversation privée existante entre deux utilisateurs
*/
private function findExistingPrivateRoom(int $user1, int $user2): ?array {
$stmt = $this->db->prepare('
SELECT r.*
FROM chat_rooms r
WHERE r.type = "private"
AND r.is_active = 1
AND EXISTS (
SELECT 1 FROM chat_participants p1
WHERE p1.room_id = r.id
AND p1.user_id = ?
AND p1.left_at IS NULL
)
AND EXISTS (
SELECT 1 FROM chat_participants p2
WHERE p2.room_id = r.id
AND p2.user_id = ?
AND p2.left_at IS NULL
)
AND (
SELECT COUNT(*)
FROM chat_participants p
WHERE p.room_id = r.id
AND p.left_at IS NULL
) = 2
');
$stmt->execute([$user1, $user2]);
$room = $stmt->fetch(PDO::FETCH_ASSOC);
if ($room) {
$room['participants'] = $this->getRoomParticipants($room['id']);
return $room;
}
return null;
}
/**
* Mettre à jour la date de dernière lecture
*/
private function updateLastRead(string $roomId, int $userId): void {
$stmt = $this->db->prepare('
UPDATE chat_participants
SET last_read_at = NOW()
WHERE room_id = ?
AND user_id = ?
');
$stmt->execute([$roomId, $userId]);
}
/**
* Générer un UUID v4
*/
private function generateUUID(): string {
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}

View File

@@ -20,6 +20,9 @@ use ApiService;
require_once __DIR__ . '/../Services/LogService.php';
require_once __DIR__ . '/../Services/ApiService.php';
require_once __DIR__ . '/EntiteController.php';
require_once __DIR__ . '/../Services/Security/SecurityMonitor.php';
use App\Services\Security\SecurityMonitor;
class LoginController {
private PDO $db;
@@ -76,6 +79,11 @@ class LoginController {
$user = $stmt->fetch(PDO::FETCH_ASSOC);
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é', [
'level' => 'warning',
'username' => $username
@@ -88,6 +96,11 @@ class LoginController {
$passwordValid = password_verify($data['password'], $user['user_pass_hash']);
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', [
'level' => 'warning',
'username' => $username
@@ -769,6 +782,88 @@ class LoginController {
$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
Response::json($response);
} catch (PDOException $e) {

View File

@@ -6,6 +6,7 @@ namespace App\Controllers;
require_once __DIR__ . '/../Services/LogService.php';
require_once __DIR__ . '/../Services/ApiService.php';
require_once __DIR__ . '/../Services/ReceiptService.php';
use PDO;
use PDOException;
@@ -551,10 +552,44 @@ class PassageController {
'operationId' => $operationId
]);
// Générer automatiquement un reçu si c'est un don (fk_type = 1) avec email valide
$receiptGenerated = false;
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
]);
}
}
}
Response::json([
'status' => 'success',
'message' => 'Passage créé avec succès',
'passage_id' => $passageId
'passage_id' => $passageId,
'receipt_generated' => $receiptGenerated
], 201);
} catch (Exception $e) {
LogService::log('Erreur lors de la création du passage', [
@@ -705,9 +740,52 @@ class PassageController {
'passageId' => $passageId
]);
// Vérifier si un reçu doit être généré après la mise à jour
$receiptGenerated = false;
// 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
try {
$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
]);
}
}
}
Response::json([
'status' => 'success',
'message' => 'Passage mis à jour avec succès'
'message' => 'Passage mis à jour avec succès',
'receipt_generated' => $receiptGenerated
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de la mise à jour du passage', [
@@ -800,4 +878,150 @@ class PassageController {
], 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);
}
}
}

View 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'
]);
}
}

View File

@@ -214,11 +214,41 @@ class UserController {
$data = Request::getJson();
$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
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([
'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);
return;
}
@@ -260,9 +290,16 @@ class UserController {
// Validation de l'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([
'status' => 'error',
'message' => 'Format d\'email invalide'
'message' => 'Format d\'email invalide',
'field' => 'email',
'value' => $email
], 400);
return;
}
@@ -290,20 +327,56 @@ class UserController {
if ($chkUsernameManuel === 1) {
// Username manuel obligatoire
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([
'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);
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
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
// Validation ultra-souple : seulement la longueur en caractères UTF-8
$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([
'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);
return;
}
@@ -316,7 +389,8 @@ class UserController {
if ($checkUsernameStmt->fetch()) {
Response::json([
'status' => 'error',
'message' => 'Ce nom d\'utilisateur est déjà utilisé dans GeoSector'
'message' => 'Identifiant déjà utilisé',
'field' => 'username'
], 409);
return;
}
@@ -338,9 +412,18 @@ class UserController {
if ($chkMdpManuel === 1) {
// Mot de passe manuel obligatoire
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([
'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);
return;
}
@@ -927,22 +1010,60 @@ class UserController {
try {
$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
if (!isset($data['username']) || empty(trim($data['username']))) {
LogService::log('Erreur vérification username : Username manquant', [
'level' => 'warning',
'checkedBy' => Session::getUserId()
]);
Response::json([
'status' => 'error',
'message' => 'Username requis pour la vérification'
'message' => 'Identifiant requis',
'field' => 'username'
], 400);
return;
}
$username = trim(strtolower($data['username']));
// Trim du username mais on garde la casse originale
$username = trim($data['username']);
// Validation du format du username
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
// Validation ultra-souple : seulement la longueur en caractères UTF-8
$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([
'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
], 400);
return;

View File

@@ -1,6 +1,11 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/MonitoredDatabase.php';
require_once __DIR__ . '/../Services/Security/AlertService.php';
use App\Services\Security\AlertService;
class Database {
private static ?PDO $instance = null;
private static array $config;
@@ -23,13 +28,22 @@ class Database {
PDO::ATTR_EMULATE_PREPARES => false,
];
self::$instance = new PDO(
// Utiliser MonitoredDatabase pour le monitoring
self::$instance = new MonitoredDatabase(
$dsn,
self::$config['username'],
self::$config['password'],
$options
);
} 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());
}
}

View File

@@ -0,0 +1,182 @@
<?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 = PDO::ATTR_DEFAULT_FETCH_MODE, ...$args): PDOStatement|false {
// Démarrer le chronométrage
PerformanceMonitor::startDbQuery($statement);
try {
$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();
}
}

View File

@@ -34,13 +34,14 @@ class Router {
$this->post('log', ['LogController', 'index']);
// 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/:id', ['UserController', 'getUserById']);
$this->post('users', ['UserController', 'createUser']);
$this->put('users/:id', ['UserController', 'updateUser']);
$this->delete('users/:id', ['UserController', 'deleteUser']);
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
$this->post('users/check-username', ['UserController', 'checkUsername']);
$this->post('logout', ['LoginController', 'logout']);
// Routes entités
@@ -69,6 +70,7 @@ class Router {
// Routes passages
$this->get('passages', ['PassageController', 'getPassages']);
$this->get('passages/:id', ['PassageController', 'getPassageById']);
$this->get('passages/:id/receipt', ['PassageController', 'getReceipt']);
$this->get('passages/operation/:operation_id', ['PassageController', 'getPassagesByOperation']);
$this->post('passages', ['PassageController', 'createPassage']);
$this->put('passages/:id', ['PassageController', 'updatePassage']);
@@ -97,6 +99,25 @@ class Router {
$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->get('chat/rooms/:id/messages', ['ChatController', 'getRoomMessages']);
$this->post('chat/rooms/:id/messages', ['ChatController', 'sendMessage']);
$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 {

View File

@@ -0,0 +1,622 @@
<?php
declare(strict_types=1);
namespace App\Services;
require_once __DIR__ . '/LogService.php';
require_once __DIR__ . '/ApiService.php';
require_once __DIR__ . '/FileService.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';
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 = "/entites/{$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 ultra-optimisé (< 20KB)
* Utilise le format PDF natif pour minimiser la taille
*/
private function generateOptimizedPDF(array $data, ?string $logoPath): string {
// Début du PDF
$pdf = "%PDF-1.3\n";
$objects = [];
$xref = [];
// Object 1 - Catalog
$objects[1] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
// Object 2 - Pages
$objects[2] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
// Object 3 - Page
$objects[3] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n";
// Object 4 - Font (Helvetica)
$objects[4] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n";
// Contenu de la page (texte du reçu)
$content = $this->generatePDFContent($data);
// Object 5 - Content stream
$contentLength = strlen($content);
$objects[5] = "5 0 obj\n<< /Length $contentLength >>\nstream\n$content\nendstream\nendobj\n";
// Construction du PDF final
$offset = strlen($pdf);
foreach ($objects as $obj) {
$xref[] = $offset;
$pdf .= $obj;
$offset += strlen($obj);
}
// Table xref
$pdf .= "xref\n";
$pdf .= "0 " . (count($objects) + 1) . "\n";
$pdf .= "0000000000 65535 f \n";
foreach ($xref as $off) {
$pdf .= sprintf("%010d 00000 n \n", $off);
}
// Trailer
$pdf .= "trailer\n";
$pdf .= "<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\n";
$pdf .= "startxref\n";
$pdf .= "$offset\n";
$pdf .= "%%EOF\n";
return $pdf;
}
/**
* Génère le contenu textuel du reçu pour le PDF
*/
private function generatePDFContent(array $data): string {
$content = "BT\n";
$content .= "/F1 12 Tf\n";
$y = 750;
// En-tête
$content .= "50 $y Td\n";
$content .= "(" . $this->escapeString($data['entite_name']) . ") Tj\n";
$y -= 20;
if (!empty($data['entite_address'])) {
$content .= "0 -20 Td\n";
$content .= "(" . $this->escapeString($data['entite_address']) . ") Tj\n";
$y -= 20;
}
// Titre du reçu
$y -= 40;
$content .= "/F1 16 Tf\n";
$content .= "0 -40 Td\n";
$content .= "(RECU DE DON N° " . $data['receipt_number'] . ") Tj\n";
$content .= "/F1 10 Tf\n";
$content .= "0 -15 Td\n";
$content .= "(Article 200 du Code General des Impots) Tj\n";
// Informations du donateur
$y -= 60;
$content .= "/F1 12 Tf\n";
$content .= "0 -45 Td\n";
$content .= "(DONATEUR) Tj\n";
$content .= "/F1 11 Tf\n";
$content .= "0 -20 Td\n";
$content .= "(Nom : " . $this->escapeString($data['donor_name']) . ") Tj\n";
if (!empty($data['donor_address'])) {
$content .= "0 -15 Td\n";
$content .= "(Adresse : " . $this->escapeString($data['donor_address']) . ") Tj\n";
}
if (!empty($data['donor_email'])) {
$content .= "0 -15 Td\n";
$content .= "(Email : " . $this->escapeString($data['donor_email']) . ") Tj\n";
}
// Détails du don
$content .= "0 -30 Td\n";
$content .= "/F1 12 Tf\n";
$content .= "(DETAILS DU DON) Tj\n";
$content .= "/F1 11 Tf\n";
$content .= "0 -20 Td\n";
$content .= "(Date : " . $data['donation_date'] . ") Tj\n";
$content .= "0 -15 Td\n";
$content .= "(Montant : " . $data['amount'] . " EUR) Tj\n";
$content .= "0 -15 Td\n";
$content .= "(Mode de reglement : " . $this->escapeString($data['payment_method']) . ") Tj\n";
if (!empty($data['operation_name'])) {
$content .= "0 -15 Td\n";
$content .= "(Campagne : " . $this->escapeString($data['operation_name']) . ") Tj\n";
}
// Mention légale
$content .= "/F1 9 Tf\n";
$content .= "0 -40 Td\n";
$content .= "(Reduction d'impot egale a 66% du montant verse dans la limite de 20% du revenu imposable) Tj\n";
// Date et signature
$content .= "/F1 11 Tf\n";
$content .= "0 -30 Td\n";
$content .= "(Fait a " . $this->escapeString($data['entite_city']) . ", le " . $data['signature_date'] . ") Tj\n";
$content .= "0 -20 Td\n";
$content .= "(Le President) Tj\n";
$content .= "ET\n";
return $content;
}
/**
* É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);
// Remplacer manuellement les caractères accentués les plus courants
$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',
'Œ' => 'OE', 'œ' => 'oe',
'Æ' => 'AE', 'æ' => 'ae'
];
$str = strtr($str, $accents);
// Supprimer tout caractère non-ASCII restant
$str = preg_replace('/[^\x20-\x7E]/', '', $str);
return $str;
}
/**
* 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]);
}
}

254
api/test_security.php Normal file
View 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
View 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";

View 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";

File diff suppressed because one or more lines are too long

View File

@@ -1,64 +0,0 @@
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ConversationModelAdapter extends TypeAdapter<ConversationModel> {
@override
final int typeId = 20;
@override
ConversationModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ConversationModel(
id: fields[0] as String,
type: fields[1] as String,
title: fields[2] as String?,
createdAt: fields[3] as DateTime,
updatedAt: fields[4] as DateTime,
participants: (fields[5] as List).cast<ParticipantModel>(),
isSynced: fields[6] as bool,
replyPermission: fields[7] as String,
isPinned: fields[8] as bool,
expiryDate: fields[9] as DateTime?,
);
}
@override
void write(BinaryWriter writer, ConversationModel obj) {
writer
..writeByte(10)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.type)
..writeByte(2)
..write(obj.title)
..writeByte(3)
..write(obj.createdAt)
..writeByte(4)
..write(obj.updatedAt)
..writeByte(5)
..write(obj.participants)
..writeByte(6)
..write(obj.isSynced)
..writeByte(7)
..write(obj.replyPermission)
..writeByte(8)
..write(obj.isPinned)
..writeByte(9)
..write(obj.expiryDate);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ConversationModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -2,45 +2,48 @@
// TypeAdapterGenerator
// **************************************************************************
class AudienceTargetModelAdapter extends TypeAdapter<AudienceTargetModel> {
class MessageAdapter extends TypeAdapter<Message> {
@override
final int typeId = 24;
final int typeId = 51;
@override
AudienceTargetModel read(BinaryReader reader) {
Message read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AudienceTargetModel(
return Message(
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?,
roomId: fields[1] as String,
content: fields[2] as String,
senderId: fields[3] as int,
senderName: fields[4] as String,
sentAt: fields[5] as DateTime,
isMe: fields[6] as bool,
isRead: fields[7] as bool,
);
}
@override
void write(BinaryWriter writer, AudienceTargetModel obj) {
void write(BinaryWriter writer, Message obj) {
writer
..writeByte(7)
..writeByte(8)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.conversationId)
..write(obj.roomId)
..writeByte(2)
..write(obj.targetType)
..write(obj.content)
..writeByte(3)
..write(obj.targetId)
..write(obj.senderId)
..writeByte(4)
..write(obj.createdAt)
..write(obj.senderName)
..writeByte(5)
..write(obj.roleFilter)
..write(obj.sentAt)
..writeByte(6)
..write(obj.entityFilter);
..write(obj.isMe)
..writeByte(7)
..write(obj.isRead);
}
@override
@@ -49,7 +52,7 @@ class AudienceTargetModelAdapter extends TypeAdapter<AudienceTargetModel> {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AudienceTargetModelAdapter &&
other is MessageAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -1,67 +0,0 @@
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class MessageModelAdapter extends TypeAdapter<MessageModel> {
@override
final int typeId = 21;
@override
MessageModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return MessageModel(
id: fields[0] as String,
conversationId: fields[1] as String,
senderId: fields[2] as String?,
senderType: fields[3] as String,
content: fields[4] as String,
contentType: fields[5] as String,
createdAt: fields[6] as DateTime,
deliveredAt: fields[7] as DateTime?,
readAt: fields[8] as DateTime?,
status: fields[9] as String,
isAnnouncement: fields[10] as bool,
);
}
@override
void write(BinaryWriter writer, MessageModel obj) {
writer
..writeByte(11)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.conversationId)
..writeByte(2)
..write(obj.senderId)
..writeByte(3)
..write(obj.senderType)
..writeByte(4)
..write(obj.content)
..writeByte(5)
..write(obj.contentType)
..writeByte(6)
..write(obj.createdAt)
..writeByte(7)
..write(obj.deliveredAt)
..writeByte(8)
..write(obj.readAt)
..writeByte(9)
..write(obj.status)
..writeByte(10)
..write(obj.isAnnouncement);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MessageModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

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

View File

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

View File

@@ -2,45 +2,45 @@
// TypeAdapterGenerator
// **************************************************************************
class AnonymousUserModelAdapter extends TypeAdapter<AnonymousUserModel> {
class RoomAdapter extends TypeAdapter<Room> {
@override
final int typeId = 23;
final int typeId = 50;
@override
AnonymousUserModel read(BinaryReader reader) {
Room read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AnonymousUserModel(
return Room(
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>(),
title: fields[1] as String,
type: fields[2] as String,
createdAt: fields[3] as DateTime,
lastMessage: fields[4] as String?,
lastMessageAt: fields[5] as DateTime?,
unreadCount: fields[6] as int,
);
}
@override
void write(BinaryWriter writer, AnonymousUserModel obj) {
void write(BinaryWriter writer, Room obj) {
writer
..writeByte(7)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.deviceId)
..write(obj.title)
..writeByte(2)
..write(obj.name)
..write(obj.type)
..writeByte(3)
..write(obj.email)
..writeByte(4)
..write(obj.createdAt)
..writeByte(4)
..write(obj.lastMessage)
..writeByte(5)
..write(obj.convertedToUserId)
..write(obj.lastMessageAt)
..writeByte(6)
..write(obj.metadata);
..write(obj.unreadCount);
}
@override
@@ -49,7 +49,7 @@ class AnonymousUserModelAdapter extends TypeAdapter<AnonymousUserModel> {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AnonymousUserModelAdapter &&
other is RoomAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -10,6 +10,7 @@ import 'package:connectivity_plus/src/connectivity_plus_web.dart';
import 'package:geolocator_web/geolocator_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:sensors_plus/src/sensors_plus_web.dart';
import 'package:shared_preferences_web/shared_preferences_web.dart';
import 'package:url_launcher_web/url_launcher_web.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
@@ -20,6 +21,7 @@ void registerPlugins([final Registrar? pluginRegistrar]) {
GeolocatorPlugin.registerWith(registrar);
ImagePickerPlugin.registerWith(registrar);
PackageInfoPlusWebPlugin.registerWith(registrar);
WebSensorsPlugin.registerWith(registrar);
SharedPreferencesPlugin.registerWith(registrar);
UrlLauncherPlugin.registerWith(registrar);
registrar.registerMessageHandler();

View File

@@ -1,31 +0,0 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -1 +0,0 @@
{"version":2,"entries":[{"package":"geosector_app","rootUri":"../","packageUri":"lib/"}]}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
/home/pierre/dev/geosector/app/.dart_tool/flutter_build/9801dd92544a637fcb18c8ad3c09ddaa/dart_build_result.json:

View File

@@ -0,0 +1 @@
{"inputs":["/home/pierre/dev/flutter/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart","/home/pierre/dev/geosector/app/.dart_tool/package_config_subset"],"outputs":["/home/pierre/dev/geosector/app/.dart_tool/flutter_build/9801dd92544a637fcb18c8ad3c09ddaa/dart_build_result.json","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/9801dd92544a637fcb18c8ad3c09ddaa/dart_build_result.json"]}

View File

@@ -0,0 +1 @@
{"dependencies":[],"code_assets":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"inputs":["/home/pierre/dev/geosector/app/.dart_tool/package_config_subset"],"outputs":["/home/pierre/dev/geosector/app/.dart_tool/flutter_build/dart_plugin_registrant.dart"]}

View File

@@ -0,0 +1 @@
{"inputs":[],"outputs":[]}

View File

@@ -0,0 +1 @@
/home/pierre/dev/geosector/app/.dart_tool/flutter_build/9801dd92544a637fcb18c8ad3c09ddaa/native_assets.json:

View File

@@ -0,0 +1 @@
{"inputs":["/home/pierre/dev/flutter/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart","/home/pierre/dev/geosector/app/.dart_tool/package_config_subset"],"outputs":["/home/pierre/dev/geosector/app/.dart_tool/flutter_build/9801dd92544a637fcb18c8ad3c09ddaa/native_assets.json","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/9801dd92544a637fcb18c8ad3c09ddaa/native_assets.json"]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}

View File

@@ -0,0 +1 @@
["/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/vm_snapshot_data","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/isolate_snapshot_data","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/kernel_blob.bin","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/assets/images/logo-geosector-512.png-autosave.kra","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/assets/images/icon-geosector.svg","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/assets/images/geosector_map_admin.png","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/assets/images/logo_recu.png","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/assets/images/logo-geosector-512.png","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/assets/images/geosector-logo.png","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/assets/images/logo-geosector-1024.png","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/assets/animations/geo_main.json","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/lib/chat/chat_config.yaml","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/assets/fonts/Figtree-VariableFont_wght.ttf","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/packages/flutter_map/lib/assets/flutter_map_logo.png","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/fonts/MaterialIcons-Regular.otf","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/shaders/ink_sparkle.frag","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/AssetManifest.json","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/AssetManifest.bin","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/FontManifest.json","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/NOTICES.Z","/home/pierre/dev/geosector/app/build/app/intermediates/flutter/debug/flutter_assets/NativeAssetsManifest.json"]

File diff suppressed because one or more lines are too long

View File

@@ -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/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/go_router-16.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/lib/src/state.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/go_router.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/builder.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/configuration.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/delegate.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/information_provider.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/logging.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/match.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/custom_parameter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/error_screen.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/errors.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/extensions.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/inherited_router.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/pages/cupertino.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/pages/custom_transition_page.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/pages/material.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/parser.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/path_utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/route.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/route_data.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/router.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/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/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
@@ -502,11 +502,11 @@ 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/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/image_picker-1.1.2/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.0.6/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.0.6/lib/src/pkg_web_tweaks.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.1.0/lib/image_picker_for_web.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.1.0/lib/src/image_resizer_utils.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.11.0/lib/image_picker_platform_interface.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.11.0/lib/src/platform_interface/image_picker_platform.dart
@@ -737,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/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/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/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
@@ -774,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/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/syncfusion_flutter_charts-30.2.5/lib/charts.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/datetime_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/datetime_category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/logarithmic_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/multi_level_labels.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/numeric_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/plot_band.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/behaviors/crosshair.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/behaviors/trackball.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/behaviors/zooming.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/cartesian_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/circular_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/annotation.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/callbacks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/chart_point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/circular_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/circular_data_label_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/connector_line.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/core_legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/core_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/element_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/empty_points.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/funnel_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/interactive_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/layout_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/pyramid_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/title.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/funnel_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/accumulation_distribution_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/atr_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/bollinger_bands_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/ema_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/macd_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/momentum_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/roc_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/rsi_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/sma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/stochastic_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/technical_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/tma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/wma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/interactions/behavior.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/interactions/selection.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/interactions/tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/pyramid_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/box_and_whisker_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/bubble_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/candle_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/chart_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/doughnut_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/error_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/fast_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/funnel_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/hilo_open_close_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/hilo_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/histogram_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/pie_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/pyramid_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/radial_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/range_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/range_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/scatter_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/spline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_area100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_bar100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_column100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_line100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/step_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stepline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/waterfall_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/trendline/trendline.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/constants.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/renderer_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/typedef.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/zooming_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/sparkline/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/sparkline/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/sparkline/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/calendar/calendar_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/calendar/hijri_date_time.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/localizations/global_localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/slider_controller.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/assistview_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/barcodes_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/calendar_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/chat_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/color_scheme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/datagrid_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/datapager_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/daterangepicker_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/gauges_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/maps_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/pdfviewer_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/range_selector_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/range_slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/spark_charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/theme_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/treemap_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/utils/shape_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/charts.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/datetime_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/datetime_category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/logarithmic_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/multi_level_labels.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/numeric_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/plot_band.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/behaviors/crosshair.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/behaviors/trackball.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/behaviors/zooming.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/cartesian_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/circular_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/annotation.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/callbacks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/chart_point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/circular_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/circular_data_label_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/connector_line.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/core_legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/core_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/element_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/empty_points.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/funnel_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/interactive_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/layout_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/pyramid_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/title.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/funnel_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/accumulation_distribution_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/atr_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/bollinger_bands_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/ema_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/macd_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/momentum_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/roc_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/rsi_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/sma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/stochastic_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/technical_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/tma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/wma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/interactions/behavior.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/interactions/selection.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/interactions/tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/pyramid_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/box_and_whisker_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/bubble_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/candle_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/chart_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/doughnut_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/error_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/fast_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/funnel_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/hilo_open_close_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/hilo_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/histogram_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/pie_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/pyramid_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/radial_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/range_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/range_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/scatter_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/spline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_area100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_bar100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_column100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_line100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/step_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stepline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/waterfall_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/trendline/trendline.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/constants.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/renderer_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/typedef.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/zooming_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/sparkline/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/sparkline/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/sparkline/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/calendar/calendar_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/calendar/hijri_date_time.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/localizations/global_localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/slider_controller.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/assistview_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/barcodes_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/calendar_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/chat_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/color_scheme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/datagrid_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/datapager_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/daterangepicker_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/gauges_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/maps_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/pdfviewer_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/range_selector_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/range_slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/spark_charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/theme_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/treemap_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/utils/shape_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/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/glyph_set.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
@@ -988,7 +1001,6 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_m
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.1.4/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/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/accelerometer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
@@ -1183,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/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/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/flutter_web_sdk/kernel/dart2js_platform.dill
file:///home/pierre/dev/flutter/packages/flutter/lib/animation.dart
@@ -1852,21 +1880,18 @@ file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/web_plugin_registrant.dart
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/chat/models/anonymous_user_model.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/anonymous_user_model.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/chat_adapters.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.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/chat/chat_module.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/message.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/message.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/room.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/room.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/pages/chat_page.dart
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page.dart
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page_embedded.dart
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_config_loader.dart
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_info_service.dart
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_service.dart
file:///home/pierre/dev/geosector/app/lib/chat/widgets/recipient_selector.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.g.dart
@@ -1926,12 +1951,14 @@ file:///home/pierre/dev/geosector/app/lib/presentation/dialogs/sector_dialog.dar
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_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_map_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_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/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/charts.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
@@ -1942,9 +1969,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_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/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/custom_button.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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/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/go_router-16.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/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.1.0/lib/src/state.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/go_router.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/builder.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/configuration.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/delegate.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/information_provider.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/logging.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/match.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/custom_parameter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/error_screen.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/errors.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/extensions.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/misc/inherited_router.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/pages/cupertino.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/pages/custom_transition_page.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/pages/material.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/parser.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/path_utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/route.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/route_data.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/src/router.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/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/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
@@ -502,11 +502,11 @@ 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/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/image_picker-1.1.2/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.0.6/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.0.6/lib/src/pkg_web_tweaks.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.1.0/lib/image_picker_for_web.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.1.0/lib/src/image_resizer_utils.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.11.0/lib/image_picker_platform_interface.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.11.0/lib/src/platform_interface/image_picker_platform.dart
@@ -737,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/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/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/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
@@ -774,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/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/syncfusion_flutter_charts-30.2.5/lib/charts.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/datetime_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/datetime_category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/logarithmic_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/multi_level_labels.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/numeric_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/axis/plot_band.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/behaviors/crosshair.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/behaviors/trackball.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/behaviors/zooming.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/cartesian_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/circular_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/annotation.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/callbacks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/chart_point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/circular_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/circular_data_label_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/connector_line.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/core_legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/core_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/element_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/empty_points.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/funnel_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/interactive_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/layout_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/pyramid_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/common/title.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/funnel_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/accumulation_distribution_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/atr_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/bollinger_bands_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/ema_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/macd_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/momentum_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/roc_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/rsi_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/sma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/stochastic_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/technical_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/tma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/indicators/wma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/interactions/behavior.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/interactions/selection.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/interactions/tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/pyramid_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/box_and_whisker_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/bubble_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/candle_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/chart_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/doughnut_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/error_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/fast_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/funnel_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/hilo_open_close_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/hilo_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/histogram_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/pie_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/pyramid_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/radial_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/range_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/range_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/scatter_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/spline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_area100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_bar100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_column100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_line100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stacked_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/step_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/stepline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/series/waterfall_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/trendline/trendline.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/constants.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/renderer_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/typedef.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/charts/utils/zooming_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/sparkline/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/sparkline/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/src/sparkline/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/calendar/calendar_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/calendar/hijri_date_time.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/localizations/global_localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/slider_controller.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/assistview_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/barcodes_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/calendar_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/chat_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/color_scheme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/datagrid_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/datapager_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/daterangepicker_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/gauges_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/maps_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/pdfviewer_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/range_selector_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/range_slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/spark_charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/theme_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/theme/treemap_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/src/utils/shape_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/charts.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/datetime_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/datetime_category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/logarithmic_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/multi_level_labels.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/numeric_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/axis/plot_band.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/behaviors/crosshair.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/behaviors/trackball.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/behaviors/zooming.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/cartesian_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/circular_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/annotation.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/callbacks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/chart_point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/circular_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/circular_data_label_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/connector_line.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/core_legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/core_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/element_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/empty_points.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/funnel_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/interactive_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/layout_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/pyramid_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/common/title.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/funnel_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/accumulation_distribution_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/atr_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/bollinger_bands_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/ema_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/macd_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/momentum_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/roc_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/rsi_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/sma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/stochastic_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/technical_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/tma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/indicators/wma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/interactions/behavior.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/interactions/selection.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/interactions/tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/pyramid_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/box_and_whisker_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/bubble_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/candle_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/chart_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/doughnut_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/error_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/fast_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/funnel_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/hilo_open_close_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/hilo_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/histogram_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/pie_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/pyramid_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/radial_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/range_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/range_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/scatter_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/spline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_area100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_bar100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_column100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_line100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stacked_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/step_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/stepline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/series/waterfall_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/trendline/trendline.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/constants.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/renderer_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/typedef.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/charts/utils/zooming_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/sparkline/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/sparkline/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/src/sparkline/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/calendar/calendar_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/calendar/hijri_date_time.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/localizations/global_localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/slider_controller.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/assistview_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/barcodes_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/calendar_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/chat_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/color_scheme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/datagrid_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/datapager_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/daterangepicker_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/gauges_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/maps_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/pdfviewer_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/range_selector_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/range_slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/spark_charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/theme_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/theme/treemap_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/src/utils/shape_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/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/glyph_set.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
@@ -988,7 +1001,6 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_m
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.1.4/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/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/accelerometer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
@@ -1183,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/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/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/packages/flutter/lib/animation.dart
file:///home/pierre/dev/flutter/packages/flutter/lib/cupertino.dart
@@ -1851,21 +1879,18 @@ file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/main.dart
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/web_plugin_registrant.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/models/anonymous_user_model.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/chat_adapters.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.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/chat/chat_module.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/message.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/message.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/room.dart
file:///home/pierre/dev/geosector/app/lib/chat/models/room.g.dart
file:///home/pierre/dev/geosector/app/lib/chat/pages/chat_page.dart
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page.dart
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page_embedded.dart
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_config_loader.dart
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_info_service.dart
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_service.dart
file:///home/pierre/dev/geosector/app/lib/chat/widgets/recipient_selector.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.g.dart
@@ -1925,12 +1950,14 @@ file:///home/pierre/dev/geosector/app/lib/presentation/dialogs/sector_dialog.dar
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_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_map_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_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/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/charts.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
@@ -1941,9 +1968,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_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/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/custom_button.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart

View File

@@ -1 +1 @@
["/home/pierre/dev/geosector/app/build/web/*/index.html","/home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js","/home/pierre/dev/geosector/app/build/web/main.dart.js","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png-autosave.kra","/home/pierre/dev/geosector/app/build/web/assets/assets/images/icon-geosector.svg","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector_map_admin.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo_recu.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector-logo.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-1024.png","/home/pierre/dev/geosector/app/build/web/assets/assets/animations/geo_main.json","/home/pierre/dev/geosector/app/build/web/assets/assets/fonts/Figtree-VariableFont_wght.ttf","/home/pierre/dev/geosector/app/build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf","/home/pierre/dev/geosector/app/build/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png","/home/pierre/dev/geosector/app/build/web/assets/fonts/MaterialIcons-Regular.otf","/home/pierre/dev/geosector/app/build/web/assets/shaders/ink_sparkle.frag","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.json","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin.json","/home/pierre/dev/geosector/app/build/web/assets/FontManifest.json","/home/pierre/dev/geosector/app/build/web/assets/NOTICES","/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-152.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-180.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-167.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-512.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png","/home/pierre/dev/geosector/app/build/web/favicon-64.png","/home/pierre/dev/geosector/app/build/web/.DS_Store","/home/pierre/dev/geosector/app/build/web/favicon-32.png","/home/pierre/dev/geosector/app/build/web/favicon.png","/home/pierre/dev/geosector/app/build/web/favicon-16.png","/home/pierre/dev/geosector/app/build/web/manifest.json","/home/pierre/dev/geosector/app/build/web/flutter_service_worker.js"]
["/home/pierre/dev/geosector/app/build/web/*/index.html","/home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js","/home/pierre/dev/geosector/app/build/web/main.dart.js","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png-autosave.kra","/home/pierre/dev/geosector/app/build/web/assets/assets/images/icon-geosector.svg","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector_map_admin.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo_recu.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector-logo.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-1024.png","/home/pierre/dev/geosector/app/build/web/assets/assets/animations/geo_main.json","/home/pierre/dev/geosector/app/build/web/assets/lib/chat/chat_config.yaml","/home/pierre/dev/geosector/app/build/web/assets/assets/fonts/Figtree-VariableFont_wght.ttf","/home/pierre/dev/geosector/app/build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf","/home/pierre/dev/geosector/app/build/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png","/home/pierre/dev/geosector/app/build/web/assets/fonts/MaterialIcons-Regular.otf","/home/pierre/dev/geosector/app/build/web/assets/shaders/ink_sparkle.frag","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.json","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin.json","/home/pierre/dev/geosector/app/build/web/assets/FontManifest.json","/home/pierre/dev/geosector/app/build/web/assets/NOTICES","/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-152.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-180.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-167.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-512.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png","/home/pierre/dev/geosector/app/build/web/favicon-64.png","/home/pierre/dev/geosector/app/build/web/.DS_Store","/home/pierre/dev/geosector/app/build/web/favicon-32.png","/home/pierre/dev/geosector/app/build/web/favicon.png","/home/pierre/dev/geosector/app/build/web/favicon-16.png","/home/pierre/dev/geosector/app/build/web/manifest.json","/home/pierre/dev/geosector/app/build/web/flutter_service_worker.js"]

View File

@@ -1 +1 @@
/home/pierre/dev/geosector/app/build/web/flutter_service_worker.js: /home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png /home/pierre/dev/geosector/app/build/web/icons/Icon-192.png /home/pierre/dev/geosector/app/build/web/icons/Icon-152.png /home/pierre/dev/geosector/app/build/web/icons/Icon-180.png /home/pierre/dev/geosector/app/build/web/icons/Icon-167.png /home/pierre/dev/geosector/app/build/web/icons/Icon-512.png /home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png /home/pierre/dev/geosector/app/build/web/flutter.js /home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js /home/pierre/dev/geosector/app/build/web/favicon-64.png /home/pierre/dev/geosector/app/build/web/index.html /home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js /home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js /home/pierre/dev/geosector/app/build/web/favicon-32.png /home/pierre/dev/geosector/app/build/web/version.json /home/pierre/dev/geosector/app/build/web/favicon.png /home/pierre/dev/geosector/app/build/web/favicon-16.png /home/pierre/dev/geosector/app/build/web/assets/AssetManifest.json /home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin /home/pierre/dev/geosector/app/build/web/assets/fonts/MaterialIcons-Regular.otf /home/pierre/dev/geosector/app/build/web/assets/FontManifest.json /home/pierre/dev/geosector/app/build/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png /home/pierre/dev/geosector/app/build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png-autosave.kra /home/pierre/dev/geosector/app/build/web/assets/assets/images/icon-geosector.svg /home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector_map_admin.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo_recu.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector-logo.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-1024.png /home/pierre/dev/geosector/app/build/web/assets/assets/fonts/Figtree-VariableFont_wght.ttf /home/pierre/dev/geosector/app/build/web/assets/assets/animations/geo_main.json /home/pierre/dev/geosector/app/build/web/assets/shaders/ink_sparkle.frag /home/pierre/dev/geosector/app/build/web/assets/NOTICES /home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin.json /home/pierre/dev/geosector/app/build/web/main.dart.js /home/pierre/dev/geosector/app/build/web/manifest.json
/home/pierre/dev/geosector/app/build/web/flutter_service_worker.js: /home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png /home/pierre/dev/geosector/app/build/web/icons/Icon-192.png /home/pierre/dev/geosector/app/build/web/icons/Icon-152.png /home/pierre/dev/geosector/app/build/web/icons/Icon-180.png /home/pierre/dev/geosector/app/build/web/icons/Icon-167.png /home/pierre/dev/geosector/app/build/web/icons/Icon-512.png /home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png /home/pierre/dev/geosector/app/build/web/flutter.js /home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js /home/pierre/dev/geosector/app/build/web/favicon-64.png /home/pierre/dev/geosector/app/build/web/index.html /home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js /home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js /home/pierre/dev/geosector/app/build/web/favicon-32.png /home/pierre/dev/geosector/app/build/web/version.json /home/pierre/dev/geosector/app/build/web/favicon.png /home/pierre/dev/geosector/app/build/web/favicon-16.png /home/pierre/dev/geosector/app/build/web/assets/AssetManifest.json /home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin /home/pierre/dev/geosector/app/build/web/assets/fonts/MaterialIcons-Regular.otf /home/pierre/dev/geosector/app/build/web/assets/FontManifest.json /home/pierre/dev/geosector/app/build/web/assets/lib/chat/chat_config.yaml /home/pierre/dev/geosector/app/build/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png /home/pierre/dev/geosector/app/build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png-autosave.kra /home/pierre/dev/geosector/app/build/web/assets/assets/images/icon-geosector.svg /home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector_map_admin.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo_recu.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector-logo.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-1024.png /home/pierre/dev/geosector/app/build/web/assets/assets/fonts/Figtree-VariableFont_wght.ttf /home/pierre/dev/geosector/app/build/web/assets/assets/animations/geo_main.json /home/pierre/dev/geosector/app/build/web/assets/shaders/ink_sparkle.frag /home/pierre/dev/geosector/app/build/web/assets/NOTICES /home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin.json /home/pierre/dev/geosector/app/build/web/main.dart.js /home/pierre/dev/geosector/app/build/web/manifest.json

View File

@@ -10,6 +10,7 @@ import 'package:connectivity_plus/src/connectivity_plus_web.dart';
import 'package:geolocator_web/geolocator_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:sensors_plus/src/sensors_plus_web.dart';
import 'package:shared_preferences_web/shared_preferences_web.dart';
import 'package:url_launcher_web/url_launcher_web.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
@@ -20,6 +21,7 @@ void registerPlugins([final Registrar? pluginRegistrar]) {
GeolocatorPlugin.registerWith(registrar);
ImagePickerPlugin.registerWith(registrar);
PackageInfoPlusWebPlugin.registerWith(registrar);
WebSensorsPlugin.registerWith(registrar);
SharedPreferencesPlugin.registerWith(registrar);
UrlLauncherPlugin.registerWith(registrar);
registrar.registerMessageHandler();

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"inputs":["/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-152.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-180.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-167.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-512.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png","/home/pierre/dev/geosector/app/build/web/flutter.js","/home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js","/home/pierre/dev/geosector/app/build/web/favicon-64.png","/home/pierre/dev/geosector/app/build/web/index.html","/home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js","/home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js","/home/pierre/dev/geosector/app/build/web/favicon-32.png","/home/pierre/dev/geosector/app/build/web/version.json","/home/pierre/dev/geosector/app/build/web/favicon.png","/home/pierre/dev/geosector/app/build/web/favicon-16.png","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.json","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin","/home/pierre/dev/geosector/app/build/web/assets/fonts/MaterialIcons-Regular.otf","/home/pierre/dev/geosector/app/build/web/assets/FontManifest.json","/home/pierre/dev/geosector/app/build/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png","/home/pierre/dev/geosector/app/build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png-autosave.kra","/home/pierre/dev/geosector/app/build/web/assets/assets/images/icon-geosector.svg","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector_map_admin.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo_recu.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector-logo.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-1024.png","/home/pierre/dev/geosector/app/build/web/assets/assets/fonts/Figtree-VariableFont_wght.ttf","/home/pierre/dev/geosector/app/build/web/assets/assets/animations/geo_main.json","/home/pierre/dev/geosector/app/build/web/assets/shaders/ink_sparkle.frag","/home/pierre/dev/geosector/app/build/web/assets/NOTICES","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin.json","/home/pierre/dev/geosector/app/build/web/main.dart.js","/home/pierre/dev/geosector/app/build/web/manifest.json"],"outputs":["/home/pierre/dev/geosector/app/build/web/flutter_service_worker.js"]}
{"inputs":["/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-152.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-180.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-167.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-512.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png","/home/pierre/dev/geosector/app/build/web/flutter.js","/home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js","/home/pierre/dev/geosector/app/build/web/favicon-64.png","/home/pierre/dev/geosector/app/build/web/index.html","/home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js","/home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js","/home/pierre/dev/geosector/app/build/web/favicon-32.png","/home/pierre/dev/geosector/app/build/web/version.json","/home/pierre/dev/geosector/app/build/web/favicon.png","/home/pierre/dev/geosector/app/build/web/favicon-16.png","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.json","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin","/home/pierre/dev/geosector/app/build/web/assets/fonts/MaterialIcons-Regular.otf","/home/pierre/dev/geosector/app/build/web/assets/FontManifest.json","/home/pierre/dev/geosector/app/build/web/assets/lib/chat/chat_config.yaml","/home/pierre/dev/geosector/app/build/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png","/home/pierre/dev/geosector/app/build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png-autosave.kra","/home/pierre/dev/geosector/app/build/web/assets/assets/images/icon-geosector.svg","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector_map_admin.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo_recu.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector-logo.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-1024.png","/home/pierre/dev/geosector/app/build/web/assets/assets/fonts/Figtree-VariableFont_wght.ttf","/home/pierre/dev/geosector/app/build/web/assets/assets/animations/geo_main.json","/home/pierre/dev/geosector/app/build/web/assets/shaders/ink_sparkle.frag","/home/pierre/dev/geosector/app/build/web/assets/NOTICES","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin.json","/home/pierre/dev/geosector/app/build/web/main.dart.js","/home/pierre/dev/geosector/app/build/web/manifest.json"],"outputs":["/home/pierre/dev/geosector/app/build/web/flutter_service_worker.js"]}

View File

@@ -0,0 +1,372 @@
//
// Generated file. Do not edit.
// This file is generated from template in file `flutter_tools/lib/src/flutter_plugins.dart`.
//
// @dart = 3.0
import 'dart:io'; // flutter_ignore: dart_io_import.
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:geolocator_android/geolocator_android.dart';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:path_provider_android/path_provider_android.dart';
import 'package:shared_preferences_android/shared_preferences_android.dart';
import 'package:url_launcher_android/url_launcher_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:geolocator_apple/geolocator_apple.dart';
import 'package:image_picker_ios/image_picker_ios.dart';
import 'package:path_provider_foundation/path_provider_foundation.dart';
import 'package:shared_preferences_foundation/shared_preferences_foundation.dart';
import 'package:url_launcher_ios/url_launcher_ios.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:file_selector_linux/file_selector_linux.dart';
import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart';
import 'package:geolocator_linux/geolocator_linux.dart';
import 'package:image_picker_linux/image_picker_linux.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider_linux/path_provider_linux.dart';
import 'package:shared_preferences_linux/shared_preferences_linux.dart';
import 'package:url_launcher_linux/url_launcher_linux.dart';
import 'package:file_selector_macos/file_selector_macos.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:geolocator_apple/geolocator_apple.dart';
import 'package:image_picker_macos/image_picker_macos.dart';
import 'package:path_provider_foundation/path_provider_foundation.dart';
import 'package:shared_preferences_foundation/shared_preferences_foundation.dart';
import 'package:url_launcher_macos/url_launcher_macos.dart';
import 'package:file_selector_windows/file_selector_windows.dart';
import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart';
import 'package:image_picker_windows/image_picker_windows.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider_windows/path_provider_windows.dart';
import 'package:shared_preferences_windows/shared_preferences_windows.dart';
import 'package:url_launcher_windows/url_launcher_windows.dart';
@pragma('vm:entry-point')
class _PluginRegistrant {
@pragma('vm:entry-point')
static void register() {
if (Platform.isAndroid) {
try {
AndroidFlutterLocalNotificationsPlugin.registerWith();
} catch (err) {
print(
'`flutter_local_notifications` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
GeolocatorAndroid.registerWith();
} catch (err) {
print(
'`geolocator_android` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
ImagePickerAndroid.registerWith();
} catch (err) {
print(
'`image_picker_android` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
PathProviderAndroid.registerWith();
} catch (err) {
print(
'`path_provider_android` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
SharedPreferencesAndroid.registerWith();
} catch (err) {
print(
'`shared_preferences_android` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
UrlLauncherAndroid.registerWith();
} catch (err) {
print(
'`url_launcher_android` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
} else if (Platform.isIOS) {
try {
IOSFlutterLocalNotificationsPlugin.registerWith();
} catch (err) {
print(
'`flutter_local_notifications` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
GeolocatorApple.registerWith();
} catch (err) {
print(
'`geolocator_apple` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
ImagePickerIOS.registerWith();
} catch (err) {
print(
'`image_picker_ios` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
PathProviderFoundation.registerWith();
} catch (err) {
print(
'`path_provider_foundation` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
SharedPreferencesFoundation.registerWith();
} catch (err) {
print(
'`shared_preferences_foundation` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
UrlLauncherIOS.registerWith();
} catch (err) {
print(
'`url_launcher_ios` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
} else if (Platform.isLinux) {
try {
ConnectivityPlusLinuxPlugin.registerWith();
} catch (err) {
print(
'`connectivity_plus` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
FileSelectorLinux.registerWith();
} catch (err) {
print(
'`file_selector_linux` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
LinuxFlutterLocalNotificationsPlugin.registerWith();
} catch (err) {
print(
'`flutter_local_notifications_linux` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
GeolocatorLinux.registerWith();
} catch (err) {
print(
'`geolocator_linux` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
ImagePickerLinux.registerWith();
} catch (err) {
print(
'`image_picker_linux` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
PackageInfoPlusLinuxPlugin.registerWith();
} catch (err) {
print(
'`package_info_plus` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
PathProviderLinux.registerWith();
} catch (err) {
print(
'`path_provider_linux` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
SharedPreferencesLinux.registerWith();
} catch (err) {
print(
'`shared_preferences_linux` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
UrlLauncherLinux.registerWith();
} catch (err) {
print(
'`url_launcher_linux` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
} else if (Platform.isMacOS) {
try {
FileSelectorMacOS.registerWith();
} catch (err) {
print(
'`file_selector_macos` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
MacOSFlutterLocalNotificationsPlugin.registerWith();
} catch (err) {
print(
'`flutter_local_notifications` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
GeolocatorApple.registerWith();
} catch (err) {
print(
'`geolocator_apple` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
ImagePickerMacOS.registerWith();
} catch (err) {
print(
'`image_picker_macos` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
PathProviderFoundation.registerWith();
} catch (err) {
print(
'`path_provider_foundation` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
SharedPreferencesFoundation.registerWith();
} catch (err) {
print(
'`shared_preferences_foundation` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
UrlLauncherMacOS.registerWith();
} catch (err) {
print(
'`url_launcher_macos` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
} else if (Platform.isWindows) {
try {
FileSelectorWindows.registerWith();
} catch (err) {
print(
'`file_selector_windows` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
FlutterLocalNotificationsWindows.registerWith();
} catch (err) {
print(
'`flutter_local_notifications_windows` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
ImagePickerWindows.registerWith();
} catch (err) {
print(
'`image_picker_windows` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
PackageInfoPlusWindowsPlugin.registerWith();
} catch (err) {
print(
'`package_info_plus` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
PathProviderWindows.registerWith();
} catch (err) {
print(
'`path_provider_windows` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
SharedPreferencesWindows.registerWith();
} catch (err) {
print(
'`shared_preferences_windows` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
try {
UrlLauncherWindows.registerWith();
} catch (err) {
print(
'`url_launcher_windows` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
}
}
}

View File

@@ -223,12 +223,6 @@
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "event_bus",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/event_bus-2.0.1",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "fake_async",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/fake_async-1.3.3",
@@ -255,9 +249,9 @@
},
{
"name": "file_selector_macos",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+3",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+4",
"packageUri": "lib/",
"languageVersion": "3.6"
"languageVersion": "3.7"
},
{
"name": "file_selector_platform_interface",
@@ -351,9 +345,9 @@
},
{
"name": "flutter_svg",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.2.0",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.0.13",
"packageUri": "lib/",
"languageVersion": "3.6"
"languageVersion": "3.4"
},
{
"name": "flutter_test",
@@ -429,9 +423,9 @@
},
{
"name": "go_router",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0",
"packageUri": "lib/",
"languageVersion": "3.6"
"languageVersion": "3.7"
},
{
"name": "google_fonts",
@@ -513,39 +507,39 @@
},
{
"name": "image_picker",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.2.0",
"packageUri": "lib/",
"languageVersion": "3.3"
"languageVersion": "3.6"
},
{
"name": "image_picker_android",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+25",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.13",
"packageUri": "lib/",
"languageVersion": "3.6"
},
{
"name": "image_picker_for_web",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0",
"packageUri": "lib/",
"languageVersion": "3.4"
"languageVersion": "3.6"
},
{
"name": "image_picker_ios",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13",
"packageUri": "lib/",
"languageVersion": "3.4"
"languageVersion": "3.6"
},
{
"name": "image_picker_linux",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2",
"packageUri": "lib/",
"languageVersion": "3.4"
"languageVersion": "3.6"
},
{
"name": "image_picker_macos",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2",
"packageUri": "lib/",
"languageVersion": "3.4"
"languageVersion": "3.6"
},
{
"name": "image_picker_platform_interface",
@@ -555,9 +549,9 @@
},
{
"name": "image_picker_windows",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2",
"packageUri": "lib/",
"languageVersion": "2.19"
"languageVersion": "3.6"
},
{
"name": "intl",
@@ -667,12 +661,6 @@
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "mqtt5_client",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/mqtt5_client-4.14.0",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "nm",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/nm-0.5.0",
@@ -723,9 +711,9 @@
},
{
"name": "path_provider_foundation",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2",
"packageUri": "lib/",
"languageVersion": "3.3"
"languageVersion": "3.7"
},
{
"name": "path_provider_linux",
@@ -747,9 +735,9 @@
},
{
"name": "petitparser",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-6.1.0",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-7.0.1",
"packageUri": "lib/",
"languageVersion": "3.5"
"languageVersion": "3.8"
},
{
"name": "platform",
@@ -799,6 +787,18 @@
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "sensors_plus",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "sensors_plus_platform_interface",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1",
"packageUri": "lib/",
"languageVersion": "2.18"
},
{
"name": "shared_preferences",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3",
@@ -909,13 +909,13 @@
},
{
"name": "syncfusion_flutter_charts",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "syncfusion_flutter_core",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6",
"packageUri": "lib/",
"languageVersion": "3.7"
},
@@ -987,9 +987,9 @@
},
{
"name": "url_launcher_ios",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.3",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.4",
"packageUri": "lib/",
"languageVersion": "3.4"
"languageVersion": "3.7"
},
{
"name": "url_launcher_linux",
@@ -999,9 +999,9 @@
},
{
"name": "url_launcher_macos",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.3",
"packageUri": "lib/",
"languageVersion": "3.3"
"languageVersion": "3.7"
},
{
"name": "url_launcher_platform_interface",
@@ -1035,15 +1035,15 @@
},
{
"name": "vector_graphics_codec",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.13",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.11+1",
"packageUri": "lib/",
"languageVersion": "3.4"
"languageVersion": "2.17"
},
{
"name": "vector_graphics_compiler",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.17",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.11+1",
"packageUri": "lib/",
"languageVersion": "3.6"
"languageVersion": "2.19"
},
{
"name": "vector_math",
@@ -1101,9 +1101,9 @@
},
{
"name": "xml",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.5.0",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.6.1",
"packageUri": "lib/",
"languageVersion": "3.2"
"languageVersion": "3.8"
},
{
"name": "yaml",

View File

@@ -142,10 +142,6 @@ equatable
2.12
file:///home/pierre/.pub-cache/hosted/pub.dev/equatable-2.0.7/
file:///home/pierre/.pub-cache/hosted/pub.dev/equatable-2.0.7/lib/
event_bus
2.12
file:///home/pierre/.pub-cache/hosted/pub.dev/event_bus-2.0.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/event_bus-2.0.1/lib/
fake_async
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/fake_async-1.3.3/
@@ -163,9 +159,9 @@ file_selector_linux
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/lib/
file_selector_macos
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+3/
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+3/lib/
3.7
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+4/
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+4/lib/
file_selector_platform_interface
3.0
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_platform_interface-2.6.2/
@@ -219,9 +215,9 @@ flutter_plugin_android_lifecycle
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.29/
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.29/lib/
flutter_svg
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.2.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.2.0/lib/
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.0.13/
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.0.13/lib/
frontend_server_client
3.0
file:///home/pierre/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0/
@@ -263,9 +259,9 @@ glob
file:///home/pierre/.pub-cache/hosted/pub.dev/glob-2.1.3/
file:///home/pierre/.pub-cache/hosted/pub.dev/glob-2.1.3/lib/
go_router
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/
3.7
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/
google_fonts
2.14
file:///home/pierre/.pub-cache/hosted/pub.dev/google_fonts-6.3.0/
@@ -319,37 +315,37 @@ image
file:///home/pierre/.pub-cache/hosted/pub.dev/image-4.5.4/
file:///home/pierre/.pub-cache/hosted/pub.dev/image-4.5.4/lib/
image_picker
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/lib/
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.2.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.2.0/lib/
image_picker_android
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+25/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+25/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.13/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.13/lib/
image_picker_for_web
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/
image_picker_ios
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/lib/
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13/lib/
image_picker_linux
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/lib/
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/lib/
image_picker_macos
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2/lib/
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2/lib/
image_picker_platform_interface
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/
image_picker_windows
2.19
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/lib/
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2/lib/
intl
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/
@@ -422,10 +418,6 @@ mime
3.2
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/
mqtt5_client
3.8
file:///home/pierre/.pub-cache/hosted/pub.dev/mqtt5_client-4.14.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/mqtt5_client-4.14.0/lib/
nm
2.12
file:///home/pierre/.pub-cache/hosted/pub.dev/nm-0.5.0/
@@ -459,9 +451,9 @@ path_provider_android
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/lib/
path_provider_foundation
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/lib/
3.7
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/lib/
path_provider_linux
2.19
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/
@@ -475,9 +467,9 @@ path_provider_windows
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/lib/
petitparser
3.5
file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-6.1.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-6.1.0/lib/
3.8
file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-7.0.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-7.0.1/lib/
platform
3.2
file:///home/pierre/.pub-cache/hosted/pub.dev/platform-3.1.6/
@@ -510,6 +502,14 @@ retry
3.0
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/
sensors_plus
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/
sensors_plus_platform_interface
2.18
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/
shared_preferences
3.5
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/
@@ -580,12 +580,12 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/
syncfusion_flutter_charts
3.7
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.6/lib/
syncfusion_flutter_core
3.7
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.5/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.6/lib/
synchronized
3.8
file:///home/pierre/.pub-cache/hosted/pub.dev/synchronized-3.4.0/
@@ -631,17 +631,17 @@ url_launcher_android
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.17/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.17/lib/
url_launcher_ios
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.3/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.3/lib/
3.7
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.4/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.4/lib/
url_launcher_linux
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/lib/
url_launcher_macos
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/lib/
3.7
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.3/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.3/lib/
url_launcher_platform_interface
3.1
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_platform_interface-2.3.2/
@@ -663,13 +663,13 @@ vector_graphics
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics-1.1.19/
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics-1.1.19/lib/
vector_graphics_codec
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.13/
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.13/lib/
2.17
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.11+1/
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.11+1/lib/
vector_graphics_compiler
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.17/
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.17/lib/
2.19
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.11+1/
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.11+1/lib/
vector_math
2.14
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/
@@ -707,9 +707,9 @@ xdg_directories
file:///home/pierre/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0/lib/
xml
3.2
file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.5.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.5.0/lib/
3.8
file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.6.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.6.1/lib/
yaml
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/

View File

@@ -5,7 +5,7 @@
"packages": [
{
"name": "geosector_app",
"version": "3.0.8+308",
"version": "3.1.4+314",
"dependencies": [
"connectivity_plus",
"cupertino_icons",
@@ -26,15 +26,16 @@
"image_picker",
"intl",
"latlong2",
"mqtt5_client",
"package_info_plus",
"path_provider",
"retry",
"sensors_plus",
"shared_preferences",
"syncfusion_flutter_charts",
"universal_html",
"url_launcher",
"uuid"
"uuid",
"yaml"
],
"devDependencies": [
"build_runner",
@@ -104,17 +105,12 @@
]
},
{
"name": "image_picker",
"version": "1.1.2",
"name": "sensors_plus",
"version": "6.1.2",
"dependencies": [
"flutter",
"image_picker_android",
"image_picker_for_web",
"image_picker_ios",
"image_picker_linux",
"image_picker_macos",
"image_picker_platform_interface",
"image_picker_windows"
"flutter_web_plugins",
"sensors_plus_platform_interface"
]
},
{
@@ -390,6 +386,28 @@
"meta"
]
},
{
"name": "sensors_plus_platform_interface",
"version": "2.0.1",
"dependencies": [
"flutter",
"logging",
"meta",
"plugin_platform_interface"
]
},
{
"name": "flutter_web_plugins",
"version": "0.0.0",
"dependencies": [
"characters",
"collection",
"flutter",
"material_color_utilities",
"meta",
"vector_math"
]
},
{
"name": "universal_io",
"version": "2.2.2",
@@ -411,6 +429,14 @@
"package_info_plus"
]
},
{
"name": "geoclue",
"version": "0.1.1",
"dependencies": [
"dbus",
"meta"
]
},
{
"name": "equatable",
"version": "2.0.7",
@@ -425,11 +451,94 @@
"dependencies": []
},
{
"name": "geoclue",
"version": "0.1.1",
"name": "yaml",
"version": "3.1.3",
"dependencies": [
"dbus",
"meta"
"collection",
"source_span",
"string_scanner"
]
},
{
"name": "image_picker",
"version": "1.2.0",
"dependencies": [
"flutter",
"image_picker_android",
"image_picker_for_web",
"image_picker_ios",
"image_picker_linux",
"image_picker_macos",
"image_picker_platform_interface",
"image_picker_windows"
]
},
{
"name": "image_picker_windows",
"version": "0.2.2",
"dependencies": [
"file_selector_platform_interface",
"file_selector_windows",
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "image_picker_platform_interface",
"version": "2.11.0",
"dependencies": [
"cross_file",
"flutter",
"http",
"plugin_platform_interface"
]
},
{
"name": "image_picker_macos",
"version": "0.2.2",
"dependencies": [
"file_selector_macos",
"file_selector_platform_interface",
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "image_picker_linux",
"version": "0.2.2",
"dependencies": [
"file_selector_linux",
"file_selector_platform_interface",
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "image_picker_ios",
"version": "0.8.13",
"dependencies": [
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "image_picker_for_web",
"version": "3.1.0",
"dependencies": [
"flutter",
"flutter_web_plugins",
"image_picker_platform_interface",
"mime",
"web"
]
},
{
"name": "image_picker_android",
"version": "0.8.13",
"dependencies": [
"flutter",
"flutter_plugin_android_lifecycle",
"image_picker_platform_interface"
]
},
{
@@ -441,6 +550,11 @@
"synchronized"
]
},
{
"name": "sprintf",
"version": "7.0.0",
"dependencies": []
},
{
"name": "flutter_map_cache",
"version": "2.0.0+1",
@@ -465,6 +579,16 @@
"url_launcher_windows"
]
},
{
"name": "uuid",
"version": "4.5.1",
"dependencies": [
"crypto",
"fixnum",
"meta",
"sprintf"
]
},
{
"name": "package_info_plus",
"version": "8.3.1",
@@ -490,43 +614,11 @@
"plugin_platform_interface"
]
},
{
"name": "flutter_web_plugins",
"version": "0.0.0",
"dependencies": [
"characters",
"collection",
"flutter",
"material_color_utilities",
"meta",
"vector_math"
]
},
{
"name": "go_router",
"version": "16.1.0",
"dependencies": [
"collection",
"flutter",
"flutter_web_plugins",
"logging",
"meta"
]
},
{
"name": "cupertino_icons",
"version": "1.0.8",
"dependencies": []
},
{
"name": "yaml",
"version": "3.1.3",
"dependencies": [
"collection",
"source_span",
"string_scanner"
]
},
{
"name": "cli_util",
"version": "0.4.2",
@@ -545,15 +637,17 @@
]
},
{
"name": "image_picker_windows",
"version": "0.2.1+1",
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"file_selector_platform_interface",
"file_selector_windows",
"flutter",
"image_picker_platform_interface"
"meta"
]
},
{
"name": "logging",
"version": "1.3.0",
"dependencies": []
},
{
"name": "typed_data",
"version": "1.4.0",
@@ -570,21 +664,6 @@
"uuid"
]
},
{
"name": "uuid",
"version": "4.5.1",
"dependencies": [
"crypto",
"fixnum",
"meta",
"sprintf"
]
},
{
"name": "sprintf",
"version": "7.0.0",
"dependencies": []
},
{
"name": "fixnum",
"version": "1.1.1",
@@ -680,24 +759,6 @@
"meta"
]
},
{
"name": "syncfusion_flutter_charts",
"version": "30.2.5",
"dependencies": [
"flutter",
"intl",
"syncfusion_flutter_core",
"vector_math"
]
},
{
"name": "syncfusion_flutter_core",
"version": "30.2.5",
"dependencies": [
"flutter",
"vector_math"
]
},
{
"name": "connectivity_plus",
"version": "6.1.5",
@@ -728,46 +789,20 @@
]
},
{
"name": "args",
"version": "2.7.0",
"dependencies": []
},
{
"name": "image_picker_platform_interface",
"version": "2.11.0",
"name": "go_router",
"version": "16.2.0",
"dependencies": [
"cross_file",
"collection",
"flutter",
"http",
"plugin_platform_interface"
]
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"flutter_web_plugins",
"logging",
"meta"
]
},
{
"name": "image_picker_macos",
"version": "0.2.1+2",
"dependencies": [
"file_selector_macos",
"file_selector_platform_interface",
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "image_picker_linux",
"version": "0.2.1+2",
"dependencies": [
"file_selector_linux",
"file_selector_platform_interface",
"flutter",
"image_picker_platform_interface"
]
"name": "args",
"version": "2.7.0",
"dependencies": []
},
{
"name": "csslib",
@@ -794,14 +829,6 @@
"web"
]
},
{
"name": "url_launcher_platform_interface",
"version": "2.3.2",
"dependencies": [
"flutter",
"plugin_platform_interface"
]
},
{
"name": "web",
"version": "1.1.1",
@@ -817,6 +844,14 @@
"web"
]
},
{
"name": "url_launcher_platform_interface",
"version": "2.3.2",
"dependencies": [
"flutter",
"plugin_platform_interface"
]
},
{
"name": "path_provider_windows",
"version": "2.3.0",
@@ -837,11 +872,21 @@
]
},
{
"name": "path_provider_foundation",
"version": "2.4.1",
"name": "syncfusion_flutter_charts",
"version": "30.2.6",
"dependencies": [
"flutter",
"path_provider_platform_interface"
"intl",
"syncfusion_flutter_core",
"vector_math"
]
},
{
"name": "syncfusion_flutter_core",
"version": "30.2.6",
"dependencies": [
"flutter",
"vector_math"
]
},
{
@@ -897,6 +942,11 @@
"xml"
]
},
{
"name": "mime",
"version": "2.0.0",
"dependencies": []
},
{
"name": "dio_cache_interceptor",
"version": "4.0.3",
@@ -906,19 +956,19 @@
]
},
{
"name": "url_launcher_macos",
"version": "3.2.2",
"name": "url_launcher_linux",
"version": "3.2.1",
"dependencies": [
"flutter",
"url_launcher_platform_interface"
]
},
{
"name": "url_launcher_linux",
"version": "3.2.1",
"name": "path_provider_foundation",
"version": "2.4.2",
"dependencies": [
"flutter",
"url_launcher_platform_interface"
"path_provider_platform_interface"
]
},
{
@@ -937,6 +987,14 @@
"url_launcher_platform_interface"
]
},
{
"name": "url_launcher_macos",
"version": "3.2.3",
"dependencies": [
"flutter",
"url_launcher_platform_interface"
]
},
{
"name": "flutter_local_notifications",
"version": "19.4.0",
@@ -980,15 +1038,6 @@
"xdg_directories"
]
},
{
"name": "xml",
"version": "6.5.0",
"dependencies": [
"collection",
"meta",
"petitparser"
]
},
{
"name": "timezone",
"version": "0.10.1",
@@ -1002,14 +1051,59 @@
"version": "2.1.4",
"dependencies": []
},
{
"name": "xml",
"version": "6.6.1",
"dependencies": [
"collection",
"meta",
"petitparser"
]
},
{
"name": "petitparser",
"version": "6.1.0",
"version": "7.0.1",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "flutter_svg",
"version": "2.0.13",
"dependencies": [
"flutter",
"http",
"vector_graphics",
"vector_graphics_codec",
"vector_graphics_compiler"
]
},
{
"name": "vector_graphics_compiler",
"version": "1.1.11+1",
"dependencies": [
"args",
"meta",
"path",
"path_parsing",
"vector_graphics_codec",
"xml"
]
},
{
"name": "vector_graphics_codec",
"version": "1.1.11+1",
"dependencies": []
},
{
"name": "path_parsing",
"version": "1.1.0",
"dependencies": [
"meta",
"vector_math"
]
},
{
"name": "geolocator_apple",
"version": "2.3.13",
@@ -1018,22 +1112,6 @@
"geolocator_platform_interface"
]
},
{
"name": "url_launcher_ios",
"version": "6.3.3",
"dependencies": [
"flutter",
"url_launcher_platform_interface"
]
},
{
"name": "xdg_directories",
"version": "1.1.0",
"dependencies": [
"meta",
"path"
]
},
{
"name": "file_selector_linux",
"version": "0.9.3+2",
@@ -1063,6 +1141,14 @@
"typed_data"
]
},
{
"name": "xdg_directories",
"version": "1.1.0",
"dependencies": [
"meta",
"path"
]
},
{
"name": "crypto",
"version": "3.0.6",
@@ -1071,9 +1157,12 @@
]
},
{
"name": "logging",
"version": "1.3.0",
"dependencies": []
"name": "url_launcher_ios",
"version": "6.3.4",
"dependencies": [
"flutter",
"url_launcher_platform_interface"
]
},
{
"name": "shared_preferences",
@@ -1166,45 +1255,25 @@
]
},
{
"name": "image_picker_for_web",
"version": "3.0.6",
"name": "source_helper",
"version": "1.3.5",
"dependencies": [
"flutter",
"flutter_web_plugins",
"image_picker_platform_interface",
"mime",
"web"
"analyzer",
"collection",
"source_gen"
]
},
{
"name": "mime",
"version": "2.0.0",
"dependencies": []
},
{
"name": "mqtt5_client",
"version": "4.14.0",
"name": "glob",
"version": "2.1.3",
"dependencies": [
"characters",
"crypto",
"event_bus",
"meta",
"async",
"collection",
"file",
"path",
"typed_data",
"universal_html",
"web"
"string_scanner"
]
},
{
"name": "event_bus",
"version": "2.0.1",
"dependencies": []
},
{
"name": "platform",
"version": "3.1.6",
"dependencies": []
},
{
"name": "file",
"version": "7.0.1",
@@ -1214,31 +1283,17 @@
]
},
{
"name": "url_launcher_web",
"version": "2.4.1",
"name": "platform",
"version": "3.1.6",
"dependencies": []
},
{
"name": "vector_graphics",
"version": "1.1.19",
"dependencies": [
"flutter",
"flutter_web_plugins",
"url_launcher_platform_interface",
"web"
]
},
{
"name": "file_selector_macos",
"version": "0.9.4+3",
"dependencies": [
"cross_file",
"file_selector_platform_interface",
"flutter"
]
},
{
"name": "source_helper",
"version": "1.3.5",
"dependencies": [
"analyzer",
"collection",
"source_gen"
"http",
"vector_graphics_codec"
]
},
{
@@ -1256,14 +1311,22 @@
]
},
{
"name": "glob",
"version": "2.1.3",
"name": "url_launcher_web",
"version": "2.4.1",
"dependencies": [
"async",
"collection",
"file",
"path",
"string_scanner"
"flutter",
"flutter_web_plugins",
"url_launcher_platform_interface",
"web"
]
},
{
"name": "file_selector_macos",
"version": "0.9.4+4",
"dependencies": [
"cross_file",
"file_selector_platform_interface",
"flutter"
]
},
{
@@ -1273,51 +1336,6 @@
"meta"
]
},
{
"name": "flutter_svg",
"version": "2.2.0",
"dependencies": [
"flutter",
"http",
"vector_graphics",
"vector_graphics_codec",
"vector_graphics_compiler"
]
},
{
"name": "vector_graphics_codec",
"version": "1.1.13",
"dependencies": []
},
{
"name": "vector_graphics_compiler",
"version": "1.1.17",
"dependencies": [
"args",
"meta",
"path",
"path_parsing",
"vector_graphics_codec",
"xml"
]
},
{
"name": "path_parsing",
"version": "1.1.0",
"dependencies": [
"meta",
"vector_math"
]
},
{
"name": "vector_graphics",
"version": "1.1.19",
"dependencies": [
"flutter",
"http",
"vector_graphics_codec"
]
},
{
"name": "path_provider_android",
"version": "2.2.17",
@@ -1713,21 +1731,6 @@
"stream_channel"
]
},
{
"name": "image_picker_ios",
"version": "0.8.12+2",
"dependencies": [
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "win32",
"version": "5.14.0",
"dependencies": [
"ffi"
]
},
{
"name": "file_selector_windows",
"version": "0.9.3+4",
@@ -1745,6 +1748,13 @@
"web"
]
},
{
"name": "win32",
"version": "5.14.0",
"dependencies": [
"ffi"
]
},
{
"name": "url_launcher_android",
"version": "6.3.17",
@@ -1753,15 +1763,6 @@
"url_launcher_platform_interface"
]
},
{
"name": "image_picker_android",
"version": "0.8.12+25",
"dependencies": [
"flutter",
"flutter_plugin_android_lifecycle",
"image_picker_platform_interface"
]
},
{
"name": "flutter_plugin_android_lifecycle",
"version": "2.0.29",

File diff suppressed because one or more lines are too long

339
app/ANDROID-GUIDE.md Normal file
View File

@@ -0,0 +1,339 @@
# 📱 Guide de Développement Android pour GeoSector
## 🚀 Configuration initiale
### Prérequis sur Debian/Ubuntu
```bash
# Installer les outils Android de base
sudo apt update
sudo apt install adb fastboot android-tools-adb android-tools-fastboot
# Pour le support MTP (transfert de fichiers)
sudo apt install mtp-tools jmtpfs gvfs-backends
# Vérifier l'installation
adb --version
```
### Configuration du téléphone Android
1. **Activer le mode développeur** :
- Aller dans `Paramètres > À propos du téléphone`
- Taper 7 fois sur `Numéro de build`
2. **Activer le débogage USB** :
- Aller dans `Paramètres > Options pour développeurs`
- Activer `Débogage USB`
- Activer `Installation via USB` (si disponible)
3. **Connexion USB** :
- Brancher le téléphone
- Autoriser le débogage sur le téléphone (popup)
- Choisir "Toujours autoriser depuis cet ordinateur"
## 🔍 Commandes ADB essentielles
### Vérification de la connexion
```bash
# Lister les devices connectés
adb devices
# Voir les infos détaillées du device
adb shell getprop | grep -E "model|version|manufacturer"
# Vérifier si le device est en USB
lsusb | grep -i samsung
```
### Installation et gestion des APK
```bash
# Installer un APK
adb install app-release.apk
# Installer en écrasant la version existante
adb install -r app-release.apk
# Installer un APK debug (avec permission de debug)
adb install -t app-debug.apk
# Désinstaller une application
adb uninstall fr.geosector.app2025
# Lister les packages installés
adb shell pm list packages | grep geosector
# Voir le chemin d'installation d'une app
adb shell pm path fr.geosector.app2025
```
## 📊 Logs et débogage en temps réel
### Voir TOUS les logs du téléphone
```bash
# Logs en temps réel (CTRL+C pour arrêter)
adb logcat
# Logs avec filtre sur votre app uniquement
adb logcat | grep -i geosector
# Logs Flutter uniquement (très utile !)
adb logcat | grep -E "flutter|dart"
```
### Logs Flutter spécifiques (RECOMMANDÉ)
```bash
# Méthode 1: Via Flutter directement (le plus pratique)
flutter logs
# Méthode 2: Filtrer par tag Flutter
adb logcat -s flutter
# Méthode 3: Logs avec niveau de verbosité
adb logcat "*:E" # Erreurs uniquement
adb logcat "*:W" # Warnings et erreurs
adb logcat "*:I" # Info, warnings et erreurs
```
### Logs avec timestamps et couleurs
```bash
# Avec timestamp
adb logcat -v time | grep -i geosector
# Format détaillé avec PID
adb logcat -v threadtime | grep -i geosector
# Sauvegarder les logs dans un fichier
adb logcat -d > logs_android.txt
```
### Nettoyer les logs
```bash
# Effacer le buffer de logs
adb logcat -c
# Puis relancer pour voir uniquement les nouveaux logs
adb logcat | grep -i geosector
```
## 🐛 Débogage avancé
### Lancer l'application depuis ADB
```bash
# Démarrer l'application
adb shell monkey -p fr.geosector.app2025 -c android.intent.category.LAUNCHER 1
# Ou avec am (Activity Manager)
adb shell am start -n fr.geosector.app2025/.MainActivity
# Forcer l'arrêt de l'application
adb shell am force-stop fr.geosector.app2025
```
### Capturer des screenshots
```bash
# Prendre une capture d'écran
adb shell screencap /sdcard/screenshot.png
adb pull /sdcard/screenshot.png ./screenshot.png
# Enregistrer une vidéo (appuyer CTRL+C pour arrêter)
adb shell screenrecord /sdcard/demo.mp4
adb pull /sdcard/demo.mp4 ./demo.mp4
```
### Informations système
```bash
# Voir l'utilisation mémoire
adb shell dumpsys meminfo fr.geosector.app2025
# Voir les permissions de l'app
adb shell dumpsys package fr.geosector.app2025 | grep permission
# Version d'Android
adb shell getprop ro.build.version.release
# Taille de l'écran
adb shell wm size
# Densité de l'écran
adb shell wm density
```
## 🔥 Hot Reload avec Flutter
### Développement en temps réel
```bash
# Lancer l'app en mode debug avec hot reload
flutter run
# Une fois lancé, utiliser ces commandes :
# r - Hot reload (rechargement rapide)
# R - Hot restart (redémarrage complet)
# h - Afficher l'aide
# q - Quitter
# p - Afficher/masquer la grille de construction
# o - Basculer iOS/Android
```
### Lancer sur un device spécifique
```bash
# Lister les devices disponibles
flutter devices
# Lancer sur un device spécifique
flutter run -d R3CY409BQBZ # Remplacer par votre device ID
# Lancer en mode release pour tester les performances
flutter run --release
```
## 🏗️ Build et compilation
### Générer les APK
```bash
# APK debug (non signé, pour tests)
flutter build apk --debug
# APK release (signé, optimisé)
flutter build apk --release
# APK séparés par architecture (plus petits)
flutter build apk --split-per-abi
# App Bundle pour Google Play Store
flutter build appbundle --release
```
### Analyser la taille de l'APK
```bash
# Voir la taille détaillée de l'APK
flutter build apk --analyze-size
# Utiliser l'outil APK Analyzer d'Android
java -jar ~/Android/Sdk/tools/bin/apkanalyzer.jar apk summary app-release.apk
```
## 🔧 Résolution de problèmes courants
### Device non détecté
```bash
# Redémarrer le serveur ADB
adb kill-server
adb start-server
adb devices
# Vérifier les règles udev (Linux)
ls -la /etc/udev/rules.d/ | grep android
# Ajouter les permissions utilisateur
sudo usermod -aG plugdev $USER
# Puis se déconnecter/reconnecter
```
### Permissions Android
```bash
# Accorder une permission manuellement
adb shell pm grant fr.geosector.app2025 android.permission.ACCESS_FINE_LOCATION
# Révoquer une permission
adb shell pm revoke fr.geosector.app2025 android.permission.ACCESS_FINE_LOCATION
# Lister toutes les permissions
adb shell pm list permissions -g
```
### Problème de signature
```bash
# Vérifier la signature de l'APK
jarsigner -verify -verbose -certs app-release.apk
# Voir le certificat
keytool -printcert -jarfile app-release.apk
```
## 📦 Transfert de fichiers
### Via ADB (recommandé pour dev)
```bash
# Envoyer un fichier vers le téléphone
adb push fichier.txt /sdcard/Download/
# Récupérer un fichier du téléphone
adb pull /sdcard/Download/fichier.txt ./
# Lister les fichiers
adb shell ls -la /sdcard/Download/
```
### Via MTP (pour Thunar)
```bash
# Monter le téléphone
jmtpfs ~/phone
# Démonter
fusermount -u ~/phone
# Ou redémarrer Thunar
thunar -q && thunar &
```
## 🎯 Commandes utiles pour GeoSector
### Logs spécifiques à l'application
```bash
# Voir les erreurs de l'API
adb logcat | grep -E "ApiService|ApiException"
# Voir les opérations Hive (base de données locale)
adb logcat | grep -i hive
# Voir les erreurs de géolocalisation
adb logcat | grep -E "geolocator|location"
# Tout voir pour GeoSector
adb logcat | grep -E "geosector|flutter" --color=always
```
### Script de déploiement rapide
Créer un fichier `deploy-android.sh` :
```bash
#!/bin/bash
echo "🚀 Déploiement sur Android..."
# Nettoyer et compiler
flutter clean
flutter pub get
flutter build apk --release
# Installer sur le device
adb install -r build/app/outputs/flutter-apk/app-release.apk
# Lancer l'application
adb shell am start -n fr.geosector.app2025/.MainActivity
# Afficher les logs
echo "📊 Logs en temps réel (CTRL+C pour arrêter)..."
adb logcat | grep -E "geosector|flutter" --color=always
```
Rendre exécutable : `chmod +x deploy-android.sh`
## 🏁 Workflow de développement recommandé
1. **Brancher le téléphone** et vérifier : `adb devices`
2. **Lancer en mode debug** : `flutter run`
3. **Modifier le code** et appuyer sur `r` pour hot reload
4. **Voir les logs** dans un autre terminal : `flutter logs`
5. **Tester la release** : `flutter run --release`
6. **Compiler l'APK final** : `flutter build apk --release`
7. **Installer** : `adb install -r app-release.apk`
## 💡 Tips & Tricks
- **Performance** : Toujours tester en mode `--release` pour juger les vraies performances
- **Logs** : Garder un terminal avec `flutter logs` ouvert en permanence pendant le dev
- **Hot Reload** : Fonctionne uniquement en mode debug, pas en release
- **Wi-Fi Debug** : Possible avec `adb connect <IP>:5555` après configuration
- **Multi-devices** : Flutter peut déployer sur plusieurs devices simultanément
---
*Guide créé pour le projet GeoSector - Mise à jour : Août 2025*

View File

@@ -1106,11 +1106,97 @@ Cette architecture garantit une gestion des membres robuste, sécurisée et intu
## 🗺️ Cartes et géolocalisation
Flutter Map : Rendu cartographique haute performance
Tuiles Mapbox : Cartographie détaillée et personnalisable
Géolocalisation temps réel : Suivi GPS des équipes
Secteurs géographiques : Visualisation et attribution dynamique
Passages géolocalisés : Enregistrement précis des distributions
### 🎯 Architecture cartographique
**Flutter Map** : Rendu cartographique haute performance
**Providers de tuiles** : Solution hybride Mapbox/OpenStreetMap
**Géolocalisation temps réel** : Suivi GPS des équipes
**Secteurs géographiques** : Visualisation et attribution dynamique
**Passages géolocalisés** : Enregistrement précis des distributions
### 🌍 Configuration des tuiles de carte
GEOSECTOR v2.0 utilise une **stratégie différenciée** pour l'affichage des tuiles de carte selon la plateforme :
#### **Configuration actuelle**
| Plateforme | Provider | URL Template | Raison |
|------------|----------|--------------|---------|
| **Web** | Mapbox | `https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/` | Token compatible avec l'API styles |
| **Mobile** | OpenStreetMap | `https://tile.openstreetmap.org/{z}/{x}/{y}.png` | Gratuit, sans restriction de token |
#### **Pourquoi cette approche ?**
1. **Problème de permissions Mapbox** : Les tokens Mapbox actuels (DEV/REC/PROD) n'ont pas les permissions pour l'API `styles/v1` qui retourne une erreur 403 sur mobile
2. **Solution pragmatique** : OpenStreetMap fonctionne parfaitement sans token et offre une qualité de carte équivalente
3. **Facilité de maintenance** : Pas besoin de gérer des tokens différents selon les environnements
### 💾 Système de cache des tuiles
Le cache fonctionne **pour les deux providers** (Mapbox et OpenStreetMap) :
#### **Configuration du cache**
```dart
// Dans mapbox_map.dart
CachedTileProvider(
store: FileCacheStore(cachePath),
maxStale: Duration(days: 30), // Tuiles valides 30 jours
)
```
#### **Caches séparés par provider**
- **Web (Mapbox)** : Cache dans `MapboxTileCache/`
- **Mobile (OSM)** : Cache dans `OSMTileCache/`
#### **Avantages du cache**
**Mode hors ligne** : Les zones visitées restent disponibles sans connexion
**Performance** : Chargement instantané des tuiles en cache
**Économie de données** : Pas de re-téléchargement inutile
**Fiabilité** : Fonctionne même avec une connexion instable
### 🔧 Widget MapboxMap centralisé
Le widget `MapboxMap` (`lib/presentation/widgets/mapbox_map.dart`) centralise toute la logique cartographique :
#### **Paramètres principaux**
```dart
MapboxMap(
initialPosition: LatLng(48.1173, -1.6778), // Rennes
initialZoom: 13.0,
markers: [...], // Marqueurs (passages, etc.)
polygons: [...], // Polygones (secteurs)
useOpenStreetMap: !kIsWeb, // OSM sur mobile, Mapbox sur web
showControls: true, // Boutons zoom/localisation
)
```
#### **Utilisation dans l'application**
- **admin_map_page.dart** : Carte d'administration avec édition de secteurs
- **user_map_page.dart** : Carte utilisateur avec visualisation des passages
- **user_field_mode_page.dart** : Mode terrain avec GPS et boussole
### 🔄 Migration future vers Mapbox complet
Si vous obtenez un token Mapbox avec les permissions appropriées :
1. **Retirer le paramètre** `useOpenStreetMap: !kIsWeb` dans les pages
2. **Le widget détectera automatiquement** et utilisera Mapbox partout
3. **Le cache continuera de fonctionner** avec le nouveau provider
### 📱 Mode terrain (Field Mode)
Le mode terrain offre des fonctionnalités avancées pour les équipes sur le terrain :
- **Indicateurs GPS** : Qualité du signal (GPS/Réseau) avec actualisation 5s
- **Mode boussole** : Orientation de la carte selon le magnétomètre
- **Markers optimisés** : Affichage simplifié (première lettre de rueBis)
- **Liste triée** : Passages organisés par distance
- **Cercles de distance** : Visualisation des zones de proximité
## 🔄 Synchronisation et réactivité

View File

@@ -1,194 +1,7 @@
# TODO-APP.md
## TODO-APP.md
## 📋 Liste des tâches à effectuer sur l'application GEOSECTOR
### 🔧 Migration du pattern de gestion des erreurs API
#### 🎯 Objectif
Appliquer le nouveau pattern de gestion des erreurs pour que tous les messages spécifiques de l'API soient correctement affichés aux utilisateurs (au lieu du message générique "Erreur inattendue").
---
### ✅ Repositories déjà migrés
- [x] **MembreRepository** (`lib/core/repositories/membre_repository.dart`)
- ✅ Conversion DioException → ApiException
- ✅ Simplification du code
- ✅ Logs avec LoggerService
---
### 📝 Repositories à migrer
#### 1. **UserRepository** (`lib/core/repositories/user_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `updateUser()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `syncUser()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `syncAllUsers()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
#### 2. **OperationRepository** (`lib/core/repositories/operation_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `createOperation()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `updateOperation()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `deleteOperation()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `activateOperation()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
#### 3. **PassageRepository** (`lib/core/repositories/passage_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `createPassage()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `updatePassage()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `updatePassageStatus()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `syncPassages()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
#### 4. **SectorRepository** (`lib/core/repositories/sector_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `createSector()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `updateSector()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `deleteSector()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `assignUserToSector()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
#### 5. **AmicaleRepository** (`lib/core/repositories/amicale_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `updateAmicale()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `createAmicale()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `syncAmicales()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
---
### 🛠️ Pattern à appliquer
#### ❌ Code à supprimer (ancien pattern)
```dart
try {
final response = await ApiService.instance.put('/endpoint', data: data);
if (response.statusCode == 200) {
// Traitement succès
return true;
}
// ⚠️ Ce code ne sera JAMAIS exécuté
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
if (responseData['status'] == 'error') {
throw Exception(responseData['message']);
}
}
return false;
} catch (e) {
debugPrint('Erreur: $e');
rethrow;
}
```
#### ✅ Nouveau pattern à utiliser
```dart
try {
final response = await ApiService.instance.put('/endpoint', data: data);
// Si on arrive ici, la requête a réussi (200/201)
// Traitement succès
return true;
} catch (e) {
// Log propre sans détails techniques
if (e is ApiException) {
LoggerService.error('Erreur lors de l\'opération: ${e.message}');
} else {
LoggerService.error('Erreur lors de l\'opération');
}
rethrow; // Propager pour affichage UI
}
```
---
### 📦 Imports nécessaires
Ajouter ces imports dans chaque repository si manquants :
```dart
import 'package:geosector_app/core/services/logger_service.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
```
---
### 🧪 Tests de validation
Pour chaque repository migré, tester :
1. **Test d'erreur 409 (Conflict)** :
- Créer/modifier avec un email déjà existant
- Vérifier que le message "Cet email est déjà utilisé" s'affiche
2. **Test d'erreur 400 (Bad Request)** :
- Envoyer des données invalides
- Vérifier que le message d'erreur spécifique s'affiche
3. **Test d'erreur réseau** :
- Couper la connexion
- Vérifier que "Problème de connexion réseau" s'affiche
4. **Test de succès** :
- Opération normale
- Vérifier que tout fonctionne comme avant
---
### 📊 Critères de succès
- [ ] Tous les repositories utilisent le nouveau pattern
- [ ] Plus aucun `debugPrint` dans les repositories (remplacé par `LoggerService`)
- [ ] Les messages d'erreur spécifiques de l'API s'affichent correctement
- [ ] Les logs en production sont désactivés (sauf erreurs)
- [ ] Le code est plus simple et maintenable
---
### 🔍 Commandes utiles pour vérifier
```bash
# Rechercher les anciens patterns à remplacer
grep -r "response.statusCode ==" lib/core/repositories/
grep -r "responseData\['status'\] == 'error'" lib/core/repositories/
grep -r "debugPrint" lib/core/repositories/
# Vérifier les imports manquants
grep -L "LoggerService" lib/core/repositories/*.dart
grep -L "ApiException" lib/core/repositories/*.dart
```
---
### 📅 Priorité
1. **Haute** : UserRepository (utilisé partout)
2. **Haute** : OperationRepository (fonctionnalité critique)
3. **Moyenne** : PassageRepository (utilisé fréquemment)
4. **Moyenne** : AmicaleRepository (gestion administrative)
5. **Basse** : SectorRepository (moins critique)
---
### 📝 Notes
- Cette migration améliore l'UX en affichant des messages d'erreur clairs
- Le LoggerService désactive automatiquement les logs en production
- Le code devient plus maintenable et cohérent
- Documenter dans README-APP.md une fois terminé
---
## 🧪 Tests unitaires Flutter à implémenter
### 📁 Structure des tests
@@ -197,95 +10,108 @@ Le dossier `/test` doit contenir des tests unitaires pour valider le comportemen
### 📝 Tests prioritaires à créer
#### 1. **Tests des Repositories** (`test/repositories/`)
#### 1\. **Tests des Repositories** (`test/repositories/`)
##### `test/repositories/membre_repository_test.dart`
- [ ] Test de création d'un membre avec succès
- [ ] Test de création avec email déjà existant (409)
- [ ] Test de mise à jour d'un membre
- [ ] Test de suppression d'un membre
- [ ] Test de gestion des erreurs réseau
- [ ] Test du cache Hive local
`test/repositories/membre_repository_test.dart`
##### `test/repositories/user_repository_test.dart`
- [ ] Test de connexion réussie
- [ ] Test de connexion avec mauvais identifiants
- [ ] Test de mise à jour du profil utilisateur
- [ ] Test de synchronisation des données
- [ ] Test de gestion de session
- Test de création d'un membre avec succès
- Test de création avec email déjà existant (409)
- Test de mise à jour d'un membre
- Test de suppression d'un membre
- Test de gestion des erreurs réseau
- Test du cache Hive local
##### `test/repositories/operation_repository_test.dart`
- [ ] Test de création d'opération
- [ ] Test d'activation/désactivation
- [ ] Test de récupération des opérations par amicale
- [ ] Test de suppression avec transfert de passages
`test/repositories/user_repository_test.dart`
#### 2. **Tests des Services** (`test/services/`)
- Test de connexion réussie
- Test de connexion avec mauvais identifiants
- Test de mise à jour du profil utilisateur
- Test de synchronisation des données
- Test de gestion de session
##### `test/services/api_service_test.dart`
- [ ] Test de détection d'environnement (DEV/REC/PROD)
- [ ] Test de gestion des sessions
- [ ] Test de conversion DioException vers ApiException
- [ ] Test des différents codes d'erreur HTTP
- [ ] Test du retry automatique
`test/repositories/operation_repository_test.dart`
##### `test/services/logger_service_test.dart`
- [ ] Test de désactivation des logs en PROD
- [ ] Test des différents niveaux de log
- [ ] Test du formatage des messages
- Test de création d'opération
- Test d'activation/désactivation
- Test de récupération des opérations par amicale
- Test de suppression avec transfert de passages
##### `test/services/hive_service_test.dart`
- [ ] Test d'initialisation des boîtes Hive
- [ ] Test de sauvegarde et récupération
- [ ] Test de suppression de données
- [ ] Test de migration de schéma
#### 2\. **Tests des Services** (`test/services/`)
#### 3. **Tests des Models** (`test/models/`)
`test/services/api_service_test.dart`
##### `test/models/membre_model_test.dart`
- [ ] Test de sérialisation JSON
- [ ] Test de désérialisation JSON
- [ ] Test de conversion vers UserModel
- [ ] Test des valeurs par défaut
- Test de détection d'environnement (DEV/REC/PROD)
- Test de gestion des sessions
- Test de conversion DioException vers ApiException
- Test des différents codes d'erreur HTTP
- Test du retry automatique
##### `test/models/amicale_model_test.dart`
- [ ] Test de sérialisation/désérialisation
- [ ] Test des nouveaux champs booléens (chk_mdp_manuel, chk_username_manuel)
- [ ] Test de validation des données
`test/services/logger_service_test.dart`
#### 4. **Tests des Widgets** (`test/widgets/`)
- Test de désactivation des logs en PROD
- Test des différents niveaux de log
- Test du formatage des messages
##### `test/widgets/user_form_test.dart`
- [ ] Test d'affichage conditionnel des champs (username/password)
- [ ] Test de validation du mot de passe (regex)
- [ ] Test de validation de l'username (pas d'espaces)
- [ ] Test de génération automatique d'username
- [ ] Test du mode création vs modification
`test/services/hive_service_test.dart`
##### `test/widgets/membre_table_widget_test.dart`
- [ ] Test d'affichage responsive (mobile vs web)
- [ ] Test de masquage des colonnes sur mobile
- [ ] Test des icônes de statut
- [ ] Test du tri et filtrage
- Test d'initialisation des boîtes Hive
- Test de sauvegarde et récupération
- Test de suppression de données
- Test de migration de schéma
#### 5. **Tests d'intégration** (`test/integration/`)
#### 3\. **Tests des Models** (`test/models/`)
##### `test/integration/auth_flow_test.dart`
- [ ] Test du flow complet de connexion
- [ ] Test de persistance de session
- [ ] Test de déconnexion
- [ ] Test de redirection après expiration
`test/models/membre_model_test.dart`
##### `test/integration/membre_management_test.dart`
- [ ] Test de création complète d'un membre
- [ ] Test de modification avec validation
- [ ] Test de suppression avec transfert
- [ ] Test de gestion des erreurs
- Test de sérialisation JSON
- Test de désérialisation JSON
- Test de conversion vers UserModel
- Test des valeurs par défaut
`test/models/amicale_model_test.dart`
- Test de sérialisation/désérialisation
- Test des nouveaux champs booléens (chk_mdp_manuel, chk_username_manuel)
- Test de validation des données
#### 4\. **Tests des Widgets** (`test/widgets/`)
`test/widgets/user_form_test.dart`
- Test d'affichage conditionnel des champs (username/password)
- Test de validation du mot de passe (regex)
- Test de validation de l'username (pas d'espaces)
- Test de génération automatique d'username
- Test du mode création vs modification
`test/widgets/membre_table_widget_test.dart`
- Test d'affichage responsive (mobile vs web)
- Test de masquage des colonnes sur mobile
- Test des icônes de statut
- Test du tri et filtrage
#### 5\. **Tests d'intégration** (`test/integration/`)
`test/integration/auth_flow_test.dart`
- Test du flow complet de connexion
- Test de persistance de session
- Test de déconnexion
- Test de redirection après expiration
`test/integration/membre_management_test.dart`
- Test de création complète d'un membre
- Test de modification avec validation
- Test de suppression avec transfert
- Test de gestion des erreurs
### 🛠️ Configuration des tests
#### Fichier `test/test_helpers.dart`
```dart
```plaintext
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:dio/dio.dart';
@@ -293,7 +119,7 @@ import 'package:hive_flutter/hive_flutter.dart';
// Mocks nécessaires
class MockDio extends Mock implements Dio {}
class MockBox<T> extends Mock implements Box<T> {}
class MockBox<t> extends Mock implements Box<t> {}
class MockApiService extends Mock implements ApiService {}
// Helper pour initialiser Hive en tests
@@ -332,7 +158,7 @@ class TestDataFactory {
### 📋 Commandes de test
```bash
```plaintext
# Lancer tous les tests
flutter test
@@ -349,16 +175,17 @@ open coverage/html/index.html
### 🎯 Objectifs de couverture
- [ ] **Repositories** : 80% minimum
- [ ] **Services** : 90% minimum
- [ ] **Models** : 95% minimum
- [ ] **Widgets critiques** : 70% minimum
- [ ] **Couverture globale** : 75% minimum
- **Repositories** : 80% minimum
- **Services** : 90% minimum
- **Models** : 95% minimum
- **Widgets critiques** : 70% minimum
- **Couverture globale** : 75% minimum
### 📚 Dépendances de test à ajouter
Dans `pubspec.yaml` :
```yaml
```plaintext
dev_dependencies:
flutter_test:
sdk: flutter
@@ -372,7 +199,8 @@ dev_dependencies:
### 🔄 Intégration CI/CD
Ajouter dans le pipeline CI :
```yaml
```plaintext
test:
stage: test
script:
@@ -391,9 +219,7 @@ test:
3. **Nommage** : Utiliser des noms descriptifs (test_should_xxx_when_yyy)
4. **AAA** : Suivre le pattern Arrange-Act-Assert
5. **Edge cases** : Tester les cas limites et erreurs
6. **Performance** : Les tests unitaires doivent être rapides (<100ms)
---
6. **Performance** : Les tests unitaires doivent être rapides (\<100ms)
## 💬 Module Chat en ligne GEOSECTOR
@@ -403,7 +229,7 @@ Le module chat est partiellement implémenté avec une architecture MQTT pour le
### 🏗️ Architecture existante
```
```plaintext
lib/chat/
├── models/ # ✅ Modèles créés avec Hive
│ ├── conversation_model.dart # Conversations (one-to-one, groupe, annonce)
@@ -429,60 +255,67 @@ lib/chat/
### 📝 Tâches de développement Chat
#### 1. **Finalisation des modèles Hive**
- [ ] Compléter les adaptateurs Hive pour tous les modèles
- [ ] Ajouter la gestion des pièces jointes (images, documents)
- [ ] Implémenter le modèle `AnonymousUserModel` pour les utilisateurs temporaires
- [ ] Ajouter les timestamps et statuts de lecture
#### 1\. **Finalisation des modèles Hive**
#### 2. **Repository ChatRepository**
- [ ] Implémenter `createConversation()` avec participants
- [ ] Implémenter `sendMessage()` avec queue hors ligne
- [ ] Implémenter `getConversations()` avec pagination
- [ ] Implémenter `getMessages()` avec lazy loading
- [ ] Ajouter la gestion des participants (ajout/suppression)
- [ ] Implémenter les annonces ciblées par groupe
- Compléter les adaptateurs Hive pour tous les modèles
- Ajouter la gestion des pièces jointes (images, documents)
- Implémenter le modèle `AnonymousUserModel` pour les utilisateurs temporaires
- Ajouter les timestamps et statuts de lecture
#### 3. **Services**
- [ ] Compléter `ChatApiService` avec endpoints REST
- [ ] Implémenter la synchronisation bidirectionnelle
- [ ] Configurer `OfflineQueueService` pour messages en attente
- [ ] Implémenter le retry automatique avec exponential backoff
- [ ] Ajouter la compression des images avant envoi
#### 2\. **Repository ChatRepository**
#### 4. **Notifications MQTT**
- [ ] Installer et configurer Mosquitto dans le container Incus
- [ ] Configurer SSL/TLS pour MQTT (port 8883)
- [ ] Implémenter l'authentification par token JWT
- [ ] Créer les topics par utilisateur/groupe/conversation
- [ ] Implémenter le système de présence (online/offline/typing)
- [ ] Ajouter les ACLs pour sécuriser les topics
- Implémenter `createConversation()` avec participants
- Implémenter `sendMessage()` avec queue hors ligne
- Implémenter `getConversations()` avec pagination
- Implémenter `getMessages()` avec lazy loading
- Ajouter la gestion des participants (ajout/suppression)
- Implémenter les annonces ciblées par groupe
#### 5. **Interface utilisateur**
- [ ] Créer `ChatScreen` avec liste de messages
- [ ] Implémenter `ConversationsList` avec badges non-lus
- [ ] Designer `MessageBubble` (texte, images, documents)
- [ ] Créer `ChatInput` avec:
- [ ] Saisie de texte avec emoji picker
- [ ] Bouton d'envoi de fichiers
- [ ] Indicateur "en train d'écrire"
- [ ] Enregistrement vocal (optionnel)
- [ ] Ajouter les animations (apparition messages, typing indicator)
- [ ] Implémenter le swipe pour répondre
- [ ] Ajouter la recherche dans les conversations
#### 3\. **Services**
#### 6. **Fonctionnalités avancées**
- [ ] Notifications push locales via `flutter_local_notifications`
- [ ] Chiffrement end-to-end des messages sensibles
- [ ] Réactions aux messages (emojis)
- [ ] Messages éphémères avec auto-suppression
- [ ] Partage de localisation en temps réel
- [ ] Appels audio/vidéo via WebRTC (phase 2)
- Compléter `ChatApiService` avec endpoints REST
- Implémenter la synchronisation bidirectionnelle
- Configurer `OfflineQueueService` pour messages en attente
- Implémenter le retry automatique avec exponential backoff
- Ajouter la compression des images avant envoi
#### 4\. **Notifications MQTT**
- Installer et configurer Mosquitto dans le container Incus
- Configurer SSL/TLS pour MQTT (port 8883)
- Implémenter l'authentification par token JWT
- Créer les topics par utilisateur/groupe/conversation
- Implémenter le système de présence (online/offline/typing)
- Ajouter les ACLs pour sécuriser les topics
#### 5\. **Interface utilisateur**
- Créer `ChatScreen` avec liste de messages
- Implémenter `ConversationsList` avec badges non-lus
- Designer `MessageBubble` (texte, images, documents)
- Créer `ChatInput` avec:
- Saisie de texte avec emoji picker
- Bouton d'envoi de fichiers
- Indicateur "en train d'écrire"
- Enregistrement vocal (optionnel)
- Ajouter les animations (apparition messages, typing indicator)
- Implémenter le swipe pour répondre
- Ajouter la recherche dans les conversations
#### 6\. **Fonctionnalités avancées**
- Notifications push locales via `flutter_local_notifications`
- Chiffrement end-to-end des messages sensibles
- Réactions aux messages (emojis)
- Messages éphémères avec auto-suppression
- Partage de localisation en temps réel
- Appels audio/vidéo via WebRTC (phase 2)
### 🔧 Configuration backend requise
#### Base de données
```sql
```plaintext
-- Tables à créer (voir chat_tables.sql)
CREATE TABLE chat_conversations (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -512,7 +345,8 @@ CREATE TABLE chat_participants (
```
#### Configuration MQTT
```bash
```plaintext
# Installation Mosquitto
apt-get install mosquitto mosquitto-clients
@@ -528,7 +362,7 @@ password_file /etc/mosquitto/passwd
### 📦 Dépendances à ajouter
```yaml
```plaintext
dependencies:
# MQTT et notifications
mqtt5_client: ^4.0.0
@@ -548,13 +382,12 @@ dependencies:
```
### 🧪 Tests à implémenter
- [ ] Tests unitaires des repositories
- [ ] Tests d'intégration MQTT
- [ ] Tests de performance (1000+ messages)
- [ ] Tests hors ligne/online
- [ ] Tests de sécurité (injection, XSS)
---
- Tests unitaires des repositories
- Tests d'intégration MQTT
- Tests de performance (1000+ messages)
- Tests hors ligne/online
- Tests de sécurité (injection, XSS)
## 💳 Module de paiement Stripe
@@ -571,38 +404,42 @@ Intégration de Stripe pour permettre aux amicales ayant activé `chk_stripe` d'
### 📝 Tâches de développement Stripe
#### 1. **Configuration initiale Stripe**
- [ ] Créer un compte Stripe Connect Platform
- [ ] Configurer les webhooks Stripe
- [ ] Mettre en place l'environnement de test (sandbox)
- [ ] Configurer les clés API (publishable/secret)
- [ ] Implémenter la gestion sécurisée des clés
#### 1\. **Configuration initiale Stripe**
#### 2. **Onboarding des amicales**
- [ ] Créer un workflow d'inscription Stripe pour les amicales
- [ ] Implémenter Stripe Connect Onboarding
- [ ] Gérer le KYC (Know Your Customer) requis par Stripe
- [ ] Stocker de manière sécurisée le `stripe_account_id`
- [ ] Créer une page de statut du compte Stripe
- [ ] Gérer les documents requis (RIB, statuts, etc.)
- Créer un compte Stripe Connect Platform
- Configurer les webhooks Stripe
- Mettre en place l'environnement de test (sandbox)
- Configurer les clés API (publishable/secret)
- Implémenter la gestion sécurisée des clés
#### 3. **Modèles de données**
- [ ] Créer `StripeAccountModel` pour les comptes Connect
- [ ] Créer `PaymentIntentModel` pour les intentions de paiement
- [ ] Créer `TransactionModel` pour l'historique
- [ ] Ajouter les champs Stripe dans `PassageModel`
- [ ] Implémenter la table des commissions
#### 2\. **Onboarding des amicales**
#### 4. **Service StripeService**
```dart
- Créer un workflow d'inscription Stripe pour les amicales
- Implémenter Stripe Connect Onboarding
- Gérer le KYC (Know Your Customer) requis par Stripe
- Stocker de manière sécurisée le `stripe_account_id`
- Créer une page de statut du compte Stripe
- Gérer les documents requis (RIB, statuts, etc.)
#### 3\. **Modèles de données**
- Créer `StripeAccountModel` pour les comptes Connect
- Créer `PaymentIntentModel` pour les intentions de paiement
- Créer `TransactionModel` pour l'historique
- Ajouter les champs Stripe dans `PassageModel`
- Implémenter la table des commissions
#### 4\. **Service StripeService**
```plaintext
class StripeService {
// Compte Connect
Future<String> createConnectAccount(AmicaleModel amicale);
Future<string> createConnectAccount(AmicaleModel amicale);
Future<void> updateAccountStatus(String accountId);
Future<String> createAccountLink(String accountId);
Future<string> createAccountLink(String accountId);
// Paiements
Future<PaymentIntent> createPaymentIntent({
Future<paymentintent> createPaymentIntent({
required double amount,
required String currency,
required String connectedAccountId,
@@ -616,17 +453,19 @@ class StripeService {
}
```
#### 5. **Interface de paiement dans PassageForm**
- [ ] Détecter si l'amicale accepte Stripe (`chk_stripe`)
- [ ] Ajouter l'option "Paiement par carte" dans le dropdown
- [ ] Intégrer Stripe Elements pour la saisie de carte
- [ ] Implémenter le flow de paiement 3D Secure
- [ ] Gérer les erreurs de paiement
- [ ] Afficher le reçu de paiement
- [ ] Permettre l'envoi du reçu par email
#### 5\. **Interface de paiement dans PassageForm**
#### 6. **Widget StripePaymentSheet**
```dart
- Détecter si l'amicale accepte Stripe (`chk_stripe`)
- Ajouter l'option "Paiement par carte" dans le dropdown
- Intégrer Stripe Elements pour la saisie de carte
- Implémenter le flow de paiement 3D Secure
- Gérer les erreurs de paiement
- Afficher le reçu de paiement
- Permettre l'envoi du reçu par email
#### 6\. **Widget StripePaymentSheet**
```plaintext
class StripePaymentSheet extends StatefulWidget {
final double amount;
final String currency;
@@ -643,15 +482,17 @@ class StripePaymentSheet extends StatefulWidget {
}
```
#### 7. **Tableau de bord financier**
- [ ] Page de suivi des transactions Stripe
- [ ] Graphiques des paiements par période
- [ ] Export des transactions (CSV/Excel)
- [ ] Calcul automatique des commissions
- [ ] Rapprochement bancaire
- [ ] Dashboard temps réel des paiements
#### 7\. **Tableau de bord financier**
- Page de suivi des transactions Stripe
- Graphiques des paiements par période
- Export des transactions (CSV/Excel)
- Calcul automatique des commissions
- Rapprochement bancaire
- Dashboard temps réel des paiements
#### 8\. **Webhooks Stripe**
#### 8. **Webhooks Stripe**
```php
// Backend PHP pour gérer les webhooks
class StripeWebhookHandler {
@@ -664,26 +505,29 @@ class StripeWebhookHandler {
}
```
#### 9. **Sécurité**
- [ ] Chiffrement des données sensibles
- [ ] Validation PCI DSS
- [ ] Audit trail des transactions
- [ ] Détection de fraude
- [ ] Rate limiting sur les API
- [ ] Tokenisation des cartes
#### 9\. **Sécurité**
#### 10. **Tests et conformité**
- [ ] Tests avec cartes de test Stripe
- [ ] Tests des cas d'erreur (carte refusée, etc.)
- [ ] Tests 3D Secure
- [ ] Tests de performance
- [ ] Conformité RGPD pour les données de paiement
- [ ] Documentation utilisateur
- Chiffrement des données sensibles
- Validation PCI DSS
- Audit trail des transactions
- Détection de fraude
- Rate limiting sur les API
- Tokenisation des cartes
#### 10\. **Tests et conformité**
- Tests avec cartes de test Stripe
- Tests des cas d'erreur (carte refusée, etc.)
- Tests 3D Secure
- Tests de performance
- Conformité RGPD pour les données de paiement
- Documentation utilisateur
### 🔧 Configuration backend requise
#### Tables base de données
```sql
```plaintext
CREATE TABLE stripe_accounts (
id INT PRIMARY KEY AUTO_INCREMENT,
fk_entite INT NOT NULL,
@@ -708,7 +552,8 @@ CREATE TABLE stripe_transactions (
```
#### Variables d'environnement
```env
```plaintext
STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
@@ -717,7 +562,7 @@ STRIPE_APPLICATION_FEE_PERCENT=1.4
### 📦 Dépendances Stripe
```yaml
```plaintext
dependencies:
# Stripe
flutter_stripe: ^10.1.1
@@ -733,21 +578,25 @@ dependencies:
### 🚀 Roadmap d'implémentation
**Phase 1 (2 semaines)**
- Configuration Stripe Connect
- Onboarding basique des amicales
- Tests en sandbox
**Phase 2 (3 semaines)**
- Intégration dans PassageForm
- Gestion des paiements simples
- Webhooks essentiels
**Phase 3 (2 semaines)**
- Dashboard financier
- Export et rapports
- Tests complets
**Phase 4 (1 semaine)**
- Mise en production
- Monitoring
- Documentation
@@ -759,8 +608,6 @@ dependencies:
- **Commission application** : 1.4% (reversée à GEOSECTOR)
- **Coût net pour l'amicale** : ~2.8% + 0.25€ par transaction
---
**Date de création** : 2025-08-07
**Auteur** : Architecture Team
**Version** : 1.2.0
**Version** : 1.2.0\</string,>\</string,>

View File

@@ -54,12 +54,24 @@ android {
buildTypes {
release {
// Optimisations sans ProGuard pour éviter les problèmes
isMinifyEnabled = false
isShrinkResources = false
// Configuration de signature
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("release")
} else {
signingConfig = signingConfigs.getByName("debug")
}
}
debug {
// Mode debug pour le développement
isDebuggable = true
applicationIdSuffix = ".debug"
versionNameSuffix = "-DEBUG"
}
}
}

View File

@@ -1,11 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permission Internet pour l'API -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Permissions pour la géolocalisation -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Feature GPS requise pour l'application -->
<uses-feature android:name="android.hardware.location.gps" android:required="true" />
<application
android:label="geosector_app"
android:label="GeoSector"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJetifier=true
# Optimisations de build
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configureondemand=true

View File

@@ -1,14 +1,14 @@
flutter_launcher_icons:
# Configuration générale
image_path: "assets/images/icon-geosector.svg"
image_path_android: "assets/images/icon-geosector.svg"
image_path_ios: "assets/images/icon-geosector.svg"
image_path: "assets/images/logo-geosector-1024.png"
image_path_android: "assets/images/logo-geosector-1024.png"
image_path_ios: "assets/images/logo-geosector-1024.png"
# Configuration Android
android: true
min_sdk_android: 21
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/images/icon-geosector.svg"
adaptive_icon_foreground: "assets/images/logo-geosector-512.png"
# Configuration iOS
ios: true

View File

@@ -471,7 +471,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEFINES_MODULE = YES;
@@ -566,7 +566,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -624,7 +624,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -680,7 +680,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEFINES_MODULE = YES;
@@ -727,7 +727,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEFINES_MODULE = YES;

View File

@@ -1,122 +1 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Some files were not shown because too many files have changed in this diff Show More