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
334
api/docs/API-SECURITY.md
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# API Security & Performance Monitoring
|
||||||
|
|
||||||
|
## 📋 Vue d'ensemble
|
||||||
|
|
||||||
|
Système complet de sécurité et monitoring pour l'API GeoSector implémenté et opérationnel.
|
||||||
|
|
||||||
|
### ✅ Fonctionnalités implémentées
|
||||||
|
|
||||||
|
- **Détection d'intrusions** : Brute force, SQL injection, patterns de scan
|
||||||
|
- **Monitoring des performances** : Temps de réponse, utilisation mémoire, requêtes DB
|
||||||
|
- **Alertes email intelligentes** : Throttling, niveaux de priorité
|
||||||
|
- **Blocage d'IP automatique** : Temporaire ou permanent
|
||||||
|
- **Traçabilité complète** : Historique pour audit et analyse
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Tables de base de données (préfixe `sec_`)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 4 tables créées dans scripts/sql/create_security_tables.sql
|
||||||
|
sec_alerts -- Alertes de sécurité
|
||||||
|
sec_performance_metrics -- Métriques de performance
|
||||||
|
sec_failed_login_attempts -- Tentatives de connexion échouées
|
||||||
|
sec_blocked_ips -- IPs bloquées
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services PHP implémentés
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Services/Security/
|
||||||
|
├── AlertService.php # Gestion centralisée des alertes
|
||||||
|
├── EmailThrottler.php # Anti-spam pour emails
|
||||||
|
├── SecurityMonitor.php # Détection des menaces
|
||||||
|
├── PerformanceMonitor.php # Monitoring des temps
|
||||||
|
└── IPBlocker.php # Gestion des blocages IP
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contrôleur d'administration
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Controllers/SecurityController.php # Interface d'administration
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### 1. Créer les tables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Exécuter le script SQL sur chaque environnement
|
||||||
|
mysql -u root -p geo_app < scripts/sql/create_security_tables.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurer le cron de purge
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ajouter dans crontab (crontab -e)
|
||||||
|
0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php >> /var/log/security_cleanup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Tester l'installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php test_security.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Fonctionnement
|
||||||
|
|
||||||
|
### Détection automatique
|
||||||
|
|
||||||
|
Le système détecte et bloque automatiquement :
|
||||||
|
|
||||||
|
- **Brute force** : 5 tentatives échouées en 5 minutes → IP bloquée 1h
|
||||||
|
- **SQL injection** : Patterns suspects → IP bloquée définitivement
|
||||||
|
- **Scan de vulnérabilités** : Accès aux fichiers sensibles → IP bloquée 1h
|
||||||
|
- **Rate limiting** : Plus de 60 requêtes/minute → Rejet temporaire
|
||||||
|
|
||||||
|
### Monitoring de performance
|
||||||
|
|
||||||
|
Chaque requête est automatiquement monitorée :
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Dans index.php
|
||||||
|
PerformanceMonitor::startRequest();
|
||||||
|
// ... traitement ...
|
||||||
|
PerformanceMonitor::endRequest($endpoint, $method, $statusCode);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alertes email
|
||||||
|
|
||||||
|
Configuration des niveaux :
|
||||||
|
|
||||||
|
- **INFO** : Log uniquement
|
||||||
|
- **WARNING** : Email avec throttling 1h
|
||||||
|
- **ERROR** : Email avec throttling 15min
|
||||||
|
- **CRITICAL** : Email avec throttling 5min
|
||||||
|
- **SECURITY** : Email immédiat, priorité haute
|
||||||
|
|
||||||
|
## 📊 Endpoints d'administration
|
||||||
|
|
||||||
|
Tous les endpoints nécessitent une authentification admin (role >= 2) :
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/admin/metrics # Métriques de performance
|
||||||
|
GET /api/admin/alerts # Alertes actives
|
||||||
|
POST /api/admin/alerts/:id/resolve # Résoudre une alerte
|
||||||
|
GET /api/admin/blocked-ips # IPs bloquées
|
||||||
|
POST /api/admin/unblock-ip # Débloquer une IP
|
||||||
|
POST /api/admin/block-ip # Bloquer une IP manuellement
|
||||||
|
GET /api/admin/security-report # Rapport complet
|
||||||
|
POST /api/admin/cleanup # Nettoyer les anciennes données
|
||||||
|
POST /api/admin/test-alert # Tester les alertes
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Seuils par défaut (modifiables dans les services)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// PerformanceMonitor.php
|
||||||
|
const DEFAULT_THRESHOLDS = [
|
||||||
|
'response_time_warning' => 1000, // 1 seconde
|
||||||
|
'response_time_critical' => 3000, // 3 secondes
|
||||||
|
'db_time_warning' => 500, // 500ms
|
||||||
|
'db_time_critical' => 1000, // 1 seconde
|
||||||
|
'memory_warning' => 64, // 64 MB
|
||||||
|
'memory_critical' => 128 // 128 MB
|
||||||
|
];
|
||||||
|
|
||||||
|
// SecurityMonitor.php
|
||||||
|
- Brute force : 5 tentatives en 5 minutes
|
||||||
|
- Rate limit : 60 requêtes par minute
|
||||||
|
- 404 pattern : 10 erreurs 404 en 10 minutes
|
||||||
|
|
||||||
|
// EmailThrottler.php
|
||||||
|
const DEFAULT_CONFIG = [
|
||||||
|
'max_per_hour' => 10,
|
||||||
|
'max_per_day' => 50,
|
||||||
|
'digest_after' => 5,
|
||||||
|
'cooldown_minutes' => 60
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rétention des données
|
||||||
|
|
||||||
|
Configurée dans `scripts/cron/cleanup_security_data.php` :
|
||||||
|
|
||||||
|
```php
|
||||||
|
$RETENTION_DAYS = [
|
||||||
|
'performance_metrics' => 30, // 30 jours
|
||||||
|
'failed_login_attempts' => 7, // 7 jours
|
||||||
|
'resolved_alerts' => 90, // 90 jours
|
||||||
|
'expired_blocks' => 0 // Déblocage immédiat
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 Métriques surveillées
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Temps de réponse total
|
||||||
|
- Temps cumulé des requêtes DB
|
||||||
|
- Nombre de requêtes DB
|
||||||
|
- Utilisation mémoire (pic et moyenne)
|
||||||
|
- Codes HTTP de réponse
|
||||||
|
|
||||||
|
### Sécurité
|
||||||
|
- Tentatives de connexion échouées
|
||||||
|
- IPs bloquées (temporaires/permanentes)
|
||||||
|
- Patterns d'attaque détectés
|
||||||
|
- Alertes par type et niveau
|
||||||
|
|
||||||
|
## 🛡️ Patterns de détection
|
||||||
|
|
||||||
|
### SQL Injection
|
||||||
|
```php
|
||||||
|
// Patterns détectés dans SecurityMonitor.php
|
||||||
|
- UNION SELECT
|
||||||
|
- DROP TABLE
|
||||||
|
- INSERT INTO
|
||||||
|
- UPDATE SET
|
||||||
|
- DELETE FROM
|
||||||
|
- Script tags
|
||||||
|
- OR 1=1
|
||||||
|
- Commentaires SQL (--)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fichiers sensibles
|
||||||
|
```php
|
||||||
|
// Patterns de scan détectés
|
||||||
|
- admin, administrator
|
||||||
|
- wp-admin, phpmyadmin
|
||||||
|
- .git, .env
|
||||||
|
- config.php
|
||||||
|
- backup, .sql, .zip
|
||||||
|
- shell.php, eval.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Exemples d'utilisation
|
||||||
|
|
||||||
|
### Déclencher une alerte manuelle
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\Security\AlertService;
|
||||||
|
|
||||||
|
AlertService::trigger('CUSTOM_ALERT', [
|
||||||
|
'message' => 'Événement important détecté',
|
||||||
|
'details' => ['user' => $userId, 'action' => $action]
|
||||||
|
], 'WARNING');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bloquer une IP manuellement
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\Security\IPBlocker;
|
||||||
|
|
||||||
|
// Blocage temporaire (1 heure)
|
||||||
|
IPBlocker::block('192.168.1.100', 3600, 'Comportement suspect');
|
||||||
|
|
||||||
|
// Blocage permanent
|
||||||
|
IPBlocker::blockPermanent('192.168.1.100', 'Attaque confirmée');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Obtenir les statistiques
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
use App\Services\Security\PerformanceMonitor;
|
||||||
|
|
||||||
|
$securityStats = SecurityMonitor::getSecurityStats();
|
||||||
|
$perfStats = PerformanceMonitor::getStats(null, 24); // 24h
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Points d'attention
|
||||||
|
|
||||||
|
### RGPD
|
||||||
|
- Les IPs sont des données personnelles
|
||||||
|
- Durée de conservation limitée (voir rétention)
|
||||||
|
- Anonymisation après traitement
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Overhead < 5ms par requête
|
||||||
|
- Optimisation des tables avec index
|
||||||
|
- Purge automatique des anciennes données
|
||||||
|
|
||||||
|
### Sécurité
|
||||||
|
- Pas d'exposition de données sensibles dans les alertes
|
||||||
|
- Chiffrement des données utilisateur
|
||||||
|
- Whitelist pour IPs de confiance (localhost)
|
||||||
|
|
||||||
|
## 🔄 Maintenance
|
||||||
|
|
||||||
|
### Quotidienne (cron)
|
||||||
|
```bash
|
||||||
|
# Purge automatique à 2h du matin
|
||||||
|
0 2 * * * php /var/www/geosector/api/scripts/cron/cleanup_security_data.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hebdomadaire
|
||||||
|
- Vérifier les alertes actives
|
||||||
|
- Analyser les tendances de performance
|
||||||
|
- Ajuster les seuils si nécessaire
|
||||||
|
|
||||||
|
### Mensuelle
|
||||||
|
- Analyser le rapport de sécurité
|
||||||
|
- Mettre à jour les IPs whitelist/blacklist
|
||||||
|
- Optimiser les tables si nécessaire
|
||||||
|
|
||||||
|
## 🐛 Dépannage
|
||||||
|
|
||||||
|
### Les tables n'existent pas
|
||||||
|
```bash
|
||||||
|
# Créer les tables
|
||||||
|
mysql -u root -p geo_app < scripts/sql/create_security_tables.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pas d'alertes email
|
||||||
|
- Vérifier la configuration email dans `AppConfig`
|
||||||
|
- Vérifier les logs : `tail -f logs/geosector-*.log`
|
||||||
|
- Tester avec : `POST /api/admin/test-alert`
|
||||||
|
|
||||||
|
### IP bloquée par erreur
|
||||||
|
```bash
|
||||||
|
# Via API
|
||||||
|
curl -X POST https://dapp.geosector.fr/api/admin/unblock-ip \
|
||||||
|
-H "Authorization: Bearer TOKEN" \
|
||||||
|
-d '{"ip": "192.168.1.100"}'
|
||||||
|
|
||||||
|
# Via MySQL
|
||||||
|
UPDATE sec_blocked_ips SET unblocked_at = NOW() WHERE ip_address = '192.168.1.100';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Ressources
|
||||||
|
|
||||||
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||||
|
- [PHP Security Best Practices](https://www.php.net/manual/en/security.php)
|
||||||
|
- Code source : `/src/Services/Security/`
|
||||||
|
- Tests : `test_security.php`
|
||||||
|
- Logs : `/logs/geosector-*.log`
|
||||||
|
|
||||||
|
## 🎯 Statut d'implémentation
|
||||||
|
|
||||||
|
✅ **Phase 1** : Infrastructure de base - COMPLÉTÉ
|
||||||
|
- Tables créées avec préfixe `sec_`
|
||||||
|
- Services PHP implémentés
|
||||||
|
- Intégration dans index.php et Database.php
|
||||||
|
|
||||||
|
✅ **Phase 2** : Monitoring de Performance - COMPLÉTÉ
|
||||||
|
- Chronométrage automatique des requêtes
|
||||||
|
- Monitoring des requêtes DB
|
||||||
|
- Alertes sur dégradation
|
||||||
|
|
||||||
|
✅ **Phase 3** : Détection d'intrusions - COMPLÉTÉ
|
||||||
|
- Détection brute force
|
||||||
|
- Détection SQL injection
|
||||||
|
- Blocage IP automatique
|
||||||
|
|
||||||
|
✅ **Phase 4** : Alertes Email - COMPLÉTÉ
|
||||||
|
- Service d'alertes avec throttling
|
||||||
|
- Templates d'emails
|
||||||
|
- Niveaux de priorité
|
||||||
|
|
||||||
|
✅ **Phase 5** : Administration - COMPLÉTÉ
|
||||||
|
- Endpoints d'administration
|
||||||
|
- Interface de gestion
|
||||||
|
- Rapports de sécurité
|
||||||
|
|
||||||
|
✅ **Phase 6** : Maintenance - COMPLÉTÉ
|
||||||
|
- Script de purge automatique
|
||||||
|
- Optimisation des tables
|
||||||
|
- Documentation complète
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour : 2025-01-17*
|
||||||
|
*Version : 1.0.0*
|
||||||
419
api/docs/CHAT_MODULE.md
Normal file
@@ -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
|
||||||
176
api/docs/FIX_USER_CREATION_400_ERRORS.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Correction des erreurs 400 lors de la création d'utilisateurs
|
||||||
|
|
||||||
|
## Problème identifié
|
||||||
|
Un administrateur (fk_role=2) rencontrait des erreurs 400 répétées lors de tentatives de création de membre, menant à un bannissement par fail2ban :
|
||||||
|
- 17:09:39 - POST /api/users HTTP/1.1 400 (Bad Request)
|
||||||
|
- 17:10:44 - POST /api/users/check-username HTTP/1.1 400 (Bad Request)
|
||||||
|
- 17:11:21 - POST /api/users HTTP/1.1 400 (Bad Request)
|
||||||
|
|
||||||
|
## Causes identifiées
|
||||||
|
|
||||||
|
### 1. Conflit de routage (CRITIQUE)
|
||||||
|
**Problème:** La route `/api/users/check-username` était déclarée APRÈS la route générique `/api/users` dans Router.php, causant une mauvaise interprétation où "check-username" était traité comme un ID utilisateur.
|
||||||
|
|
||||||
|
**Solution:** Déplacer la déclaration de la route spécifique AVANT les routes avec paramètres.
|
||||||
|
|
||||||
|
### 2. Messages d'erreur non informatifs
|
||||||
|
**Problème:** Les erreurs 400 retournaient des messages génériques sans détails sur le champ problématique.
|
||||||
|
|
||||||
|
**Solution:** Ajout de messages d'erreur détaillés incluant :
|
||||||
|
- Le champ en erreur (`field`)
|
||||||
|
- La valeur problématique (`value`)
|
||||||
|
- Le format attendu (`format`)
|
||||||
|
- La raison de l'erreur (`reason`)
|
||||||
|
|
||||||
|
### 3. Manque de logs de débogage
|
||||||
|
**Problème:** Aucun log n'était généré pour tracer les erreurs de validation.
|
||||||
|
|
||||||
|
**Solution:** Ajout de logs détaillés à chaque point de validation.
|
||||||
|
|
||||||
|
## Modifications apportées
|
||||||
|
|
||||||
|
### 1. Router.php (ligne 36-44)
|
||||||
|
```php
|
||||||
|
// AVANT (incorrect)
|
||||||
|
$this->post('users', ['UserController', 'createUser']);
|
||||||
|
$this->post('users/check-username', ['UserController', 'checkUsername']);
|
||||||
|
|
||||||
|
// APRÈS (correct)
|
||||||
|
$this->post('users/check-username', ['UserController', 'checkUsername']); // Route spécifique en premier
|
||||||
|
$this->post('users', ['UserController', 'createUser']);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. UserController.php - Amélioration des validations
|
||||||
|
|
||||||
|
#### Validation de l'email
|
||||||
|
```php
|
||||||
|
// Réponse améliorée
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Email requis',
|
||||||
|
'field' => 'email' // Indique clairement le champ problématique
|
||||||
|
], 400);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Validation du username manuel
|
||||||
|
```php
|
||||||
|
// Réponse améliorée
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Le nom d\'utilisateur est requis pour cette entité',
|
||||||
|
'field' => 'username',
|
||||||
|
'reason' => 'L\'entité requiert la saisie manuelle des identifiants'
|
||||||
|
], 400);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Format du username
|
||||||
|
```php
|
||||||
|
// Réponse améliorée
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Format du nom d\'utilisateur invalide',
|
||||||
|
'field' => 'username',
|
||||||
|
'format' => '10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _',
|
||||||
|
'value' => $username // Montre la valeur soumise
|
||||||
|
], 400);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Ajout de logs détaillés
|
||||||
|
|
||||||
|
Chaque point de validation génère maintenant un log avec :
|
||||||
|
- Le type d'erreur
|
||||||
|
- L'utilisateur qui fait la requête
|
||||||
|
- Les données reçues (sans données sensibles)
|
||||||
|
- Le contexte de l'erreur
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
```php
|
||||||
|
LogService::log('Erreur création utilisateur : Format username invalide', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'username' => $username,
|
||||||
|
'username_length' => strlen($username)
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cas d'erreur 400 possibles
|
||||||
|
|
||||||
|
### Pour /api/users (création)
|
||||||
|
|
||||||
|
1. **Email manquant ou vide**
|
||||||
|
- Message: "Email requis"
|
||||||
|
- Field: "email"
|
||||||
|
|
||||||
|
2. **Nom manquant ou vide**
|
||||||
|
- Message: "Nom requis"
|
||||||
|
- Field: "name"
|
||||||
|
|
||||||
|
3. **Format email invalide**
|
||||||
|
- Message: "Format d'email invalide"
|
||||||
|
- Field: "email"
|
||||||
|
- Value: [email soumis]
|
||||||
|
|
||||||
|
4. **Username manuel requis mais manquant** (si chk_username_manuel=1)
|
||||||
|
- Message: "Le nom d'utilisateur est requis pour cette entité"
|
||||||
|
- Field: "username"
|
||||||
|
- Reason: "L'entité requiert la saisie manuelle des identifiants"
|
||||||
|
|
||||||
|
5. **Format username invalide**
|
||||||
|
- Message: "Format du nom d'utilisateur invalide"
|
||||||
|
- Field: "username"
|
||||||
|
- Format: "10-30 caractères, commence par une lettre..."
|
||||||
|
- Value: [username soumis]
|
||||||
|
|
||||||
|
6. **Mot de passe manuel requis mais manquant** (si chk_mdp_manuel=1)
|
||||||
|
- Message: "Le mot de passe est requis pour cette entité"
|
||||||
|
- Field: "password"
|
||||||
|
- Reason: "L'entité requiert la saisie manuelle des mots de passe"
|
||||||
|
|
||||||
|
### Pour /api/users/check-username
|
||||||
|
|
||||||
|
1. **Username manquant**
|
||||||
|
- Message: "Username requis pour la vérification"
|
||||||
|
- Field: "username"
|
||||||
|
|
||||||
|
2. **Format username invalide**
|
||||||
|
- Message: "Format invalide"
|
||||||
|
- Field: "username"
|
||||||
|
- Format: "10-30 caractères, commence par une lettre..."
|
||||||
|
- Value: [username soumis]
|
||||||
|
|
||||||
|
## Test de la solution
|
||||||
|
|
||||||
|
Un script de test a été créé : `/tests/test_user_creation.php`
|
||||||
|
|
||||||
|
Il teste tous les cas d'erreur possibles et vérifie que :
|
||||||
|
1. Les codes HTTP sont corrects
|
||||||
|
2. Les messages d'erreur sont informatifs
|
||||||
|
3. Les champs en erreur sont identifiés
|
||||||
|
|
||||||
|
## Recommandations pour éviter le bannissement fail2ban
|
||||||
|
|
||||||
|
1. **Côté client (application Flutter)** :
|
||||||
|
- Valider les données AVANT l'envoi
|
||||||
|
- Afficher clairement les erreurs à l'utilisateur
|
||||||
|
- Implémenter un délai entre les tentatives (rate limiting côté client)
|
||||||
|
|
||||||
|
2. **Côté API** :
|
||||||
|
- Les messages d'erreur détaillés permettent maintenant de corriger rapidement les problèmes
|
||||||
|
- Les logs permettent de diagnostiquer les problèmes récurrents
|
||||||
|
|
||||||
|
3. **Configuration fail2ban** :
|
||||||
|
- Considérer d'augmenter le seuil pour les erreurs 400 (ex: 5 tentatives au lieu de 3)
|
||||||
|
- Exclure certaines IP de confiance si nécessaire
|
||||||
|
|
||||||
|
## Suivi des logs
|
||||||
|
|
||||||
|
Les logs sont maintenant générés dans :
|
||||||
|
- `/logs/geosector-[environment]-[date].log` : Logs généraux avec détails des erreurs
|
||||||
|
|
||||||
|
Format des logs :
|
||||||
|
```
|
||||||
|
timestamp;browser;os;client_type;level;metadata;message
|
||||||
|
```
|
||||||
|
|
||||||
|
Les erreurs de validation sont loggées avec le niveau "warning" pour permettre un suivi sans être critiques.
|
||||||
@@ -779,7 +779,7 @@ fetch('/api/endpoint', {
|
|||||||
|
|
||||||
## Changements récents
|
## 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
|
#### 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
|
- **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
|
- **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
|
- **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
|
#### 1. Correction des rôles administrateurs
|
||||||
- **Avant :** Les administrateurs d'amicale devaient avoir `fk_role > 2`
|
- **Avant :** Les administrateurs d'amicale devaient avoir `fk_role > 2`
|
||||||
@@ -836,3 +836,28 @@ fetch('/api/endpoint', {
|
|||||||
- **Format d'envoi des images :** Base64 data URL pour compatibilité multiplateforme
|
- **Format d'envoi des images :** Base64 data URL pour compatibilité multiplateforme
|
||||||
- **Structure de réponse enrichie :** Le logo est inclus dans l'objet `amicale` lors du login
|
- **Structure de réponse enrichie :** Le logo est inclus dans l'objet `amicale` lors du login
|
||||||
- **Optimisation :** Pas de requête HTTP supplémentaire nécessaire pour afficher le logo
|
- **Optimisation :** Pas de requête HTTP supplémentaire nécessaire pour afficher le logo
|
||||||
|
|
||||||
|
### Version 3.0.8 (Janvier 2025)
|
||||||
|
|
||||||
|
#### 1. Système de génération automatique de reçus fiscaux pour les dons
|
||||||
|
- **Nouveau service :** `ReceiptService` pour la génération automatique de reçus PDF
|
||||||
|
- **Déclencheurs automatiques :**
|
||||||
|
- Création d'un passage avec `fk_type=1` (don) et email valide
|
||||||
|
- Mise à jour d'un passage en don si `nom_recu` est vide/null
|
||||||
|
- **Caractéristiques techniques :**
|
||||||
|
- PDF ultra-légers (< 5KB) générés en format natif sans librairie externe
|
||||||
|
- Support des caractères accentués avec conversion automatique
|
||||||
|
- Stockage structuré : `/uploads/entites/{entite_id}/recus/{operation_id}/`
|
||||||
|
- Enregistrement dans la table `medias` avec catégorie `recu`
|
||||||
|
- **Queue d'envoi email :**
|
||||||
|
- Envoi automatique par email avec pièce jointe PDF
|
||||||
|
- Format MIME multipart pour compatibilité maximale
|
||||||
|
- Gestion dans la table `email_queue` avec statut de suivi
|
||||||
|
- **Nouvelle route API :**
|
||||||
|
- `GET /api/passages/{id}/receipt` : Récupération du PDF d'un reçu
|
||||||
|
- Retourne le PDF en base64 ou téléchargement direct selon Accept header
|
||||||
|
- **Champs base de données utilisés :**
|
||||||
|
- `nom_recu` : Nom du fichier PDF généré
|
||||||
|
- `date_creat_recu` : Date de génération du reçu
|
||||||
|
- `date_sent_recu` : Date d'envoi par email
|
||||||
|
- `chk_email_sent` : Indicateur d'envoi réussi
|
||||||
|
|||||||
135
api/docs/USERNAME_VALIDATION_CHANGES.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Changements de validation des usernames - Version ultra-souple
|
||||||
|
|
||||||
|
## Date : 17 janvier 2025
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
Suite aux problèmes d'erreurs 400 et au besoin d'avoir une approche plus moderne et inclusive, les règles de validation des usernames ont été assouplies pour accepter tous les caractères UTF-8, similaire à l'approche NIST pour les mots de passe.
|
||||||
|
|
||||||
|
## Anciennes règles (trop restrictives)
|
||||||
|
- ❌ 10-30 caractères
|
||||||
|
- ❌ Doit commencer par une lettre minuscule
|
||||||
|
- ❌ Seulement : a-z, 0-9, ., -, _
|
||||||
|
- ❌ Pas d'espaces
|
||||||
|
- ❌ Pas de majuscules
|
||||||
|
- ❌ Pas d'accents ou caractères spéciaux
|
||||||
|
|
||||||
|
## Nouvelles règles (ultra-souples)
|
||||||
|
- ✅ **8-30 caractères UTF-8**
|
||||||
|
- ✅ **Tous caractères acceptés** :
|
||||||
|
- Lettres (majuscules/minuscules)
|
||||||
|
- Chiffres
|
||||||
|
- Espaces
|
||||||
|
- Caractères spéciaux (!@#$%^&*()_+-=[]{}|;:'"<>,.?/)
|
||||||
|
- Accents (é, è, à, ñ, ü, etc.)
|
||||||
|
- Émojis (😀, 🎉, ❤️, etc.)
|
||||||
|
- Caractères non-latins (中文, العربية, Русский, etc.)
|
||||||
|
- ✅ **Sensible à la casse** (Jean ≠ jean)
|
||||||
|
- ✅ **Trim automatique** des espaces début/fin
|
||||||
|
- ✅ **Unicité vérifiée** dans toute la base
|
||||||
|
|
||||||
|
## Exemples de usernames valides
|
||||||
|
|
||||||
|
### Noms classiques
|
||||||
|
- `Jean-Pierre`
|
||||||
|
- `Marie Claire` (avec espace)
|
||||||
|
- `O'Connor`
|
||||||
|
- `José García`
|
||||||
|
|
||||||
|
### Avec chiffres et caractères spéciaux
|
||||||
|
- `admin2024`
|
||||||
|
- `user@company`
|
||||||
|
- `test_user#1`
|
||||||
|
- `Marie*123!`
|
||||||
|
|
||||||
|
### International
|
||||||
|
- `李明` (chinois)
|
||||||
|
- `محمد` (arabe)
|
||||||
|
- `Владимир` (russe)
|
||||||
|
- `さくら` (japonais)
|
||||||
|
- `Παύλος` (grec)
|
||||||
|
|
||||||
|
### Modernes/Fun
|
||||||
|
- `🦄Unicorn`
|
||||||
|
- `Player_1 🎮`
|
||||||
|
- `☕Coffee.Lover`
|
||||||
|
- `2024_User`
|
||||||
|
|
||||||
|
## Exemples de usernames invalides
|
||||||
|
- `short` ❌ (moins de 8 caractères)
|
||||||
|
- ` ` ❌ (espaces seulement)
|
||||||
|
- `very_long_username_that_exceeds_thirty_chars` ❌ (plus de 30 caractères)
|
||||||
|
|
||||||
|
## Modifications techniques
|
||||||
|
|
||||||
|
### 1. Code PHP (UserController.php)
|
||||||
|
```php
|
||||||
|
// Avant (restrictif)
|
||||||
|
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username))
|
||||||
|
|
||||||
|
// Après (ultra-souple)
|
||||||
|
$username = trim($data['username']);
|
||||||
|
$usernameLength = mb_strlen($username, 'UTF-8');
|
||||||
|
|
||||||
|
if ($usernameLength < 8) {
|
||||||
|
// Erreur : trop court
|
||||||
|
}
|
||||||
|
if ($usernameLength > 30) {
|
||||||
|
// Erreur : trop long
|
||||||
|
}
|
||||||
|
// C'est tout ! Pas d'autre validation
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Base de données
|
||||||
|
```sql
|
||||||
|
-- Script à exécuter : scripts/sql/migration_username_utf8_support.sql
|
||||||
|
ALTER TABLE `users`
|
||||||
|
MODIFY COLUMN `encrypted_user_name` varchar(255) DEFAULT '';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Messages d'erreur simplifiés
|
||||||
|
- Avant : "Format du nom d'utilisateur invalide (10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _)"
|
||||||
|
- Après :
|
||||||
|
- "Identifiant trop court" + "Minimum 8 caractères"
|
||||||
|
- "Identifiant trop long" + "Maximum 30 caractères"
|
||||||
|
- "Identifiant déjà utilisé"
|
||||||
|
|
||||||
|
## Impact sur l'expérience utilisateur
|
||||||
|
|
||||||
|
### Avantages
|
||||||
|
1. **Inclusivité** : Support de toutes les langues et cultures
|
||||||
|
2. **Modernité** : Permet les émojis et caractères spéciaux
|
||||||
|
3. **Simplicité** : Règles faciles à comprendre (juste la longueur)
|
||||||
|
4. **Flexibilité** : Les utilisateurs peuvent choisir l'identifiant qu'ils veulent
|
||||||
|
5. **Moins d'erreurs** : Moins de rejets pour format invalide
|
||||||
|
|
||||||
|
### Points d'attention
|
||||||
|
1. **Support client** : Former le support aux nouveaux formats possibles
|
||||||
|
2. **Affichage** : S'assurer que l'UI supporte bien l'UTF-8
|
||||||
|
3. **Recherche** : La recherche d'utilisateurs doit gérer la casse et l'UTF-8
|
||||||
|
4. **Export** : Vérifier que les exports CSV/Excel gèrent bien l'UTF-8
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Pas d'impact sur la sécurité
|
||||||
|
- ✅ Les usernames sont toujours chiffrés en base (AES-256-CBC)
|
||||||
|
- ✅ L'unicité est toujours vérifiée
|
||||||
|
- ✅ Les injections SQL sont impossibles (prepared statements)
|
||||||
|
- ✅ Le trim empêche les espaces invisibles
|
||||||
|
|
||||||
|
### Recommandations
|
||||||
|
- Continuer à générer automatiquement des usernames simples (ASCII) pour éviter les problèmes
|
||||||
|
- Mais permettre la saisie manuelle de tout format
|
||||||
|
- Logger les usernames "exotiques" pour détecter d'éventuels abus
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- Script de test disponible : `/tests/test_username_validation.php`
|
||||||
|
- Teste tous les cas limites et formats internationaux
|
||||||
|
|
||||||
|
## Rollback si nécessaire
|
||||||
|
Si besoin de revenir en arrière :
|
||||||
|
1. Restaurer l'ancienne validation dans UserController
|
||||||
|
2. Les usernames UTF-8 existants continueront de fonctionner
|
||||||
|
3. Seuls les nouveaux seront restreints
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
Cette approche ultra-souple aligne les usernames sur les standards modernes d'inclusivité et d'accessibilité, tout en maintenant la sécurité grâce au chiffrement et à la validation de l'unicité.
|
||||||
BIN
api/docs/_logo_recu.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
api/docs/_recu_template.pdf
Normal file
@@ -18,168 +18,39 @@
|
|||||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE `chat_anonymous_users` (
|
-- Tables préfixées "chat_"
|
||||||
`id` varchar(50) NOT NULL,
|
CREATE TABLE chat_rooms (
|
||||||
`device_id` varchar(100) NOT NULL,
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
`name` varchar(100) DEFAULT NULL,
|
title VARCHAR(255),
|
||||||
`email` varchar(100) DEFAULT NULL,
|
type ENUM('private', 'group', 'broadcast'),
|
||||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
created_at TIMESTAMP,
|
||||||
`converted_to_user_id` int(10) unsigned DEFAULT NULL,
|
created_by INT
|
||||||
`metadata` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),
|
);
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_device_id` (`device_id`),
|
|
||||||
KEY `idx_converted_user` (`converted_to_user_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_attachments` (
|
CREATE TABLE chat_messages (
|
||||||
`id` varchar(50) NOT NULL,
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
`fk_message` varchar(50) NOT NULL,
|
room_id VARCHAR(36),
|
||||||
`file_name` varchar(255) NOT NULL,
|
content TEXT,
|
||||||
`file_path` varchar(500) NOT NULL,
|
sender_id INT,
|
||||||
`file_type` varchar(100) NOT NULL,
|
sent_at TIMESTAMP,
|
||||||
`file_size` int(10) unsigned NOT NULL,
|
FOREIGN KEY (room_id) REFERENCES chat_rooms(id)
|
||||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
);
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_message` (`fk_message`),
|
|
||||||
CONSTRAINT `fk_chat_attachments_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_audience_targets` (
|
CREATE TABLE chat_participants (
|
||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
room_id VARCHAR(36),
|
||||||
`fk_room` varchar(50) NOT NULL,
|
user_id INT,
|
||||||
`target_type` enum('role','entity','all','combined') NOT NULL DEFAULT 'all',
|
role INT,
|
||||||
`target_id` varchar(50) DEFAULT NULL,
|
entite_id INT,
|
||||||
`role_filter` varchar(20) DEFAULT NULL,
|
joined_at TIMESTAMP,
|
||||||
`entity_filter` varchar(50) DEFAULT NULL,
|
PRIMARY KEY (room_id, user_id)
|
||||||
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
);
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_room` (`fk_room`),
|
|
||||||
KEY `idx_type` (`target_type`),
|
|
||||||
CONSTRAINT `fk_chat_audience_targets_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_broadcast_lists` (
|
CREATE TABLE chat_read_receipts (
|
||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
message_id VARCHAR(36),
|
||||||
`fk_room` varchar(50) NOT NULL,
|
user_id INT,
|
||||||
`name` varchar(100) NOT NULL,
|
read_at TIMESTAMP,
|
||||||
`description` text DEFAULT NULL,
|
PRIMARY KEY (message_id, user_id)
|
||||||
`fk_user_creator` int(10) unsigned NOT NULL,
|
);
|
||||||
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_room` (`fk_room`),
|
|
||||||
KEY `idx_user_creator` (`fk_user_creator`),
|
|
||||||
CONSTRAINT `fk_chat_broadcast_lists_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE `chat_messages` (
|
|
||||||
`id` varchar(50) NOT NULL,
|
|
||||||
`fk_room` varchar(50) NOT NULL,
|
|
||||||
`fk_user` int(10) unsigned DEFAULT NULL,
|
|
||||||
`sender_type` enum('user','anonymous','system') NOT NULL DEFAULT 'user',
|
|
||||||
`content` text DEFAULT NULL,
|
|
||||||
`content_type` enum('text','image','file') NOT NULL DEFAULT 'text',
|
|
||||||
`date_sent` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`date_delivered` timestamp NULL DEFAULT NULL,
|
|
||||||
`date_read` timestamp NULL DEFAULT NULL,
|
|
||||||
`statut` enum('envoye','livre','lu','error') NOT NULL DEFAULT 'envoye',
|
|
||||||
`is_announcement` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_room` (`fk_room`),
|
|
||||||
KEY `idx_user` (`fk_user`),
|
|
||||||
KEY `idx_date` (`date_sent`),
|
|
||||||
KEY `idx_status` (`statut`),
|
|
||||||
KEY `idx_messages_unread` (`fk_room`,`statut`),
|
|
||||||
CONSTRAINT `fk_chat_messages_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_notifications` (
|
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
`fk_user` int(10) unsigned NOT NULL,
|
|
||||||
`fk_message` varchar(50) DEFAULT NULL,
|
|
||||||
`fk_room` varchar(50) DEFAULT NULL,
|
|
||||||
`type` varchar(50) NOT NULL,
|
|
||||||
`contenu` text DEFAULT NULL,
|
|
||||||
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`date_lecture` timestamp NULL DEFAULT NULL,
|
|
||||||
`statut` enum('non_lue','lue') NOT NULL DEFAULT 'non_lue',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_user` (`fk_user`),
|
|
||||||
KEY `idx_message` (`fk_message`),
|
|
||||||
KEY `idx_room` (`fk_room`),
|
|
||||||
KEY `idx_statut` (`statut`),
|
|
||||||
KEY `idx_notifications_unread` (`fk_user`,`statut`),
|
|
||||||
CONSTRAINT `fk_chat_notifications_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE SET NULL,
|
|
||||||
CONSTRAINT `fk_chat_notifications_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE SET NULL
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_offline_queue` (
|
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
`user_id` int(10) unsigned NOT NULL,
|
|
||||||
`operation_type` varchar(50) NOT NULL,
|
|
||||||
`operation_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`operation_data`)),
|
|
||||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`processed_at` timestamp NULL DEFAULT NULL,
|
|
||||||
`status` enum('pending','processing','completed','failed') NOT NULL DEFAULT 'pending',
|
|
||||||
`error_message` text DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_user_id` (`user_id`),
|
|
||||||
KEY `idx_status` (`status`),
|
|
||||||
KEY `idx_created_at` (`created_at`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_participants` (
|
|
||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
`id_room` varchar(50) NOT NULL,
|
|
||||||
`id_user` int(10) unsigned DEFAULT NULL,
|
|
||||||
`anonymous_id` varchar(50) DEFAULT NULL,
|
|
||||||
`role` enum('administrateur','participant','en_lecture_seule') NOT NULL DEFAULT 'participant',
|
|
||||||
`date_ajout` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`notification_activee` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
|
||||||
`last_read_message_id` varchar(50) DEFAULT NULL,
|
|
||||||
`via_target` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
|
||||||
`can_reply` tinyint(1) unsigned DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `uc_room_user` (`id_room`,`id_user`),
|
|
||||||
KEY `idx_room` (`id_room`),
|
|
||||||
KEY `idx_user` (`id_user`),
|
|
||||||
KEY `idx_anonymous_id` (`anonymous_id`),
|
|
||||||
KEY `idx_participants_active` (`id_room`,`id_user`,`notification_activee`),
|
|
||||||
CONSTRAINT `fk_chat_participants_room` FOREIGN KEY (`id_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_read_messages` (
|
|
||||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
`fk_message` varchar(50) NOT NULL,
|
|
||||||
`fk_user` int(10) unsigned NOT NULL,
|
|
||||||
`date_read` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `uc_message_user` (`fk_message`,`fk_user`),
|
|
||||||
KEY `idx_message` (`fk_message`),
|
|
||||||
KEY `idx_user` (`fk_user`),
|
|
||||||
CONSTRAINT `fk_chat_read_messages_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `chat_rooms` (
|
|
||||||
`id` varchar(50) NOT NULL,
|
|
||||||
`type` enum('privee','groupe','liste_diffusion','broadcast','announcement') NOT NULL,
|
|
||||||
`title` varchar(100) DEFAULT NULL,
|
|
||||||
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`fk_user` int(10) unsigned NOT NULL,
|
|
||||||
`fk_entite` int(10) unsigned DEFAULT NULL,
|
|
||||||
`statut` enum('active','archive') NOT NULL DEFAULT 'active',
|
|
||||||
`description` text DEFAULT NULL,
|
|
||||||
`reply_permission` enum('all','admins_only','sender_only','none') NOT NULL DEFAULT 'all',
|
|
||||||
`is_pinned` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
|
||||||
`expiry_date` timestamp NULL DEFAULT NULL,
|
|
||||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_user` (`fk_user`),
|
|
||||||
KEY `idx_entite` (`fk_entite`),
|
|
||||||
KEY `idx_type` (`type`),
|
|
||||||
KEY `idx_statut` (`statut`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
|
||||||
|
|
||||||
CREATE TABLE `email_counter` (
|
CREATE TABLE `email_counter` (
|
||||||
`id` int(10) unsigned NOT NULL DEFAULT 1,
|
`id` int(10) unsigned NOT NULL DEFAULT 1,
|
||||||
|
|||||||
BIN
api/docs/recu_537254062.pdf
Normal file
BIN
api/docs/recu_972506460.pdf
Normal file
132
api/index.php
@@ -15,6 +15,12 @@ require_once __DIR__ . '/src/Core/Response.php';
|
|||||||
require_once __DIR__ . '/src/Utils/ClientDetector.php';
|
require_once __DIR__ . '/src/Utils/ClientDetector.php';
|
||||||
require_once __DIR__ . '/src/Services/LogService.php';
|
require_once __DIR__ . '/src/Services/LogService.php';
|
||||||
|
|
||||||
|
// Chargement des services de sécurité
|
||||||
|
require_once __DIR__ . '/src/Services/Security/PerformanceMonitor.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/IPBlocker.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/SecurityMonitor.php';
|
||||||
|
require_once __DIR__ . '/src/Services/Security/AlertService.php';
|
||||||
|
|
||||||
// Chargement des contrôleurs
|
// Chargement des contrôleurs
|
||||||
require_once __DIR__ . '/src/Controllers/LogController.php';
|
require_once __DIR__ . '/src/Controllers/LogController.php';
|
||||||
require_once __DIR__ . '/src/Controllers/LoginController.php';
|
require_once __DIR__ . '/src/Controllers/LoginController.php';
|
||||||
@@ -26,6 +32,8 @@ require_once __DIR__ . '/src/Controllers/VilleController.php';
|
|||||||
require_once __DIR__ . '/src/Controllers/FileController.php';
|
require_once __DIR__ . '/src/Controllers/FileController.php';
|
||||||
require_once __DIR__ . '/src/Controllers/SectorController.php';
|
require_once __DIR__ . '/src/Controllers/SectorController.php';
|
||||||
require_once __DIR__ . '/src/Controllers/PasswordController.php';
|
require_once __DIR__ . '/src/Controllers/PasswordController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/ChatController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/SecurityController.php';
|
||||||
|
|
||||||
// Initialiser la configuration
|
// Initialiser la configuration
|
||||||
$appConfig = AppConfig::getInstance();
|
$appConfig = AppConfig::getInstance();
|
||||||
@@ -57,8 +65,132 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|||||||
// Initialiser la session
|
// Initialiser la session
|
||||||
Session::start();
|
Session::start();
|
||||||
|
|
||||||
|
// ===== DÉBUT DU MONITORING DE SÉCURITÉ =====
|
||||||
|
use App\Services\Security\PerformanceMonitor;
|
||||||
|
use App\Services\Security\IPBlocker;
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
use App\Services\Security\AlertService;
|
||||||
|
|
||||||
|
// Obtenir l'IP du client
|
||||||
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
|
||||||
|
// Vérifier si l'IP est bloquée
|
||||||
|
if (IPBlocker::isBlocked($clientIp)) {
|
||||||
|
http_response_code(403);
|
||||||
|
Response::json([
|
||||||
|
'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
|
// Créer l'instance de routeur
|
||||||
$router = new Router();
|
$router = new Router();
|
||||||
|
|
||||||
|
// Enregistrer une fonction de shutdown pour capturer les métriques
|
||||||
|
register_shutdown_function(function() use ($requestUri, $requestMethod) {
|
||||||
|
$statusCode = http_response_code();
|
||||||
|
|
||||||
|
// Terminer le monitoring de performance
|
||||||
|
PerformanceMonitor::endRequest($requestUri, $requestMethod, $statusCode);
|
||||||
|
|
||||||
|
// Vérifier les patterns 404
|
||||||
|
if ($statusCode === 404) {
|
||||||
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
SecurityMonitor::check404Pattern($clientIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alerter sur les erreurs 500
|
||||||
|
if ($statusCode >= 500) {
|
||||||
|
$error = error_get_last();
|
||||||
|
AlertService::trigger('HTTP_500', [
|
||||||
|
'endpoint' => $requestUri,
|
||||||
|
'method' => $requestMethod,
|
||||||
|
'error_message' => $error['message'] ?? 'Unknown error',
|
||||||
|
'error_file' => $error['file'] ?? 'Unknown',
|
||||||
|
'error_line' => $error['line'] ?? 0,
|
||||||
|
'message' => "Erreur serveur 500 sur $requestUri"
|
||||||
|
], 'ERROR');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer périodiquement les IPs expirées (1% de chance)
|
||||||
|
if (rand(1, 100) === 1) {
|
||||||
|
IPBlocker::cleanupExpired();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gérer les erreurs non capturées
|
||||||
|
set_exception_handler(function($exception) use ($requestUri, $requestMethod) {
|
||||||
|
// Logger l'erreur
|
||||||
|
error_log("Uncaught exception: " . $exception->getMessage());
|
||||||
|
|
||||||
|
// Créer une alerte
|
||||||
|
AlertService::trigger('UNCAUGHT_EXCEPTION', [
|
||||||
|
'endpoint' => $requestUri,
|
||||||
|
'method' => $requestMethod,
|
||||||
|
'exception' => get_class($exception),
|
||||||
|
'message' => $exception->getMessage(),
|
||||||
|
'file' => $exception->getFile(),
|
||||||
|
'line' => $exception->getLine(),
|
||||||
|
'trace' => substr($exception->getTraceAsString(), 0, 1000)
|
||||||
|
], 'ERROR');
|
||||||
|
|
||||||
|
// Retourner une erreur 500
|
||||||
|
http_response_code(500);
|
||||||
|
Response::json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Internal server error'
|
||||||
|
], 500);
|
||||||
|
});
|
||||||
|
|
||||||
// Gérer la requête
|
// Gérer la requête
|
||||||
|
try {
|
||||||
$router->handle();
|
$router->handle();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Les exceptions sont gérées par le handler ci-dessus
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|||||||
150
api/scripts/cron/cleanup_security_data.php
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script de nettoyage des données de sécurité
|
||||||
|
* À exécuter via cron quotidiennement
|
||||||
|
* Exemple crontab: 0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
require_once __DIR__ . '/../../bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||||
|
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||||
|
|
||||||
|
// Initialiser la configuration
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
$config = $appConfig->getFullConfig();
|
||||||
|
|
||||||
|
// Initialiser la base de données
|
||||||
|
Database::init($config['database']);
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Configuration de rétention (en jours)
|
||||||
|
$RETENTION_DAYS = [
|
||||||
|
'performance_metrics' => 30, // Garder 30 jours de métriques
|
||||||
|
'failed_login_attempts' => 7, // Garder 7 jours de tentatives
|
||||||
|
'resolved_alerts' => 90, // Garder 90 jours d'alertes résolues
|
||||||
|
'expired_blocks' => 0 // Débloquer immédiatement les IPs expirées
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] Début du nettoyage des données de sécurité\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$totalDeleted = 0;
|
||||||
|
|
||||||
|
// 1. Nettoyer les métriques de performance
|
||||||
|
echo "- Nettoyage des métriques de performance (>" . $RETENTION_DAYS['performance_metrics'] . " jours)...\n";
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
DELETE FROM sec_performance_metrics
|
||||||
|
WHERE created_at < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||||
|
');
|
||||||
|
$stmt->execute(['days' => $RETENTION_DAYS['performance_metrics']]);
|
||||||
|
$deleted = $stmt->rowCount();
|
||||||
|
echo " → $deleted lignes supprimées\n";
|
||||||
|
$totalDeleted += $deleted;
|
||||||
|
|
||||||
|
// 2. Nettoyer les tentatives de login échouées
|
||||||
|
echo "- Nettoyage des tentatives de login (>" . $RETENTION_DAYS['failed_login_attempts'] . " jours)...\n";
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
DELETE FROM sec_failed_login_attempts
|
||||||
|
WHERE attempt_time < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||||
|
');
|
||||||
|
$stmt->execute(['days' => $RETENTION_DAYS['failed_login_attempts']]);
|
||||||
|
$deleted = $stmt->rowCount();
|
||||||
|
echo " → $deleted lignes supprimées\n";
|
||||||
|
$totalDeleted += $deleted;
|
||||||
|
|
||||||
|
// 3. Nettoyer les alertes résolues
|
||||||
|
echo "- Nettoyage des alertes résolues (>" . $RETENTION_DAYS['resolved_alerts'] . " jours)...\n";
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
DELETE FROM sec_alerts
|
||||||
|
WHERE resolved = 1
|
||||||
|
AND resolved_at < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||||
|
');
|
||||||
|
$stmt->execute(['days' => $RETENTION_DAYS['resolved_alerts']]);
|
||||||
|
$deleted = $stmt->rowCount();
|
||||||
|
echo " → $deleted lignes supprimées\n";
|
||||||
|
$totalDeleted += $deleted;
|
||||||
|
|
||||||
|
// 4. Débloquer les IPs expirées
|
||||||
|
echo "- Déblocage des IPs expirées...\n";
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
UPDATE sec_blocked_ips
|
||||||
|
SET unblocked_at = NOW()
|
||||||
|
WHERE blocked_until <= NOW()
|
||||||
|
AND unblocked_at IS NULL
|
||||||
|
AND permanent = 0
|
||||||
|
');
|
||||||
|
$stmt->execute();
|
||||||
|
$unblocked = $stmt->rowCount();
|
||||||
|
echo " → $unblocked IPs débloquées\n";
|
||||||
|
|
||||||
|
// 5. Supprimer les anciennes IPs débloquées (optionnel, garder 180 jours d'historique)
|
||||||
|
echo "- Suppression des anciennes IPs débloquées (>180 jours)...\n";
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
DELETE FROM sec_blocked_ips
|
||||||
|
WHERE unblocked_at IS NOT NULL
|
||||||
|
AND unblocked_at < DATE_SUB(NOW(), INTERVAL 180 DAY)
|
||||||
|
');
|
||||||
|
$stmt->execute();
|
||||||
|
$deleted = $stmt->rowCount();
|
||||||
|
echo " → $deleted lignes supprimées\n";
|
||||||
|
$totalDeleted += $deleted;
|
||||||
|
|
||||||
|
// 6. Optimiser les tables (optionnel, peut être long sur de grosses tables)
|
||||||
|
if ($totalDeleted > 1000) {
|
||||||
|
echo "- Optimisation des tables...\n";
|
||||||
|
$tables = [
|
||||||
|
'sec_performance_metrics',
|
||||||
|
'sec_failed_login_attempts',
|
||||||
|
'sec_alerts',
|
||||||
|
'sec_blocked_ips'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
try {
|
||||||
|
$db->exec("OPTIMIZE TABLE $table");
|
||||||
|
echo " → Table $table optimisée\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ⚠ Impossible d'optimiser $table: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Statistiques finales
|
||||||
|
echo "\n=== RÉSUMÉ ===\n";
|
||||||
|
echo "Total supprimé: $totalDeleted lignes\n";
|
||||||
|
echo "IPs débloquées: $unblocked\n";
|
||||||
|
|
||||||
|
// Obtenir les statistiques actuelles
|
||||||
|
$stats = [];
|
||||||
|
$tables = [
|
||||||
|
'sec_alerts' => "SELECT COUNT(*) as total, SUM(resolved = 0) as active FROM sec_alerts",
|
||||||
|
'sec_performance_metrics' => "SELECT COUNT(*) as total FROM sec_performance_metrics",
|
||||||
|
'sec_failed_login_attempts' => "SELECT COUNT(*) as total FROM sec_failed_login_attempts",
|
||||||
|
'sec_blocked_ips' => "SELECT COUNT(*) as total, SUM(permanent = 1) as permanent FROM sec_blocked_ips WHERE unblocked_at IS NULL"
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "\nÉtat actuel des tables:\n";
|
||||||
|
foreach ($tables as $table => $query) {
|
||||||
|
$result = $db->query($query)->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($table === 'sec_alerts') {
|
||||||
|
echo "- $table: {$result['total']} total, {$result['active']} actives\n";
|
||||||
|
} elseif ($table === 'sec_blocked_ips') {
|
||||||
|
$permanent = $result['permanent'] ?? 0;
|
||||||
|
echo "- $table: {$result['total']} bloquées, $permanent permanentes\n";
|
||||||
|
} else {
|
||||||
|
echo "- $table: {$result['total']} enregistrements\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n[" . date('Y-m-d H:i:s') . "] Nettoyage terminé avec succès\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "\n❌ ERREUR: " . $e->getMessage() . "\n";
|
||||||
|
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
249
api/scripts/php/init_security_tables.php
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script d'initialisation des tables de sécurité
|
||||||
|
* Crée les tables si elles n'existent pas
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||||
|
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||||
|
|
||||||
|
// Initialiser la configuration
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
$config = $appConfig->getFullConfig();
|
||||||
|
|
||||||
|
// Initialiser la base de données
|
||||||
|
Database::init($config['database']);
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
echo "\n========================================\n";
|
||||||
|
echo " CRÉATION DES TABLES DE SÉCURITÉ\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Désactiver temporairement le mode strict pour les clés étrangères
|
||||||
|
$db->exec("SET FOREIGN_KEY_CHECKS = 0");
|
||||||
|
|
||||||
|
// 1. Table des alertes
|
||||||
|
echo "1. Création de la table sec_alerts...\n";
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS `sec_alerts` (
|
||||||
|
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`alert_type` VARCHAR(50) NOT NULL COMMENT 'Type d\'alerte (BRUTE_FORCE, SQL_ERROR, etc.)',
|
||||||
|
`alert_level` ENUM('INFO', 'WARNING', 'ERROR', 'CRITICAL', 'SECURITY') NOT NULL DEFAULT 'INFO',
|
||||||
|
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP source',
|
||||||
|
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||||
|
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté ou utilisé',
|
||||||
|
`endpoint` VARCHAR(255) DEFAULT NULL COMMENT 'Endpoint API concerné',
|
||||||
|
`method` VARCHAR(10) DEFAULT NULL COMMENT 'Méthode HTTP',
|
||||||
|
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels en JSON',
|
||||||
|
`occurrences` INT(11) DEFAULT 1 COMMENT 'Nombre d\'occurrences',
|
||||||
|
`first_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`last_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`email_sent` TINYINT(1) DEFAULT 0 COMMENT 'Email d\'alerte envoyé',
|
||||||
|
`email_sent_at` TIMESTAMP NULL DEFAULT NULL,
|
||||||
|
`resolved` TINYINT(1) DEFAULT 0 COMMENT 'Alerte résolue',
|
||||||
|
`resolved_at` TIMESTAMP NULL DEFAULT NULL,
|
||||||
|
`resolved_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID admin qui a résolu',
|
||||||
|
`notes` TEXT DEFAULT NULL COMMENT 'Notes de résolution',
|
||||||
|
KEY `idx_ip` (`ip_address`),
|
||||||
|
KEY `idx_type_time` (`alert_type`, `last_seen`),
|
||||||
|
KEY `idx_level` (`alert_level`),
|
||||||
|
KEY `idx_resolved` (`resolved`),
|
||||||
|
KEY `idx_user` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Alertes de sécurité et monitoring'
|
||||||
|
");
|
||||||
|
echo " ✓ Table sec_alerts créée\n";
|
||||||
|
|
||||||
|
// 2. Table des métriques de performance (SANS PARTITIONNEMENT)
|
||||||
|
echo "2. Création de la table sec_performance_metrics...\n";
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS `sec_performance_metrics` (
|
||||||
|
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`endpoint` VARCHAR(255) NOT NULL COMMENT 'Endpoint API',
|
||||||
|
`method` VARCHAR(10) NOT NULL COMMENT 'Méthode HTTP',
|
||||||
|
`response_time_ms` INT(11) NOT NULL COMMENT 'Temps de réponse total en ms',
|
||||||
|
`db_time_ms` INT(11) DEFAULT 0 COMMENT 'Temps cumulé des requêtes DB en ms',
|
||||||
|
`db_queries_count` INT(11) DEFAULT 0 COMMENT 'Nombre de requêtes DB',
|
||||||
|
`memory_peak_mb` FLOAT DEFAULT NULL COMMENT 'Pic mémoire en MB',
|
||||||
|
`memory_start_mb` FLOAT DEFAULT NULL COMMENT 'Mémoire au début en MB',
|
||||||
|
`http_status` INT(11) DEFAULT NULL COMMENT 'Code HTTP de réponse',
|
||||||
|
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||||
|
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP',
|
||||||
|
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent complet',
|
||||||
|
`request_size` INT(11) DEFAULT NULL COMMENT 'Taille de la requête en octets',
|
||||||
|
`response_size` INT(11) DEFAULT NULL COMMENT 'Taille de la réponse en octets',
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY `idx_endpoint_time` (`endpoint`, `created_at`),
|
||||||
|
KEY `idx_response_time` (`response_time_ms`),
|
||||||
|
KEY `idx_created` (`created_at`),
|
||||||
|
KEY `idx_status` (`http_status`),
|
||||||
|
KEY `idx_user` (`user_id`),
|
||||||
|
KEY `idx_date_endpoint` (`created_at`, `endpoint`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Métriques de performance des requêtes'
|
||||||
|
");
|
||||||
|
echo " ✓ Table sec_performance_metrics créée\n";
|
||||||
|
|
||||||
|
// 3. Table des tentatives de login échouées
|
||||||
|
echo "3. Création de la table sec_failed_login_attempts...\n";
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS `sec_failed_login_attempts` (
|
||||||
|
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté',
|
||||||
|
`encrypted_username` VARCHAR(255) DEFAULT NULL COMMENT 'Username chiffré si trouvé',
|
||||||
|
`ip_address` VARCHAR(45) NOT NULL COMMENT 'Adresse IP',
|
||||||
|
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent',
|
||||||
|
`attempt_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`error_type` VARCHAR(50) DEFAULT NULL COMMENT 'Type d\'erreur (invalid_password, user_not_found, etc.)',
|
||||||
|
`country_code` VARCHAR(2) DEFAULT NULL COMMENT 'Code pays de l\'IP (si géoloc activée)',
|
||||||
|
KEY `idx_ip_time` (`ip_address`, `attempt_time`),
|
||||||
|
KEY `idx_username` (`username`),
|
||||||
|
KEY `idx_encrypted_username` (`encrypted_username`),
|
||||||
|
KEY `idx_time` (`attempt_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Tentatives de connexion échouées'
|
||||||
|
");
|
||||||
|
echo " ✓ Table sec_failed_login_attempts créée\n";
|
||||||
|
|
||||||
|
// 4. Table des IPs bloquées
|
||||||
|
echo "4. Création de la table sec_blocked_ips...\n";
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS `sec_blocked_ips` (
|
||||||
|
`ip_address` VARCHAR(45) NOT NULL PRIMARY KEY COMMENT 'Adresse IP bloquée',
|
||||||
|
`reason` VARCHAR(255) NOT NULL COMMENT 'Raison du blocage',
|
||||||
|
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels',
|
||||||
|
`blocked_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`blocked_until` TIMESTAMP NOT NULL COMMENT 'Bloqué jusqu\'à',
|
||||||
|
`blocked_by` VARCHAR(50) DEFAULT 'system' COMMENT 'Qui a bloqué (system ou user ID)',
|
||||||
|
`permanent` TINYINT(1) DEFAULT 0 COMMENT 'Blocage permanent',
|
||||||
|
`unblocked_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Date de déblocage effectif',
|
||||||
|
`unblocked_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'Qui a débloqué',
|
||||||
|
`block_count` INT(11) DEFAULT 1 COMMENT 'Nombre de fois bloquée',
|
||||||
|
KEY `idx_blocked_until` (`blocked_until`),
|
||||||
|
KEY `idx_permanent` (`permanent`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IPs bloquées temporairement ou définitivement'
|
||||||
|
");
|
||||||
|
echo " ✓ Table sec_blocked_ips créée\n";
|
||||||
|
|
||||||
|
// 5. Créer les vues
|
||||||
|
echo "5. Création des vues...\n";
|
||||||
|
|
||||||
|
// Vue pour les alertes actives
|
||||||
|
$db->exec("
|
||||||
|
CREATE OR REPLACE VIEW sec_active_alerts AS
|
||||||
|
SELECT
|
||||||
|
a.*,
|
||||||
|
u.encrypted_name as user_name,
|
||||||
|
r.encrypted_name as resolver_name
|
||||||
|
FROM sec_alerts a
|
||||||
|
LEFT JOIN users u ON a.user_id = u.id
|
||||||
|
LEFT JOIN users r ON a.resolved_by = r.id
|
||||||
|
WHERE a.resolved = 0
|
||||||
|
OR (a.resolved = 1 AND a.resolved_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR))
|
||||||
|
ORDER BY
|
||||||
|
CASE a.alert_level
|
||||||
|
WHEN 'SECURITY' THEN 1
|
||||||
|
WHEN 'CRITICAL' THEN 2
|
||||||
|
WHEN 'ERROR' THEN 3
|
||||||
|
WHEN 'WARNING' THEN 4
|
||||||
|
WHEN 'INFO' THEN 5
|
||||||
|
END,
|
||||||
|
a.last_seen DESC
|
||||||
|
");
|
||||||
|
echo " ✓ Vue sec_active_alerts créée\n";
|
||||||
|
|
||||||
|
// Vue pour les IPs suspectes
|
||||||
|
$db->exec("
|
||||||
|
CREATE OR REPLACE VIEW sec_suspicious_ips AS
|
||||||
|
SELECT
|
||||||
|
ip_address,
|
||||||
|
COUNT(*) as total_attempts,
|
||||||
|
COUNT(DISTINCT username) as unique_usernames,
|
||||||
|
MIN(attempt_time) as first_attempt,
|
||||||
|
MAX(attempt_time) as last_attempt,
|
||||||
|
TIMESTAMPDIFF(MINUTE, MIN(attempt_time), MAX(attempt_time)) as timespan_minutes
|
||||||
|
FROM sec_failed_login_attempts
|
||||||
|
WHERE attempt_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||||
|
GROUP BY ip_address
|
||||||
|
HAVING total_attempts >= 5
|
||||||
|
OR unique_usernames >= 3
|
||||||
|
ORDER BY total_attempts DESC
|
||||||
|
");
|
||||||
|
echo " ✓ Vue sec_suspicious_ips créée\n";
|
||||||
|
|
||||||
|
// 6. Créer les index additionnels
|
||||||
|
echo "6. Création des index additionnels...\n";
|
||||||
|
|
||||||
|
// Index pour les requêtes fréquentes
|
||||||
|
$db->exec("CREATE INDEX IF NOT EXISTS idx_sec_metrics_recent ON sec_performance_metrics(created_at DESC, endpoint)");
|
||||||
|
$db->exec("CREATE INDEX IF NOT EXISTS idx_sec_alerts_recent ON sec_alerts(last_seen DESC, alert_level)");
|
||||||
|
$db->exec("CREATE INDEX IF NOT EXISTS idx_sec_failed_recent ON sec_failed_login_attempts(attempt_time DESC, ip_address)");
|
||||||
|
echo " ✓ Index créés\n";
|
||||||
|
|
||||||
|
// 7. Créer la procédure de nettoyage
|
||||||
|
echo "7. Création de la procédure de nettoyage...\n";
|
||||||
|
$db->exec("DROP PROCEDURE IF EXISTS sec_cleanup_old_data");
|
||||||
|
$db->exec("
|
||||||
|
CREATE PROCEDURE sec_cleanup_old_data(IN days_to_keep INT)
|
||||||
|
BEGIN
|
||||||
|
-- Nettoyer les métriques de performance
|
||||||
|
DELETE FROM sec_performance_metrics
|
||||||
|
WHERE created_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||||
|
|
||||||
|
-- Nettoyer les tentatives de login
|
||||||
|
DELETE FROM sec_failed_login_attempts
|
||||||
|
WHERE attempt_time < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||||
|
|
||||||
|
-- Nettoyer les alertes résolues
|
||||||
|
DELETE FROM sec_alerts
|
||||||
|
WHERE resolved = 1
|
||||||
|
AND resolved_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||||
|
|
||||||
|
-- Retourner le nombre de lignes supprimées
|
||||||
|
SELECT ROW_COUNT() as deleted_rows;
|
||||||
|
END
|
||||||
|
");
|
||||||
|
echo " ✓ Procédure sec_cleanup_old_data créée\n";
|
||||||
|
|
||||||
|
// Réactiver les clés étrangères
|
||||||
|
$db->exec("SET FOREIGN_KEY_CHECKS = 1");
|
||||||
|
|
||||||
|
// 8. Vérifier que tout est créé
|
||||||
|
echo "\n8. Vérification finale...\n";
|
||||||
|
$tables = ['sec_alerts', 'sec_performance_metrics', 'sec_failed_login_attempts', 'sec_blocked_ips'];
|
||||||
|
$allOk = true;
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$stmt = $db->query("SELECT COUNT(*) as count FROM $table");
|
||||||
|
if ($stmt) {
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
echo " ✓ Table $table : OK ({$result['count']} enregistrements)\n";
|
||||||
|
} else {
|
||||||
|
echo " ✗ Table $table : ERREUR\n";
|
||||||
|
$allOk = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($allOk) {
|
||||||
|
echo "\n========================================\n";
|
||||||
|
echo "✅ TOUTES LES TABLES ONT ÉTÉ CRÉÉES AVEC SUCCÈS\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
echo "Le système de sécurité est maintenant prêt à être utilisé.\n";
|
||||||
|
echo "Vous pouvez tester avec : php test_security.php\n\n";
|
||||||
|
} else {
|
||||||
|
echo "\n⚠️ Certaines tables n'ont pas pu être créées.\n";
|
||||||
|
echo "Vérifiez les erreurs ci-dessus.\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "\n❌ ERREUR SQL : " . $e->getMessage() . "\n\n";
|
||||||
|
echo "Code d'erreur : " . $e->getCode() . "\n";
|
||||||
|
echo "Vérifiez les permissions et la configuration de la base de données.\n\n";
|
||||||
|
exit(1);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "\n❌ ERREUR : " . $e->getMessage() . "\n\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
157
api/scripts/sql/create_chat_tables.sql
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
-- Script de création des tables pour le module Chat
|
||||||
|
-- Date : 2025-01-17
|
||||||
|
-- Version : 1.0
|
||||||
|
|
||||||
|
-- Tables préfixées "chat_" pour le module de messagerie
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- SUPPRESSION DES TABLES EXISTANTES
|
||||||
|
-- ============================================
|
||||||
|
-- Attention : Ceci supprimera toutes les données existantes du chat !
|
||||||
|
|
||||||
|
-- Désactiver temporairement les contraintes de clés étrangères
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- Supprimer la vue si elle existe
|
||||||
|
DROP VIEW IF EXISTS chat_rooms_with_last_message;
|
||||||
|
|
||||||
|
-- Supprimer les tables dans l'ordre inverse des dépendances
|
||||||
|
DROP TABLE IF EXISTS `chat_read_receipts`;
|
||||||
|
DROP TABLE IF EXISTS `chat_participants`;
|
||||||
|
DROP TABLE IF EXISTS `chat_messages`;
|
||||||
|
DROP TABLE IF EXISTS `chat_rooms`;
|
||||||
|
|
||||||
|
-- Supprimer toute autre table commençant par chat_ qui pourrait exister
|
||||||
|
-- Note : Cette procédure supprime dynamiquement toutes les tables avec le préfixe chat_
|
||||||
|
DELIMITER $$
|
||||||
|
DROP PROCEDURE IF EXISTS drop_chat_tables$$
|
||||||
|
CREATE PROCEDURE drop_chat_tables()
|
||||||
|
BEGIN
|
||||||
|
DECLARE done INT DEFAULT FALSE;
|
||||||
|
DECLARE tableName VARCHAR(255);
|
||||||
|
DECLARE cur CURSOR FOR
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name LIKE 'chat_%';
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||||
|
|
||||||
|
OPEN cur;
|
||||||
|
|
||||||
|
read_loop: LOOP
|
||||||
|
FETCH cur INTO tableName;
|
||||||
|
IF done THEN
|
||||||
|
LEAVE read_loop;
|
||||||
|
END IF;
|
||||||
|
SET @sql = CONCAT('DROP TABLE IF EXISTS `', tableName, '`');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
CLOSE cur;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
-- Exécuter la procédure
|
||||||
|
CALL drop_chat_tables();
|
||||||
|
|
||||||
|
-- Supprimer la procédure après utilisation
|
||||||
|
DROP PROCEDURE IF EXISTS drop_chat_tables;
|
||||||
|
|
||||||
|
-- Réactiver les contraintes de clés étrangères
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- CRÉATION DES NOUVELLES TABLES
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Table des salles de conversation
|
||||||
|
CREATE TABLE IF NOT EXISTS `chat_rooms` (
|
||||||
|
`id` VARCHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID de la salle',
|
||||||
|
`title` VARCHAR(255) DEFAULT NULL COMMENT 'Titre de la conversation',
|
||||||
|
`type` ENUM('private', 'group', 'broadcast') NOT NULL DEFAULT 'private' COMMENT 'Type de conversation',
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||||
|
`created_by` INT(11) UNSIGNED NOT NULL COMMENT 'ID du créateur',
|
||||||
|
`updated_at` TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Dernière modification',
|
||||||
|
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Conversation active',
|
||||||
|
KEY `idx_created_by` (`created_by`),
|
||||||
|
KEY `idx_type` (`type`),
|
||||||
|
KEY `idx_created_at` (`created_at`),
|
||||||
|
CONSTRAINT `fk_chat_rooms_creator` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Salles de conversation';
|
||||||
|
|
||||||
|
-- Table des messages
|
||||||
|
CREATE TABLE IF NOT EXISTS `chat_messages` (
|
||||||
|
`id` VARCHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID du message',
|
||||||
|
`room_id` VARCHAR(36) NOT NULL COMMENT 'ID de la salle',
|
||||||
|
`content` TEXT NOT NULL COMMENT 'Contenu du message',
|
||||||
|
`sender_id` INT(11) UNSIGNED NOT NULL COMMENT 'ID de l\'expéditeur',
|
||||||
|
`sent_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date d\'envoi',
|
||||||
|
`edited_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Date de modification',
|
||||||
|
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Message supprimé',
|
||||||
|
KEY `idx_room_id` (`room_id`),
|
||||||
|
KEY `idx_sender_id` (`sender_id`),
|
||||||
|
KEY `idx_sent_at` (`sent_at`),
|
||||||
|
KEY `idx_room_sent` (`room_id`, `sent_at`),
|
||||||
|
CONSTRAINT `fk_chat_messages_room` FOREIGN KEY (`room_id`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_chat_messages_sender` FOREIGN KEY (`sender_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Messages du chat';
|
||||||
|
|
||||||
|
-- Table des participants
|
||||||
|
CREATE TABLE IF NOT EXISTS `chat_participants` (
|
||||||
|
`room_id` VARCHAR(36) NOT NULL COMMENT 'ID de la salle',
|
||||||
|
`user_id` INT(11) UNSIGNED NOT NULL COMMENT 'ID de l\'utilisateur',
|
||||||
|
`role` INT(11) DEFAULT NULL COMMENT 'Rôle de l\'utilisateur (fk_role)',
|
||||||
|
`entite_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID de l\'entité',
|
||||||
|
`joined_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date d\'adhésion',
|
||||||
|
`left_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Date de départ',
|
||||||
|
`is_admin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Admin de la salle',
|
||||||
|
`last_read_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Dernière lecture',
|
||||||
|
PRIMARY KEY (`room_id`, `user_id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_entite_id` (`entite_id`),
|
||||||
|
KEY `idx_joined_at` (`joined_at`),
|
||||||
|
CONSTRAINT `fk_chat_participants_room` FOREIGN KEY (`room_id`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_chat_participants_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_chat_participants_entite` FOREIGN KEY (`entite_id`) REFERENCES `entites` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Participants aux conversations';
|
||||||
|
|
||||||
|
-- Table des accusés de lecture
|
||||||
|
CREATE TABLE IF NOT EXISTS `chat_read_receipts` (
|
||||||
|
`message_id` VARCHAR(36) NOT NULL COMMENT 'ID du message',
|
||||||
|
`user_id` INT(11) UNSIGNED NOT NULL COMMENT 'ID de l\'utilisateur',
|
||||||
|
`read_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de lecture',
|
||||||
|
PRIMARY KEY (`message_id`, `user_id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_read_at` (`read_at`),
|
||||||
|
CONSTRAINT `fk_chat_read_message` FOREIGN KEY (`message_id`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_chat_read_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Accusés de lecture';
|
||||||
|
|
||||||
|
-- Index supplémentaires pour les performances
|
||||||
|
CREATE INDEX idx_chat_active_rooms ON chat_rooms(is_active, created_at DESC);
|
||||||
|
CREATE INDEX idx_chat_user_rooms ON chat_participants(user_id, left_at, joined_at DESC);
|
||||||
|
CREATE INDEX idx_chat_unread ON chat_messages(room_id, sent_at) WHERE id NOT IN (SELECT message_id FROM chat_read_receipts);
|
||||||
|
|
||||||
|
-- Vue pour faciliter la récupération des conversations avec le dernier message
|
||||||
|
CREATE OR REPLACE VIEW chat_rooms_with_last_message AS
|
||||||
|
SELECT
|
||||||
|
r.*,
|
||||||
|
m.content as last_message_content,
|
||||||
|
m.sender_id as last_message_sender,
|
||||||
|
m.sent_at as last_message_at,
|
||||||
|
u.encrypted_name as last_message_sender_name
|
||||||
|
FROM chat_rooms r
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT m1.*
|
||||||
|
FROM chat_messages m1
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT room_id, MAX(sent_at) as max_sent_at
|
||||||
|
FROM chat_messages
|
||||||
|
WHERE is_deleted = 0
|
||||||
|
GROUP BY room_id
|
||||||
|
) m2 ON m1.room_id = m2.room_id AND m1.sent_at = m2.max_sent_at
|
||||||
|
) m ON r.id = m.room_id
|
||||||
|
LEFT JOIN users u ON m.sender_id = u.id
|
||||||
|
WHERE r.is_active = 1;
|
||||||
123
api/scripts/sql/create_security_tables.sql
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
-- Script de création des tables pour le module Security & Monitoring
|
||||||
|
-- Date : 2025-01-17
|
||||||
|
-- Version : 1.0
|
||||||
|
-- Préfixe : sec_ (security)
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- SUPPRESSION DES TABLES EXISTANTES (OPTIONNEL)
|
||||||
|
-- ============================================
|
||||||
|
-- Décommenter si vous voulez recréer les tables
|
||||||
|
|
||||||
|
-- DROP TABLE IF EXISTS `sec_blocked_ips`;
|
||||||
|
-- DROP TABLE IF EXISTS `sec_failed_login_attempts`;
|
||||||
|
-- DROP TABLE IF EXISTS `sec_performance_metrics`;
|
||||||
|
-- DROP TABLE IF EXISTS `sec_alerts`;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- CRÉATION DES TABLES DE SÉCURITÉ ET MONITORING
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Table principale des alertes de sécurité
|
||||||
|
CREATE TABLE IF NOT EXISTS `sec_alerts` (
|
||||||
|
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`alert_type` VARCHAR(50) NOT NULL COMMENT 'Type d\'alerte (BRUTE_FORCE, SQL_ERROR, etc.)',
|
||||||
|
`alert_level` ENUM('INFO', 'WARNING', 'ERROR', 'CRITICAL', 'SECURITY') NOT NULL DEFAULT 'INFO',
|
||||||
|
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP source',
|
||||||
|
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||||
|
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté ou utilisé',
|
||||||
|
`endpoint` VARCHAR(255) DEFAULT NULL COMMENT 'Endpoint API concerné',
|
||||||
|
`method` VARCHAR(10) DEFAULT NULL COMMENT 'Méthode HTTP',
|
||||||
|
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels en JSON',
|
||||||
|
`occurrences` INT(11) DEFAULT 1 COMMENT 'Nombre d\'occurrences',
|
||||||
|
`first_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`last_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`email_sent` TINYINT(1) DEFAULT 0 COMMENT 'Email d\'alerte envoyé',
|
||||||
|
`email_sent_at` TIMESTAMP NULL DEFAULT NULL,
|
||||||
|
`resolved` TINYINT(1) DEFAULT 0 COMMENT 'Alerte résolue',
|
||||||
|
`resolved_at` TIMESTAMP NULL DEFAULT NULL,
|
||||||
|
`resolved_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID admin qui a résolu',
|
||||||
|
`notes` TEXT DEFAULT NULL COMMENT 'Notes de résolution',
|
||||||
|
KEY `idx_ip` (`ip_address`),
|
||||||
|
KEY `idx_type_time` (`alert_type`, `last_seen`),
|
||||||
|
KEY `idx_level` (`alert_level`),
|
||||||
|
KEY `idx_resolved` (`resolved`),
|
||||||
|
KEY `idx_user` (`user_id`),
|
||||||
|
CONSTRAINT `fk_sec_alerts_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,
|
||||||
|
CONSTRAINT `fk_sec_alerts_resolver` FOREIGN KEY (`resolved_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Alertes de sécurité et monitoring';
|
||||||
|
|
||||||
|
-- Table des métriques de performance
|
||||||
|
CREATE TABLE IF NOT EXISTS `sec_performance_metrics` (
|
||||||
|
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`endpoint` VARCHAR(255) NOT NULL COMMENT 'Endpoint API',
|
||||||
|
`method` VARCHAR(10) NOT NULL COMMENT 'Méthode HTTP',
|
||||||
|
`response_time_ms` INT(11) NOT NULL COMMENT 'Temps de réponse total en ms',
|
||||||
|
`db_time_ms` INT(11) DEFAULT 0 COMMENT 'Temps cumulé des requêtes DB en ms',
|
||||||
|
`db_queries_count` INT(11) DEFAULT 0 COMMENT 'Nombre de requêtes DB',
|
||||||
|
`memory_peak_mb` FLOAT DEFAULT NULL COMMENT 'Pic mémoire en MB',
|
||||||
|
`memory_start_mb` FLOAT DEFAULT NULL COMMENT 'Mémoire au début en MB',
|
||||||
|
`http_status` INT(11) DEFAULT NULL COMMENT 'Code HTTP de réponse',
|
||||||
|
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||||
|
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP',
|
||||||
|
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent complet',
|
||||||
|
`request_size` INT(11) DEFAULT NULL COMMENT 'Taille de la requête en octets',
|
||||||
|
`response_size` INT(11) DEFAULT NULL COMMENT 'Taille de la réponse en octets',
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY `idx_endpoint_time` (`endpoint`, `created_at`),
|
||||||
|
KEY `idx_response_time` (`response_time_ms`),
|
||||||
|
KEY `idx_created` (`created_at`),
|
||||||
|
KEY `idx_status` (`http_status`),
|
||||||
|
KEY `idx_user` (`user_id`),
|
||||||
|
KEY `idx_date_endpoint` (`created_at`, `endpoint`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Métriques de performance des requêtes';
|
||||||
|
|
||||||
|
-- Table des tentatives de login échouées
|
||||||
|
CREATE TABLE IF NOT EXISTS `sec_failed_login_attempts` (
|
||||||
|
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté',
|
||||||
|
`encrypted_username` VARCHAR(255) DEFAULT NULL COMMENT 'Username chiffré si trouvé',
|
||||||
|
`ip_address` VARCHAR(45) NOT NULL COMMENT 'Adresse IP',
|
||||||
|
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent',
|
||||||
|
`attempt_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`error_type` VARCHAR(50) DEFAULT NULL COMMENT 'Type d\'erreur (invalid_password, user_not_found, etc.)',
|
||||||
|
`country_code` VARCHAR(2) DEFAULT NULL COMMENT 'Code pays de l\'IP (si géoloc activée)',
|
||||||
|
KEY `idx_ip_time` (`ip_address`, `attempt_time`),
|
||||||
|
KEY `idx_username` (`username`),
|
||||||
|
KEY `idx_encrypted_username` (`encrypted_username`),
|
||||||
|
KEY `idx_time` (`attempt_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Tentatives de connexion échouées';
|
||||||
|
|
||||||
|
-- Table des IPs bloquées
|
||||||
|
CREATE TABLE IF NOT EXISTS `sec_blocked_ips` (
|
||||||
|
`ip_address` VARCHAR(45) NOT NULL PRIMARY KEY COMMENT 'Adresse IP bloquée',
|
||||||
|
`reason` VARCHAR(255) NOT NULL COMMENT 'Raison du blocage',
|
||||||
|
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels',
|
||||||
|
`blocked_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`blocked_until` TIMESTAMP NOT NULL COMMENT 'Bloqué jusqu\'à',
|
||||||
|
`blocked_by` VARCHAR(50) DEFAULT 'system' COMMENT 'Qui a bloqué (system ou user ID)',
|
||||||
|
`permanent` TINYINT(1) DEFAULT 0 COMMENT 'Blocage permanent',
|
||||||
|
`unblocked_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Date de déblocage effectif',
|
||||||
|
`unblocked_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'Qui a débloqué',
|
||||||
|
`block_count` INT(11) DEFAULT 1 COMMENT 'Nombre de fois bloquée',
|
||||||
|
KEY `idx_blocked_until` (`blocked_until`),
|
||||||
|
KEY `idx_permanent` (`permanent`),
|
||||||
|
CONSTRAINT `fk_sec_blocked_unblocked_by` FOREIGN KEY (`unblocked_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IPs bloquées temporairement ou définitivement';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- INDEX ADDITIONNELS POUR PERFORMANCES
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Index pour requêtes de monitoring fréquentes
|
||||||
|
CREATE INDEX idx_sec_metrics_recent ON sec_performance_metrics(created_at DESC, endpoint);
|
||||||
|
CREATE INDEX idx_sec_alerts_recent ON sec_alerts(last_seen DESC, alert_level);
|
||||||
|
CREATE INDEX idx_sec_failed_recent ON sec_failed_login_attempts(attempt_time DESC, ip_address);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- FIN DU SCRIPT
|
||||||
|
-- ============================================
|
||||||
|
-- Note: La purge des données anciennes doit être gérée par:
|
||||||
|
-- 1. Un cron qui appelle l'endpoint API /api/admin/cleanup
|
||||||
|
-- 2. Ou directement via les méthodes cleanup des services PHP
|
||||||
35
api/scripts/sql/migration_username_utf8_support.sql
Normal file
@@ -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
|
||||||
875
api/src/Controllers/ChatController.php
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ use ApiService;
|
|||||||
require_once __DIR__ . '/../Services/LogService.php';
|
require_once __DIR__ . '/../Services/LogService.php';
|
||||||
require_once __DIR__ . '/../Services/ApiService.php';
|
require_once __DIR__ . '/../Services/ApiService.php';
|
||||||
require_once __DIR__ . '/EntiteController.php';
|
require_once __DIR__ . '/EntiteController.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/SecurityMonitor.php';
|
||||||
|
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
|
||||||
class LoginController {
|
class LoginController {
|
||||||
private PDO $db;
|
private PDO $db;
|
||||||
@@ -76,6 +79,11 @@ class LoginController {
|
|||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
// Enregistrer la tentative échouée
|
||||||
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
SecurityMonitor::recordFailedLogin($clientIp, $username, 'user_not_found', $userAgent);
|
||||||
|
|
||||||
LogService::log('Tentative de connexion GeoSector échouée : utilisateur non trouvé', [
|
LogService::log('Tentative de connexion GeoSector échouée : utilisateur non trouvé', [
|
||||||
'level' => 'warning',
|
'level' => 'warning',
|
||||||
'username' => $username
|
'username' => $username
|
||||||
@@ -88,6 +96,11 @@ class LoginController {
|
|||||||
$passwordValid = password_verify($data['password'], $user['user_pass_hash']);
|
$passwordValid = password_verify($data['password'], $user['user_pass_hash']);
|
||||||
|
|
||||||
if (!$passwordValid) {
|
if (!$passwordValid) {
|
||||||
|
// Enregistrer la tentative échouée
|
||||||
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
SecurityMonitor::recordFailedLogin($clientIp, $username, 'invalid_password', $userAgent);
|
||||||
|
|
||||||
LogService::log('Tentative de connexion GeoSector échouée : mot de passe incorrect', [
|
LogService::log('Tentative de connexion GeoSector échouée : mot de passe incorrect', [
|
||||||
'level' => 'warning',
|
'level' => 'warning',
|
||||||
'username' => $username
|
'username' => $username
|
||||||
@@ -769,6 +782,88 @@ class LoginController {
|
|||||||
$response['regions'] = $regionsData;
|
$response['regions'] = $regionsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ajout des informations du module chat
|
||||||
|
$chatData = [];
|
||||||
|
|
||||||
|
// Récupérer le nombre total de conversations de l'utilisateur
|
||||||
|
$roomCountStmt = $this->db->prepare('
|
||||||
|
SELECT COUNT(DISTINCT r.id) as total_rooms
|
||||||
|
FROM chat_rooms r
|
||||||
|
INNER JOIN chat_participants p ON r.id = p.room_id
|
||||||
|
WHERE p.user_id = :user_id
|
||||||
|
AND p.left_at IS NULL
|
||||||
|
AND r.is_active = 1
|
||||||
|
');
|
||||||
|
$roomCountStmt->execute(['user_id' => $user['id']]);
|
||||||
|
$roomCount = $roomCountStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$chatData['total_rooms'] = (int)($roomCount['total_rooms'] ?? 0);
|
||||||
|
|
||||||
|
// Récupérer le nombre de messages non lus
|
||||||
|
$unreadStmt = $this->db->prepare('
|
||||||
|
SELECT COUNT(*) as unread_count
|
||||||
|
FROM chat_messages m
|
||||||
|
INNER JOIN chat_participants p ON m.room_id = p.room_id
|
||||||
|
WHERE p.user_id = :user_id
|
||||||
|
AND p.left_at IS NULL
|
||||||
|
AND m.sender_id != :sender_id
|
||||||
|
AND m.sent_at > COALESCE(p.last_read_at, p.joined_at)
|
||||||
|
AND m.is_deleted = 0
|
||||||
|
');
|
||||||
|
$unreadStmt->execute([
|
||||||
|
'user_id' => $user['id'],
|
||||||
|
'sender_id' => $user['id']
|
||||||
|
]);
|
||||||
|
$unreadResult = $unreadStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$chatData['unread_messages'] = (int)($unreadResult['unread_count'] ?? 0);
|
||||||
|
|
||||||
|
// Récupérer la dernière conversation active (optionnel, pour affichage rapide)
|
||||||
|
$lastRoomStmt = $this->db->prepare('
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.title,
|
||||||
|
r.type,
|
||||||
|
(SELECT m.content
|
||||||
|
FROM chat_messages m
|
||||||
|
WHERE m.room_id = r.id
|
||||||
|
AND m.is_deleted = 0
|
||||||
|
ORDER BY m.sent_at DESC
|
||||||
|
LIMIT 1) as last_message,
|
||||||
|
(SELECT m.sent_at
|
||||||
|
FROM chat_messages m
|
||||||
|
WHERE m.room_id = r.id
|
||||||
|
AND m.is_deleted = 0
|
||||||
|
ORDER BY m.sent_at DESC
|
||||||
|
LIMIT 1) as last_message_at
|
||||||
|
FROM chat_rooms r
|
||||||
|
INNER JOIN chat_participants p ON r.id = p.room_id
|
||||||
|
WHERE p.user_id = :user_id
|
||||||
|
AND p.left_at IS NULL
|
||||||
|
AND r.is_active = 1
|
||||||
|
ORDER BY COALESCE(
|
||||||
|
(SELECT MAX(m.sent_at) FROM chat_messages m WHERE m.room_id = r.id),
|
||||||
|
r.created_at
|
||||||
|
) DESC
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
$lastRoomStmt->execute(['user_id' => $user['id']]);
|
||||||
|
$lastRoom = $lastRoomStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($lastRoom) {
|
||||||
|
$chatData['last_active_room'] = [
|
||||||
|
'id' => $lastRoom['id'],
|
||||||
|
'title' => $lastRoom['title'],
|
||||||
|
'type' => $lastRoom['type'],
|
||||||
|
'last_message' => $lastRoom['last_message'],
|
||||||
|
'last_message_at' => $lastRoom['last_message_at']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicateur si le chat est disponible pour cet utilisateur
|
||||||
|
$chatData['chat_enabled'] = true; // Peut être conditionné selon le rôle ou l'entité
|
||||||
|
|
||||||
|
// Ajouter les données du chat à la réponse
|
||||||
|
$response['chat'] = $chatData;
|
||||||
|
|
||||||
// Envoi de la réponse
|
// Envoi de la réponse
|
||||||
Response::json($response);
|
Response::json($response);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace App\Controllers;
|
|||||||
|
|
||||||
require_once __DIR__ . '/../Services/LogService.php';
|
require_once __DIR__ . '/../Services/LogService.php';
|
||||||
require_once __DIR__ . '/../Services/ApiService.php';
|
require_once __DIR__ . '/../Services/ApiService.php';
|
||||||
|
require_once __DIR__ . '/../Services/ReceiptService.php';
|
||||||
|
|
||||||
use PDO;
|
use PDO;
|
||||||
use PDOException;
|
use PDOException;
|
||||||
@@ -551,10 +552,44 @@ class PassageController {
|
|||||||
'operationId' => $operationId
|
'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([
|
Response::json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Passage créé avec succès',
|
'message' => 'Passage créé avec succès',
|
||||||
'passage_id' => $passageId
|
'passage_id' => $passageId,
|
||||||
|
'receipt_generated' => $receiptGenerated
|
||||||
], 201);
|
], 201);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
LogService::log('Erreur lors de la création du passage', [
|
LogService::log('Erreur lors de la création du passage', [
|
||||||
@@ -705,9 +740,52 @@ class PassageController {
|
|||||||
'passageId' => $passageId
|
'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([
|
Response::json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Passage mis à jour avec succès'
|
'message' => 'Passage mis à jour avec succès',
|
||||||
|
'receipt_generated' => $receiptGenerated
|
||||||
], 200);
|
], 200);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
LogService::log('Erreur lors de la mise à jour du passage', [
|
LogService::log('Erreur lors de la mise à jour du passage', [
|
||||||
@@ -800,4 +878,150 @@ class PassageController {
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le reçu PDF d'un passage
|
||||||
|
*
|
||||||
|
* @param string $id ID du passage
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function getReceipt(string $id): void {
|
||||||
|
try {
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
if (!$userId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||||
|
], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$passageId = (int)$id;
|
||||||
|
|
||||||
|
// Vérifier que le passage existe et que l'utilisateur y a accès
|
||||||
|
$entiteId = $this->getUserEntiteId($userId);
|
||||||
|
if (!$entiteId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Entité non trouvée pour cet utilisateur'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les informations du passage et du reçu
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT p.id, p.nom_recu, p.date_creat_recu, p.fk_operation, o.fk_entite
|
||||||
|
FROM ope_pass p
|
||||||
|
INNER JOIN operations o ON p.fk_operation = o.id
|
||||||
|
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$passageId, $entiteId]);
|
||||||
|
$passage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$passage) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Passage non trouvé ou accès non autorisé'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($passage['nom_recu'])) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Aucun reçu disponible pour ce passage'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le fichier depuis la table medias
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT file_path, mime_type, file_size, fichier
|
||||||
|
FROM medias
|
||||||
|
WHERE support = ? AND support_id = ? AND file_category = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
$stmt->execute(['passage', $passageId, 'recu']);
|
||||||
|
$media = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$media) {
|
||||||
|
// Si pas trouvé dans medias, essayer de construire le chemin
|
||||||
|
$filePath = __DIR__ . '/../../uploads/entites/' . $passage['fk_entite'] .
|
||||||
|
'/recus/' . $passage['fk_operation'] . '/' . $passage['nom_recu'];
|
||||||
|
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Fichier reçu introuvable'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$media = [
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'fichier' => $passage['nom_recu'],
|
||||||
|
'file_size' => filesize($filePath)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le fichier existe
|
||||||
|
if (!file_exists($media['file_path'])) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Fichier reçu introuvable sur le serveur'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lire le contenu du fichier
|
||||||
|
$pdfContent = file_get_contents($media['file_path']);
|
||||||
|
if ($pdfContent === false) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Impossible de lire le fichier reçu'
|
||||||
|
], 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 1: Retourner le PDF directement (pour téléchargement)
|
||||||
|
if (isset($_GET['download']) && $_GET['download'] === 'true') {
|
||||||
|
header('Content-Type: ' . $media['mime_type']);
|
||||||
|
header('Content-Disposition: attachment; filename="' . $media['fichier'] . '"');
|
||||||
|
header('Content-Length: ' . $media['file_size']);
|
||||||
|
header('Cache-Control: no-cache, must-revalidate');
|
||||||
|
echo $pdfContent;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 2: Retourner le PDF en base64 dans JSON (pour Flutter)
|
||||||
|
$base64 = base64_encode($pdfContent);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'receipt' => [
|
||||||
|
'passage_id' => $passageId,
|
||||||
|
'file_name' => $media['fichier'],
|
||||||
|
'mime_type' => $media['mime_type'],
|
||||||
|
'file_size' => $media['file_size'],
|
||||||
|
'created_at' => $passage['date_creat_recu'],
|
||||||
|
'data_base64' => $base64
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la récupération du reçu', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'passageId' => $id,
|
||||||
|
'userId' => $userId ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la récupération du reçu'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
251
api/src/Controllers/SecurityController.php
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../Services/Security/AlertService.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/SecurityMonitor.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/PerformanceMonitor.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/IPBlocker.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/EmailThrottler.php';
|
||||||
|
|
||||||
|
use App\Services\Security\AlertService;
|
||||||
|
use App\Services\Security\SecurityMonitor;
|
||||||
|
use App\Services\Security\PerformanceMonitor;
|
||||||
|
use App\Services\Security\IPBlocker;
|
||||||
|
use App\Services\Security\EmailThrottler;
|
||||||
|
use Database;
|
||||||
|
use Session;
|
||||||
|
use Response;
|
||||||
|
use Request;
|
||||||
|
|
||||||
|
class SecurityController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les métriques de performance
|
||||||
|
* GET /api/admin/metrics
|
||||||
|
*/
|
||||||
|
public function getMetrics(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpoint = Request::getQuery('endpoint');
|
||||||
|
$hours = (int)(Request::getQuery('hours') ?? 24);
|
||||||
|
|
||||||
|
$stats = PerformanceMonitor::getStats($endpoint, $hours);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $stats
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les alertes actives
|
||||||
|
* GET /api/admin/alerts
|
||||||
|
*/
|
||||||
|
public function getAlerts(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = (int)(Request::getQuery('limit') ?? 50);
|
||||||
|
$alerts = AlertService::getActiveAlerts($limit);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $alerts
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résoudre une alerte
|
||||||
|
* POST /api/admin/alerts/:id/resolve
|
||||||
|
*/
|
||||||
|
public function resolveAlert(string $id): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::getJson();
|
||||||
|
$notes = $data['notes'] ?? '';
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
|
||||||
|
$success = AlertService::resolve((int)$id, $userId, $notes);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => $success,
|
||||||
|
'message' => $success ? 'Alerte résolue' : 'Erreur lors de la résolution'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir les IPs bloquées
|
||||||
|
* GET /api/admin/blocked-ips
|
||||||
|
*/
|
||||||
|
public function getBlockedIPs(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeOnly = Request::getQuery('active_only') !== 'false';
|
||||||
|
$ips = IPBlocker::getBlockedIPs($activeOnly);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $ips
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Débloquer une IP
|
||||||
|
* POST /api/admin/unblock-ip
|
||||||
|
*/
|
||||||
|
public function unblockIP(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
if (!isset($data['ip'])) {
|
||||||
|
Response::json(['error' => 'IP address required'], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
$success = IPBlocker::unblock($data['ip'], $userId);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => $success,
|
||||||
|
'message' => $success ? 'IP débloquée' : 'Erreur lors du déblocage'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bloquer une IP manuellement
|
||||||
|
* POST /api/admin/block-ip
|
||||||
|
*/
|
||||||
|
public function blockIP(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
if (!isset($data['ip'])) {
|
||||||
|
Response::json(['error' => 'IP address required'], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reason = $data['reason'] ?? 'Blocked by admin';
|
||||||
|
$duration = (int)($data['duration'] ?? 3600);
|
||||||
|
$permanent = $data['permanent'] ?? false;
|
||||||
|
|
||||||
|
if ($permanent) {
|
||||||
|
$success = IPBlocker::blockPermanent($data['ip'], $reason, 'admin_' . Session::getUserId());
|
||||||
|
} else {
|
||||||
|
$success = IPBlocker::block($data['ip'], $duration, $reason, 'admin_' . Session::getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => $success,
|
||||||
|
'message' => $success ? 'IP bloquée' : 'Erreur lors du blocage'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir le rapport de sécurité
|
||||||
|
* GET /api/admin/security-report
|
||||||
|
*/
|
||||||
|
public function getSecurityReport(): void {
|
||||||
|
// Vérifier l'authentification et les droits admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 2) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compiler le rapport
|
||||||
|
$report = [
|
||||||
|
'security_stats' => SecurityMonitor::getSecurityStats(),
|
||||||
|
'performance_stats' => PerformanceMonitor::getStats(null, 24),
|
||||||
|
'blocked_ips_stats' => IPBlocker::getStats(),
|
||||||
|
'email_throttle_stats' => (new EmailThrottler())->getStats(),
|
||||||
|
'recent_alerts' => AlertService::getActiveAlerts(10)
|
||||||
|
];
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $report,
|
||||||
|
'generated_at' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyer les anciennes données
|
||||||
|
* POST /api/admin/cleanup
|
||||||
|
*/
|
||||||
|
public function cleanup(): void {
|
||||||
|
// Vérifier l'authentification et les droits super admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 9) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::getJson();
|
||||||
|
$daysToKeep = (int)($data['days_to_keep'] ?? 30);
|
||||||
|
|
||||||
|
// Nettoyer les métriques de performance
|
||||||
|
$cleanedMetrics = PerformanceMonitor::cleanup($daysToKeep);
|
||||||
|
|
||||||
|
// Nettoyer les IPs expirées
|
||||||
|
$cleanedIPs = IPBlocker::cleanupExpired();
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Nettoyage effectué',
|
||||||
|
'cleaned' => [
|
||||||
|
'performance_metrics' => $cleanedMetrics,
|
||||||
|
'expired_ips' => $cleanedIPs
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tester les alertes email
|
||||||
|
* POST /api/admin/test-alert
|
||||||
|
*/
|
||||||
|
public function testAlert(): void {
|
||||||
|
// Vérifier l'authentification et les droits super admin
|
||||||
|
if (!Session::isLoggedIn() || Session::getRole() < 9) {
|
||||||
|
Response::json(['error' => 'Unauthorized'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déclencher une alerte de test
|
||||||
|
AlertService::trigger('TEST_ALERT', [
|
||||||
|
'message' => 'Ceci est une alerte de test déclenchée manuellement',
|
||||||
|
'triggered_by' => Session::getUsername(),
|
||||||
|
'timestamp' => date('Y-m-d H:i:s')
|
||||||
|
], 'INFO');
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Alerte de test envoyée'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -214,11 +214,41 @@ class UserController {
|
|||||||
$data = Request::getJson();
|
$data = Request::getJson();
|
||||||
$currentUserId = Session::getUserId();
|
$currentUserId = Session::getUserId();
|
||||||
|
|
||||||
|
// Log de début de création avec les données reçues (sans données sensibles)
|
||||||
|
LogService::log('Tentative de création d\'utilisateur', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'fields_received' => array_keys($data ?? []),
|
||||||
|
'has_email' => isset($data['email']),
|
||||||
|
'has_name' => isset($data['name']),
|
||||||
|
'has_username' => isset($data['username']),
|
||||||
|
'has_password' => isset($data['password'])
|
||||||
|
]);
|
||||||
|
|
||||||
// Validation des données requises
|
// Validation des données requises
|
||||||
if (!isset($data['email'], $data['name'])) {
|
if (!isset($data['email']) || empty(trim($data['email']))) {
|
||||||
|
LogService::log('Erreur création utilisateur : Email manquant', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Email et nom requis'
|
'message' => 'Email requis',
|
||||||
|
'field' => 'email'
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($data['name']) || empty(trim($data['name']))) {
|
||||||
|
LogService::log('Erreur création utilisateur : Nom manquant', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $data['email'] ?? 'non fourni'
|
||||||
|
]);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Nom requis',
|
||||||
|
'field' => 'name'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -260,9 +290,16 @@ class UserController {
|
|||||||
|
|
||||||
// Validation de l'email
|
// Validation de l'email
|
||||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
LogService::log('Erreur création utilisateur : Format email invalide', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Format d\'email invalide'
|
'message' => 'Format d\'email invalide',
|
||||||
|
'field' => 'email',
|
||||||
|
'value' => $email
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -290,20 +327,56 @@ class UserController {
|
|||||||
if ($chkUsernameManuel === 1) {
|
if ($chkUsernameManuel === 1) {
|
||||||
// Username manuel obligatoire
|
// Username manuel obligatoire
|
||||||
if (!isset($data['username']) || empty(trim($data['username']))) {
|
if (!isset($data['username']) || empty(trim($data['username']))) {
|
||||||
|
LogService::log('Erreur création utilisateur : Username manuel requis mais non fourni', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'entite_id' => $entiteId,
|
||||||
|
'chk_username_manuel' => $chkUsernameManuel
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Le nom d\'utilisateur est requis pour cette entité'
|
'message' => 'Identifiant requis',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Saisie manuelle obligatoire pour cette entité'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$username = trim(strtolower($data['username']));
|
// Trim du username mais on garde la casse originale (plus de lowercase forcé)
|
||||||
|
$username = trim($data['username']);
|
||||||
|
|
||||||
// Validation du format du username
|
// Validation ultra-souple : seulement la longueur en caractères UTF-8
|
||||||
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
|
$usernameLength = mb_strlen($username, 'UTF-8');
|
||||||
|
|
||||||
|
if ($usernameLength < 8) {
|
||||||
|
LogService::log('Erreur création utilisateur : Username trop court', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'username_length' => $usernameLength
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Format du nom d\'utilisateur invalide (10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _)'
|
'message' => 'Identifiant trop court',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Minimum 8 caractères'
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($usernameLength > 30) {
|
||||||
|
LogService::log('Erreur création utilisateur : Username trop long', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'username_length' => $usernameLength
|
||||||
|
]);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Identifiant trop long',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Maximum 30 caractères'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -316,7 +389,8 @@ class UserController {
|
|||||||
if ($checkUsernameStmt->fetch()) {
|
if ($checkUsernameStmt->fetch()) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Ce nom d\'utilisateur est déjà utilisé dans GeoSector'
|
'message' => 'Identifiant déjà utilisé',
|
||||||
|
'field' => 'username'
|
||||||
], 409);
|
], 409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -338,9 +412,18 @@ class UserController {
|
|||||||
if ($chkMdpManuel === 1) {
|
if ($chkMdpManuel === 1) {
|
||||||
// Mot de passe manuel obligatoire
|
// Mot de passe manuel obligatoire
|
||||||
if (!isset($data['password']) || empty($data['password'])) {
|
if (!isset($data['password']) || empty($data['password'])) {
|
||||||
|
LogService::log('Erreur création utilisateur : Mot de passe manuel requis mais non fourni', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'createdBy' => $currentUserId,
|
||||||
|
'email' => $email,
|
||||||
|
'entite_id' => $entiteId,
|
||||||
|
'chk_mdp_manuel' => $chkMdpManuel
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Le mot de passe est requis pour cette entité'
|
'message' => 'Mot de passe requis',
|
||||||
|
'field' => 'password',
|
||||||
|
'details' => 'Saisie manuelle obligatoire pour cette entité'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -927,22 +1010,60 @@ class UserController {
|
|||||||
try {
|
try {
|
||||||
$data = Request::getJson();
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
// Log de la requête
|
||||||
|
LogService::log('Vérification de disponibilité username', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'checkedBy' => Session::getUserId(),
|
||||||
|
'has_username' => isset($data['username'])
|
||||||
|
]);
|
||||||
|
|
||||||
// Validation de la présence du username
|
// Validation de la présence du username
|
||||||
if (!isset($data['username']) || empty(trim($data['username']))) {
|
if (!isset($data['username']) || empty(trim($data['username']))) {
|
||||||
|
LogService::log('Erreur vérification username : Username manquant', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'checkedBy' => Session::getUserId()
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Username requis pour la vérification'
|
'message' => 'Identifiant requis',
|
||||||
|
'field' => 'username'
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$username = trim(strtolower($data['username']));
|
// Trim du username mais on garde la casse originale
|
||||||
|
$username = trim($data['username']);
|
||||||
|
|
||||||
// Validation du format du username
|
// Validation ultra-souple : seulement la longueur en caractères UTF-8
|
||||||
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
|
$usernameLength = mb_strlen($username, 'UTF-8');
|
||||||
|
|
||||||
|
if ($usernameLength < 8) {
|
||||||
|
LogService::log('Erreur vérification username : Username trop court', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'checkedBy' => Session::getUserId(),
|
||||||
|
'username_length' => $usernameLength
|
||||||
|
]);
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Format invalide : 10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _',
|
'message' => 'Identifiant trop court',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Minimum 8 caractères',
|
||||||
|
'available' => false
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($usernameLength > 30) {
|
||||||
|
LogService::log('Erreur vérification username : Username trop long', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'checkedBy' => Session::getUserId(),
|
||||||
|
'username_length' => $usernameLength
|
||||||
|
]);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Identifiant trop long',
|
||||||
|
'field' => 'username',
|
||||||
|
'details' => 'Maximum 30 caractères',
|
||||||
'available' => false
|
'available' => false
|
||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/MonitoredDatabase.php';
|
||||||
|
require_once __DIR__ . '/../Services/Security/AlertService.php';
|
||||||
|
|
||||||
|
use App\Services\Security\AlertService;
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
private static ?PDO $instance = null;
|
private static ?PDO $instance = null;
|
||||||
private static array $config;
|
private static array $config;
|
||||||
@@ -23,13 +28,22 @@ class Database {
|
|||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
self::$instance = new PDO(
|
// Utiliser MonitoredDatabase pour le monitoring
|
||||||
|
self::$instance = new MonitoredDatabase(
|
||||||
$dsn,
|
$dsn,
|
||||||
self::$config['username'],
|
self::$config['username'],
|
||||||
self::$config['password'],
|
self::$config['password'],
|
||||||
$options
|
$options
|
||||||
);
|
);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
|
// Créer une alerte pour la connexion échouée
|
||||||
|
AlertService::trigger('DB_CONNECTION', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'host' => self::$config['host'],
|
||||||
|
'database' => self::$config['name'],
|
||||||
|
'message' => 'Échec de connexion à la base de données'
|
||||||
|
], 'CRITICAL');
|
||||||
|
|
||||||
throw new RuntimeException("Database connection failed: " . $e->getMessage());
|
throw new RuntimeException("Database connection failed: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
182
api/src/Core/MonitoredDatabase.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,13 +34,14 @@ class Router {
|
|||||||
$this->post('log', ['LogController', 'index']);
|
$this->post('log', ['LogController', 'index']);
|
||||||
|
|
||||||
// Routes privées utilisateurs
|
// Routes privées utilisateurs
|
||||||
|
// IMPORTANT: Les routes spécifiques doivent être déclarées AVANT les routes avec paramètres
|
||||||
|
$this->post('users/check-username', ['UserController', 'checkUsername']); // Déplacé avant les routes avec :id
|
||||||
$this->get('users', ['UserController', 'getUsers']);
|
$this->get('users', ['UserController', 'getUsers']);
|
||||||
$this->get('users/:id', ['UserController', 'getUserById']);
|
$this->get('users/:id', ['UserController', 'getUserById']);
|
||||||
$this->post('users', ['UserController', 'createUser']);
|
$this->post('users', ['UserController', 'createUser']);
|
||||||
$this->put('users/:id', ['UserController', 'updateUser']);
|
$this->put('users/:id', ['UserController', 'updateUser']);
|
||||||
$this->delete('users/:id', ['UserController', 'deleteUser']);
|
$this->delete('users/:id', ['UserController', 'deleteUser']);
|
||||||
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
|
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
|
||||||
$this->post('users/check-username', ['UserController', 'checkUsername']);
|
|
||||||
$this->post('logout', ['LoginController', 'logout']);
|
$this->post('logout', ['LoginController', 'logout']);
|
||||||
|
|
||||||
// Routes entités
|
// Routes entités
|
||||||
@@ -69,6 +70,7 @@ class Router {
|
|||||||
// Routes passages
|
// Routes passages
|
||||||
$this->get('passages', ['PassageController', 'getPassages']);
|
$this->get('passages', ['PassageController', 'getPassages']);
|
||||||
$this->get('passages/:id', ['PassageController', 'getPassageById']);
|
$this->get('passages/:id', ['PassageController', 'getPassageById']);
|
||||||
|
$this->get('passages/:id/receipt', ['PassageController', 'getReceipt']);
|
||||||
$this->get('passages/operation/:operation_id', ['PassageController', 'getPassagesByOperation']);
|
$this->get('passages/operation/:operation_id', ['PassageController', 'getPassagesByOperation']);
|
||||||
$this->post('passages', ['PassageController', 'createPassage']);
|
$this->post('passages', ['PassageController', 'createPassage']);
|
||||||
$this->put('passages/:id', ['PassageController', 'updatePassage']);
|
$this->put('passages/:id', ['PassageController', 'updatePassage']);
|
||||||
@@ -97,6 +99,25 @@ class Router {
|
|||||||
$this->post('password/check', ['PasswordController', 'checkStrength']);
|
$this->post('password/check', ['PasswordController', 'checkStrength']);
|
||||||
$this->post('password/compromised', ['PasswordController', 'checkCompromised']);
|
$this->post('password/compromised', ['PasswordController', 'checkCompromised']);
|
||||||
$this->get('password/generate', ['PasswordController', 'generate']);
|
$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 {
|
public function handle(): void {
|
||||||
|
|||||||
622
api/src/Services/ReceiptService.php
Normal 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
@@ -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
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script de test pour la création d'utilisateurs
|
||||||
|
* Teste les différents cas qui peuvent causer une erreur 400
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
$apiUrl = 'http://localhost/api';
|
||||||
|
$sessionId = ''; // À remplir avec un session_id valide
|
||||||
|
|
||||||
|
// Couleurs pour la sortie
|
||||||
|
$red = "\033[31m";
|
||||||
|
$green = "\033[32m";
|
||||||
|
$yellow = "\033[33m";
|
||||||
|
$blue = "\033[34m";
|
||||||
|
$reset = "\033[0m";
|
||||||
|
|
||||||
|
function testRequest($endpoint, $method, $data = null, $sessionId = '') {
|
||||||
|
$ch = curl_init($endpoint);
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||||
|
|
||||||
|
if ($sessionId) {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $sessionId
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => $httpCode,
|
||||||
|
'body' => json_decode($response, true) ?: $response
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $blue . "=== Test de création d'utilisateur et vérification username ===" . $reset . "\n\n";
|
||||||
|
|
||||||
|
// Test 1: Vérification username sans données
|
||||||
|
echo $yellow . "Test 1: POST /api/users/check-username sans données" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', [], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 2: Vérification username avec format invalide (trop court)
|
||||||
|
echo $yellow . "Test 2: POST /api/users/check-username avec username trop court" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => 'abc'], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Format attendu: " . ($result['body']['format'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 3: Vérification username avec format invalide (commence par un chiffre)
|
||||||
|
echo $yellow . "Test 3: POST /api/users/check-username avec username commençant par un chiffre" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => '123test.user'], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Valeur testée: " . ($result['body']['value'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 4: Vérification username valide
|
||||||
|
echo $yellow . "Test 4: POST /api/users/check-username avec username valide" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => 'test.user123'], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 200 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Disponible: " . ($result['body']['available'] ? 'Oui' : 'Non') . "\n";
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 5: Création utilisateur sans email
|
||||||
|
echo $yellow . "Test 5: POST /api/users sans email" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users', 'POST', [
|
||||||
|
'name' => 'Test User',
|
||||||
|
'fk_entite' => 1
|
||||||
|
], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 6: Création utilisateur sans nom
|
||||||
|
echo $yellow . "Test 6: POST /api/users sans nom" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users', 'POST', [
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'fk_entite' => 1
|
||||||
|
], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 7: Création utilisateur avec email invalide
|
||||||
|
echo $yellow . "Test 7: POST /api/users avec email invalide" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users', 'POST', [
|
||||||
|
'email' => 'invalid-email',
|
||||||
|
'name' => 'Test User',
|
||||||
|
'fk_entite' => 1
|
||||||
|
], $sessionId);
|
||||||
|
echo "Code HTTP: " . ($result['code'] == 400 ? $green : $red) . $result['code'] . $reset . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
echo "Valeur testée: " . ($result['body']['value'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test 8: Création utilisateur pour entité avec username manuel mais sans username
|
||||||
|
echo $yellow . "Test 8: POST /api/users pour entité avec chk_username_manuel=1 sans username" . $reset . "\n";
|
||||||
|
$result = testRequest($apiUrl . '/users', 'POST', [
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'name' => 'Test User',
|
||||||
|
'fk_entite' => 5 // Supposons que l'entité 5 a chk_username_manuel=1
|
||||||
|
], $sessionId);
|
||||||
|
echo "Code HTTP: " . $result['code'] . "\n";
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
echo "Field: " . ($result['body']['field'] ?? 'non spécifié') . "\n";
|
||||||
|
echo "Reason: " . ($result['body']['reason'] ?? 'non spécifié') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
echo $blue . "=== Fin des tests ===" . $reset . "\n";
|
||||||
|
echo "\nRappel: Pour que ces tests fonctionnent, vous devez:\n";
|
||||||
|
echo "1. Avoir un serveur local en cours d'exécution\n";
|
||||||
|
echo "2. Remplir la variable \$sessionId avec un token de session valide\n";
|
||||||
|
echo "3. Vérifier les logs dans /logs/ pour voir les détails\n";
|
||||||
132
api/tests/test_username_validation.php
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script de test pour la validation ultra-souple des usernames
|
||||||
|
* Teste les nouvelles règles : 8-30 caractères UTF-8, tous caractères acceptés
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
$apiUrl = 'http://localhost/api';
|
||||||
|
$sessionId = ''; // À remplir avec un session_id valide
|
||||||
|
|
||||||
|
// Couleurs pour la sortie
|
||||||
|
$red = "\033[31m";
|
||||||
|
$green = "\033[32m";
|
||||||
|
$yellow = "\033[33m";
|
||||||
|
$blue = "\033[34m";
|
||||||
|
$reset = "\033[0m";
|
||||||
|
|
||||||
|
function testRequest($endpoint, $method, $data = null, $sessionId = '') {
|
||||||
|
$ch = curl_init($endpoint);
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||||
|
|
||||||
|
if ($sessionId) {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json; charset=UTF-8',
|
||||||
|
'Authorization: Bearer ' . $sessionId
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json; charset=UTF-8']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => $httpCode,
|
||||||
|
'body' => json_decode($response, true) ?: $response
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $blue . "=== Test de validation ultra-souple des usernames ===" . $reset . "\n";
|
||||||
|
echo "Nouvelles règles : 8-30 caractères UTF-8, tous caractères acceptés\n\n";
|
||||||
|
|
||||||
|
// Cas qui doivent ÉCHOUER (trop court ou trop long)
|
||||||
|
$testsFail = [
|
||||||
|
['username' => 'abc', 'desc' => 'Trop court (3 car.)'],
|
||||||
|
['username' => '1234567', 'desc' => 'Trop court (7 car.)'],
|
||||||
|
['username' => str_repeat('a', 31), 'desc' => 'Trop long (31 car.)'],
|
||||||
|
['username' => str_repeat('😀', 31), 'desc' => 'Trop long (31 émojis)'],
|
||||||
|
['username' => ' ', 'desc' => 'Espaces seulement (devient vide après trim)'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Cas qui doivent RÉUSSIR
|
||||||
|
$testsSuccess = [
|
||||||
|
['username' => 'Jean-Pierre', 'desc' => 'Nom avec tiret et majuscules'],
|
||||||
|
['username' => '12345678', 'desc' => 'Chiffres seulement (8 car.)'],
|
||||||
|
['username' => 'user@company', 'desc' => 'Avec arobase'],
|
||||||
|
['username' => 'Marie 2024', 'desc' => 'Avec espace'],
|
||||||
|
['username' => 'josé.garcía', 'desc' => 'Avec accents'],
|
||||||
|
['username' => '用户2024', 'desc' => 'Caractères chinois'],
|
||||||
|
['username' => 'مستخدم123', 'desc' => 'Caractères arabes'],
|
||||||
|
['username' => 'Admin_#123!', 'desc' => 'Caractères spéciaux'],
|
||||||
|
['username' => '😀🎉Party2024', 'desc' => 'Avec émojis'],
|
||||||
|
['username' => 'Пользователь', 'desc' => 'Cyrillique'],
|
||||||
|
['username' => ' espacé ', 'desc' => 'Espaces (seront trimés)'],
|
||||||
|
['username' => 'a' . str_repeat('b', 28) . 'c', 'desc' => 'Exactement 30 car.'],
|
||||||
|
];
|
||||||
|
|
||||||
|
echo $yellow . "Tests qui doivent ÉCHOUER :" . $reset . "\n\n";
|
||||||
|
|
||||||
|
foreach ($testsFail as $index => $test) {
|
||||||
|
echo "Test " . ($index + 1) . ": " . $test['desc'] . "\n";
|
||||||
|
echo "Username: \"" . $test['username'] . "\"\n";
|
||||||
|
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => $test['username']], $sessionId);
|
||||||
|
|
||||||
|
$isExpectedFailure = ($result['code'] == 400);
|
||||||
|
echo "Code HTTP: " . ($isExpectedFailure ? $green : $red) . $result['code'] . $reset;
|
||||||
|
echo " - " . ($isExpectedFailure ? $green . "✓ OK" : $red . "✗ ERREUR") . $reset . "\n";
|
||||||
|
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
if (isset($result['body']['details'])) {
|
||||||
|
echo "Détails: " . $result['body']['details'] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $yellow . "Tests qui doivent RÉUSSIR :" . $reset . "\n\n";
|
||||||
|
|
||||||
|
foreach ($testsSuccess as $index => $test) {
|
||||||
|
echo "Test " . ($index + 1) . ": " . $test['desc'] . "\n";
|
||||||
|
echo "Username: \"" . $test['username'] . "\"\n";
|
||||||
|
echo "Longueur: " . mb_strlen(trim($test['username']), 'UTF-8') . " caractères\n";
|
||||||
|
|
||||||
|
$result = testRequest($apiUrl . '/users/check-username', 'POST', ['username' => $test['username']], $sessionId);
|
||||||
|
|
||||||
|
$isExpectedSuccess = ($result['code'] == 200);
|
||||||
|
echo "Code HTTP: " . ($isExpectedSuccess ? $green : $red) . $result['code'] . $reset;
|
||||||
|
echo " - " . ($isExpectedSuccess ? $green . "✓ OK" : $red . "✗ ERREUR") . $reset . "\n";
|
||||||
|
|
||||||
|
if (is_array($result['body'])) {
|
||||||
|
if ($result['code'] == 200) {
|
||||||
|
echo "Disponible: " . ($result['body']['available'] ? $green . "Oui" : $yellow . "Non (déjà pris)") . $reset . "\n";
|
||||||
|
} else {
|
||||||
|
echo "Message: " . $result['body']['message'] . "\n";
|
||||||
|
if (isset($result['body']['details'])) {
|
||||||
|
echo "Détails: " . $result['body']['details'] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $blue . "=== Résumé des nouvelles règles ===" . $reset . "\n";
|
||||||
|
echo "✓ Minimum : 8 caractères UTF-8\n";
|
||||||
|
echo "✓ Maximum : 30 caractères UTF-8\n";
|
||||||
|
echo "✓ Tous caractères acceptés (lettres, chiffres, espaces, émojis, accents, etc.)\n";
|
||||||
|
echo "✓ Trim automatique des espaces début/fin\n";
|
||||||
|
echo "✓ Sensible à la casse (Jean ≠ jean)\n";
|
||||||
|
echo "✓ Unicité vérifiée dans toute la base\n\n";
|
||||||
|
|
||||||
|
echo $yellow . "Note: N'oubliez pas d'exécuter le script SQL de migration !" . $reset . "\n";
|
||||||
|
echo "scripts/sql/migration_username_utf8_support.sql\n";
|
||||||
@@ -1,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;
|
|
||||||
}
|
|
||||||
@@ -2,45 +2,48 @@
|
|||||||
// TypeAdapterGenerator
|
// TypeAdapterGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
class AudienceTargetModelAdapter extends TypeAdapter<AudienceTargetModel> {
|
class MessageAdapter extends TypeAdapter<Message> {
|
||||||
@override
|
@override
|
||||||
final int typeId = 24;
|
final int typeId = 51;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AudienceTargetModel read(BinaryReader reader) {
|
Message read(BinaryReader reader) {
|
||||||
final numOfFields = reader.readByte();
|
final numOfFields = reader.readByte();
|
||||||
final fields = <int, dynamic>{
|
final fields = <int, dynamic>{
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
};
|
};
|
||||||
return AudienceTargetModel(
|
return Message(
|
||||||
id: fields[0] as String,
|
id: fields[0] as String,
|
||||||
conversationId: fields[1] as String,
|
roomId: fields[1] as String,
|
||||||
targetType: fields[2] as String,
|
content: fields[2] as String,
|
||||||
targetId: fields[3] as String?,
|
senderId: fields[3] as int,
|
||||||
createdAt: fields[4] as DateTime,
|
senderName: fields[4] as String,
|
||||||
roleFilter: fields[5] as String?,
|
sentAt: fields[5] as DateTime,
|
||||||
entityFilter: fields[6] as String?,
|
isMe: fields[6] as bool,
|
||||||
|
isRead: fields[7] as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, AudienceTargetModel obj) {
|
void write(BinaryWriter writer, Message obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(7)
|
..writeByte(8)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.id)
|
..write(obj.id)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
..write(obj.conversationId)
|
..write(obj.roomId)
|
||||||
..writeByte(2)
|
..writeByte(2)
|
||||||
..write(obj.targetType)
|
..write(obj.content)
|
||||||
..writeByte(3)
|
..writeByte(3)
|
||||||
..write(obj.targetId)
|
..write(obj.senderId)
|
||||||
..writeByte(4)
|
..writeByte(4)
|
||||||
..write(obj.createdAt)
|
..write(obj.senderName)
|
||||||
..writeByte(5)
|
..writeByte(5)
|
||||||
..write(obj.roleFilter)
|
..write(obj.sentAt)
|
||||||
..writeByte(6)
|
..writeByte(6)
|
||||||
..write(obj.entityFilter);
|
..write(obj.isMe)
|
||||||
|
..writeByte(7)
|
||||||
|
..write(obj.isRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,7 +52,7 @@ class AudienceTargetModelAdapter extends TypeAdapter<AudienceTargetModel> {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is AudienceTargetModelAdapter &&
|
other is MessageAdapter &&
|
||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
typeId == other.typeId;
|
typeId == other.typeId;
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class NotificationSettingsAdapter extends TypeAdapter<NotificationSettings> {
|
|
||||||
@override
|
|
||||||
final int typeId = 25;
|
|
||||||
|
|
||||||
@override
|
|
||||||
NotificationSettings read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return NotificationSettings(
|
|
||||||
enableNotifications: fields[0] as bool,
|
|
||||||
soundEnabled: fields[1] as bool,
|
|
||||||
vibrationEnabled: fields[2] as bool,
|
|
||||||
mutedConversations: (fields[3] as List).cast<String>(),
|
|
||||||
showPreview: fields[4] as bool,
|
|
||||||
conversationNotifications: (fields[5] as Map).cast<String, bool>(),
|
|
||||||
doNotDisturb: fields[6] as bool,
|
|
||||||
doNotDisturbStart: fields[7] as DateTime?,
|
|
||||||
doNotDisturbEnd: fields[8] as DateTime?,
|
|
||||||
deviceToken: fields[9] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, NotificationSettings obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(10)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.enableNotifications)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.soundEnabled)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.vibrationEnabled)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.mutedConversations)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.showPreview)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.conversationNotifications)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.doNotDisturb)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.doNotDisturbStart)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.doNotDisturbEnd)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.deviceToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is NotificationSettingsAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class ParticipantModelAdapter extends TypeAdapter<ParticipantModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 22;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ParticipantModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return ParticipantModel(
|
|
||||||
id: fields[0] as String,
|
|
||||||
conversationId: fields[1] as String,
|
|
||||||
userId: fields[2] as String?,
|
|
||||||
anonymousId: fields[3] as String?,
|
|
||||||
role: fields[4] as String,
|
|
||||||
joinedAt: fields[5] as DateTime,
|
|
||||||
lastReadMessageId: fields[6] as String?,
|
|
||||||
viaTarget: fields[7] as bool,
|
|
||||||
canReply: fields[8] as bool?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, ParticipantModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(9)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.conversationId)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.userId)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.anonymousId)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.role)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.joinedAt)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.lastReadMessageId)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.viaTarget)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.canReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is ParticipantModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -2,45 +2,45 @@
|
|||||||
// TypeAdapterGenerator
|
// TypeAdapterGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
class AnonymousUserModelAdapter extends TypeAdapter<AnonymousUserModel> {
|
class RoomAdapter extends TypeAdapter<Room> {
|
||||||
@override
|
@override
|
||||||
final int typeId = 23;
|
final int typeId = 50;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AnonymousUserModel read(BinaryReader reader) {
|
Room read(BinaryReader reader) {
|
||||||
final numOfFields = reader.readByte();
|
final numOfFields = reader.readByte();
|
||||||
final fields = <int, dynamic>{
|
final fields = <int, dynamic>{
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
};
|
};
|
||||||
return AnonymousUserModel(
|
return Room(
|
||||||
id: fields[0] as String,
|
id: fields[0] as String,
|
||||||
deviceId: fields[1] as String,
|
title: fields[1] as String,
|
||||||
name: fields[2] as String?,
|
type: fields[2] as String,
|
||||||
email: fields[3] as String?,
|
createdAt: fields[3] as DateTime,
|
||||||
createdAt: fields[4] as DateTime,
|
lastMessage: fields[4] as String?,
|
||||||
convertedToUserId: fields[5] as String?,
|
lastMessageAt: fields[5] as DateTime?,
|
||||||
metadata: (fields[6] as Map?)?.cast<String, dynamic>(),
|
unreadCount: fields[6] as int,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, AnonymousUserModel obj) {
|
void write(BinaryWriter writer, Room obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(7)
|
..writeByte(7)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.id)
|
..write(obj.id)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
..write(obj.deviceId)
|
..write(obj.title)
|
||||||
..writeByte(2)
|
..writeByte(2)
|
||||||
..write(obj.name)
|
..write(obj.type)
|
||||||
..writeByte(3)
|
..writeByte(3)
|
||||||
..write(obj.email)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.createdAt)
|
..write(obj.createdAt)
|
||||||
|
..writeByte(4)
|
||||||
|
..write(obj.lastMessage)
|
||||||
..writeByte(5)
|
..writeByte(5)
|
||||||
..write(obj.convertedToUserId)
|
..write(obj.lastMessageAt)
|
||||||
..writeByte(6)
|
..writeByte(6)
|
||||||
..write(obj.metadata);
|
..write(obj.unreadCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,7 +49,7 @@ class AnonymousUserModelAdapter extends TypeAdapter<AnonymousUserModel> {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is AnonymousUserModelAdapter &&
|
other is RoomAdapter &&
|
||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
typeId == other.typeId;
|
typeId == other.typeId;
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import 'package:connectivity_plus/src/connectivity_plus_web.dart';
|
|||||||
import 'package:geolocator_web/geolocator_web.dart';
|
import 'package:geolocator_web/geolocator_web.dart';
|
||||||
import 'package:image_picker_for_web/image_picker_for_web.dart';
|
import 'package:image_picker_for_web/image_picker_for_web.dart';
|
||||||
import 'package:package_info_plus/src/package_info_plus_web.dart';
|
import 'package:package_info_plus/src/package_info_plus_web.dart';
|
||||||
|
import 'package:sensors_plus/src/sensors_plus_web.dart';
|
||||||
import 'package:shared_preferences_web/shared_preferences_web.dart';
|
import 'package:shared_preferences_web/shared_preferences_web.dart';
|
||||||
import 'package:url_launcher_web/url_launcher_web.dart';
|
import 'package:url_launcher_web/url_launcher_web.dart';
|
||||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||||
@@ -20,6 +21,7 @@ void registerPlugins([final Registrar? pluginRegistrar]) {
|
|||||||
GeolocatorPlugin.registerWith(registrar);
|
GeolocatorPlugin.registerWith(registrar);
|
||||||
ImagePickerPlugin.registerWith(registrar);
|
ImagePickerPlugin.registerWith(registrar);
|
||||||
PackageInfoPlusWebPlugin.registerWith(registrar);
|
PackageInfoPlusWebPlugin.registerWith(registrar);
|
||||||
|
WebSensorsPlugin.registerWith(registrar);
|
||||||
SharedPreferencesPlugin.registerWith(registrar);
|
SharedPreferencesPlugin.registerWith(registrar);
|
||||||
UrlLauncherPlugin.registerWith(registrar);
|
UrlLauncherPlugin.registerWith(registrar);
|
||||||
registrar.registerMessageHandler();
|
registrar.registerMessageHandler();
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":2,"entries":[{"package":"geosector_app","rootUri":"../","packageUri":"lib/"}]}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/pierre/dev/geosector/app/.dart_tool/flutter_build/9801dd92544a637fcb18c8ad3c09ddaa/dart_build_result.json:
|
||||||
@@ -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"]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"dependencies":[],"code_assets":[]}
|
||||||
@@ -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"]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"inputs":[],"outputs":[]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/pierre/dev/geosector/app/.dart_tool/flutter_build/9801dd92544a637fcb18c8ad3c09ddaa/native_assets.json:
|
||||||
@@ -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"]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"format-version":[1,0,0],"native-assets":{}}
|
||||||
@@ -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"]
|
||||||
@@ -369,27 +369,27 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/html_
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/permissions_manager.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/permissions_manager.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/web_settings.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/web_settings.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/go_router.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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/src/state.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/hive.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/hive.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/big_int_adapter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/big_int_adapter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/date_time_adapter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/date_time_adapter.dart
|
||||||
@@ -502,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/media_type.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/lib/image_picker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.2.0/lib/image_picker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/image_picker_for_web.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/image_picker_for_web.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/image_resizer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer_utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/image_resizer_utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/pkg_web_tweaks.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/pkg_web_tweaks.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/image_picker_platform_interface.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/image_picker_platform_interface.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.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/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
|
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/utm.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/sensors_plus.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/sensors_plus_web.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/web_sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/web_sensors_interop.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/sensors_plus_platform_interface.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/accelerometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/barometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/gyroscope_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/magnetometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/method_channel_sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/sensor_interval.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/user_accelerometer_event.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/shared_preferences.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/shared_preferences.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_async.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_async.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_devtools_extension_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_devtools_extension_data.dart
|
||||||
@@ -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/string_scanner.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/charts.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.5/lib/src/charts/axis/axis.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.5/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/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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_charts-30.2.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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_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/ascii_glyph_set.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
|
||||||
@@ -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/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.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math_64.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.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.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
|
||||||
@@ -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/process.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/proj_wkt.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/proj_wkt.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/wkt_parser.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/wkt_parser.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/charcodes.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/equality.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/error_listener.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/loader.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/null_span.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/parser.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/scanner.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/style.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/token.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/utils.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_document.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_exception.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_node.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_node_wrapper.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/yaml.dart
|
||||||
file:///home/pierre/dev/flutter/bin/cache/dart-sdk/lib/libraries.json
|
file:///home/pierre/dev/flutter/bin/cache/dart-sdk/lib/libraries.json
|
||||||
file:///home/pierre/dev/flutter/bin/cache/flutter_web_sdk/kernel/dart2js_platform.dill
|
file:///home/pierre/dev/flutter/bin/cache/flutter_web_sdk/kernel/dart2js_platform.dill
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/animation.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/animation.dart
|
||||||
@@ -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/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/web_plugin_registrant.dart
|
||||||
file:///home/pierre/dev/geosector/app/.dart_tool/package_config.json
|
file:///home/pierre/dev/geosector/app/.dart_tool/package_config.json
|
||||||
file:///home/pierre/dev/geosector/app/lib/app.dart
|
file:///home/pierre/dev/geosector/app/lib/app.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/anonymous_user_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/chat_module.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/anonymous_user_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/message.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/message.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/room.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/chat_adapters.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/room.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/chat_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page_embedded.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_config_loader.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_info_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/widgets/recipient_selector.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.g.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/widgets/chat_screen.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/widgets/conversations_list.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/constants/app_keys.dart
|
file:///home/pierre/dev/geosector/app/lib/core/constants/app_keys.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.g.dart
|
||||||
@@ -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_communication_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_home_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_home_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_page.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_field_mode_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_history_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_history_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_map_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_map_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_statistics_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_statistics_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_form.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_form.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_row_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_row_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_table_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_table_widget.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/badged_navigation_destination.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/activity_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/activity_chart.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/charts.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/charts.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
|
||||||
@@ -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_data.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_pie_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_pie_chart.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_summary_card.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_summary_card.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_input.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_messages.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_sidebar.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/connectivity_indicator.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/connectivity_indicator.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_button.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_button.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart
|
||||||
|
|||||||
@@ -369,27 +369,27 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/html_
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/permissions_manager.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/permissions_manager.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/web_settings.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/lib/web_settings.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/go_router.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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/src/state.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/hive.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/hive.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/big_int_adapter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/big_int_adapter.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/date_time_adapter.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3/lib/src/adapters/date_time_adapter.dart
|
||||||
@@ -502,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/media_type.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/lib/image_picker.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.2.0/lib/image_picker.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/image_picker_for_web.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/image_picker_for_web.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/image_resizer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer_utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/image_resizer_utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/pkg_web_tweaks.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/src/pkg_web_tweaks.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/image_picker_platform_interface.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/image_picker_platform_interface.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.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/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
|
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/utm.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/sensors_plus.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/sensors_plus_web.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/web_sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/lib/src/web_sensors_interop.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/sensors_plus_platform_interface.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/accelerometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/barometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/gyroscope_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/magnetometer_event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/method_channel_sensors.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/sensor_interval.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/sensors_plus_platform_interface-2.0.1/lib/src/user_accelerometer_event.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/shared_preferences.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/shared_preferences.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_async.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_async.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_devtools_extension_data.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/src/shared_preferences_devtools_extension_data.dart
|
||||||
@@ -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/string_scanner.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.5/lib/charts.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.5/lib/src/charts/axis/axis.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.5/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/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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_charts-30.2.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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.6/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_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/ascii_glyph_set.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
|
||||||
@@ -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/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.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math_64.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.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.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
|
||||||
@@ -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/process.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/proj_wkt.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/src/proj_wkt.dart
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/wkt_parser.dart
|
file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0/lib/wkt_parser.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/charcodes.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/equality.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/error_listener.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/event.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/loader.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/null_span.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/parser.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/scanner.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/style.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/token.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/utils.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_document.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_exception.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_node.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/src/yaml_node_wrapper.dart
|
||||||
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/yaml.dart
|
||||||
file:///home/pierre/dev/flutter/bin/cache/flutter_web_sdk/kernel/dart2js_platform.dill
|
file:///home/pierre/dev/flutter/bin/cache/flutter_web_sdk/kernel/dart2js_platform.dill
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/animation.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/animation.dart
|
||||||
file:///home/pierre/dev/flutter/packages/flutter/lib/cupertino.dart
|
file:///home/pierre/dev/flutter/packages/flutter/lib/cupertino.dart
|
||||||
@@ -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/main.dart
|
||||||
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/web_plugin_registrant.dart
|
file:///home/pierre/dev/geosector/app/.dart_tool/flutter_build/d35d2e27406b267ee35b6a1db0e24c05/web_plugin_registrant.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/app.dart
|
file:///home/pierre/dev/geosector/app/lib/app.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/anonymous_user_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/chat_module.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/anonymous_user_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/message.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/message.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/audience_target_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/room.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/chat_adapters.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/models/room.g.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/chat_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/conversation_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/pages/rooms_page_embedded.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/message_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_config_loader.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_info_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/notification_settings.g.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/services/chat_service.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.dart
|
file:///home/pierre/dev/geosector/app/lib/chat/widgets/recipient_selector.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/models/participant_model.g.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/widgets/chat_screen.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/chat/widgets/conversations_list.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/constants/app_keys.dart
|
file:///home/pierre/dev/geosector/app/lib/core/constants/app_keys.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.g.dart
|
file:///home/pierre/dev/geosector/app/lib/core/data/models/amicale_model.g.dart
|
||||||
@@ -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_communication_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_home_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_home_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_dashboard_page.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_field_mode_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_history_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_history_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_map_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_map_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_statistics_page.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/user/user_statistics_page.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_form.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_form.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_row_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_row_widget.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_table_widget.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/amicale_table_widget.dart
|
||||||
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/badged_navigation_destination.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/activity_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/activity_chart.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/charts.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/charts.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/combined_chart.dart
|
||||||
@@ -1941,9 +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_data.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_pie_chart.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_pie_chart.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_summary_card.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/charts/payment_summary_card.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_input.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_messages.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/chat/chat_sidebar.dart
|
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/connectivity_indicator.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/connectivity_indicator.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_button.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_button.dart
|
||||||
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart
|
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/custom_text_field.dart
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
@@ -10,6 +10,7 @@ import 'package:connectivity_plus/src/connectivity_plus_web.dart';
|
|||||||
import 'package:geolocator_web/geolocator_web.dart';
|
import 'package:geolocator_web/geolocator_web.dart';
|
||||||
import 'package:image_picker_for_web/image_picker_for_web.dart';
|
import 'package:image_picker_for_web/image_picker_for_web.dart';
|
||||||
import 'package:package_info_plus/src/package_info_plus_web.dart';
|
import 'package:package_info_plus/src/package_info_plus_web.dart';
|
||||||
|
import 'package:sensors_plus/src/sensors_plus_web.dart';
|
||||||
import 'package:shared_preferences_web/shared_preferences_web.dart';
|
import 'package:shared_preferences_web/shared_preferences_web.dart';
|
||||||
import 'package:url_launcher_web/url_launcher_web.dart';
|
import 'package:url_launcher_web/url_launcher_web.dart';
|
||||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||||
@@ -20,6 +21,7 @@ void registerPlugins([final Registrar? pluginRegistrar]) {
|
|||||||
GeolocatorPlugin.registerWith(registrar);
|
GeolocatorPlugin.registerWith(registrar);
|
||||||
ImagePickerPlugin.registerWith(registrar);
|
ImagePickerPlugin.registerWith(registrar);
|
||||||
PackageInfoPlusWebPlugin.registerWith(registrar);
|
PackageInfoPlusWebPlugin.registerWith(registrar);
|
||||||
|
WebSensorsPlugin.registerWith(registrar);
|
||||||
SharedPreferencesPlugin.registerWith(registrar);
|
SharedPreferencesPlugin.registerWith(registrar);
|
||||||
UrlLauncherPlugin.registerWith(registrar);
|
UrlLauncherPlugin.registerWith(registrar);
|
||||||
registrar.registerMessageHandler();
|
registrar.registerMessageHandler();
|
||||||
|
|||||||
@@ -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"]}
|
||||||
372
app/.dart_tool/flutter_build/dart_plugin_registrant.dart
Normal 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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -223,12 +223,6 @@
|
|||||||
"packageUri": "lib/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "2.12"
|
"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",
|
"name": "fake_async",
|
||||||
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/fake_async-1.3.3",
|
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/fake_async-1.3.3",
|
||||||
@@ -255,9 +249,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_selector_macos",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.6"
|
"languageVersion": "3.7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_selector_platform_interface",
|
"name": "file_selector_platform_interface",
|
||||||
@@ -351,9 +345,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "flutter_svg",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.6"
|
"languageVersion": "3.4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "flutter_test",
|
"name": "flutter_test",
|
||||||
@@ -429,9 +423,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "go_router",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.6"
|
"languageVersion": "3.7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "google_fonts",
|
"name": "google_fonts",
|
||||||
@@ -513,39 +507,39 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.3"
|
"languageVersion": "3.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_android",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.6"
|
"languageVersion": "3.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_for_web",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.4"
|
"languageVersion": "3.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_ios",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.4"
|
"languageVersion": "3.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_linux",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.4"
|
"languageVersion": "3.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_macos",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.4"
|
"languageVersion": "3.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_platform_interface",
|
"name": "image_picker_platform_interface",
|
||||||
@@ -555,9 +549,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_windows",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "2.19"
|
"languageVersion": "3.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "intl",
|
"name": "intl",
|
||||||
@@ -667,12 +661,6 @@
|
|||||||
"packageUri": "lib/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.2"
|
"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",
|
"name": "nm",
|
||||||
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/nm-0.5.0",
|
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/nm-0.5.0",
|
||||||
@@ -723,9 +711,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "path_provider_foundation",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.3"
|
"languageVersion": "3.7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "path_provider_linux",
|
"name": "path_provider_linux",
|
||||||
@@ -747,9 +735,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "petitparser",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.5"
|
"languageVersion": "3.8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "platform",
|
"name": "platform",
|
||||||
@@ -799,6 +787,18 @@
|
|||||||
"packageUri": "lib/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.0"
|
"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",
|
"name": "shared_preferences",
|
||||||
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3",
|
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3",
|
||||||
@@ -909,13 +909,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "syncfusion_flutter_charts",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.7"
|
"languageVersion": "3.7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "syncfusion_flutter_core",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.7"
|
"languageVersion": "3.7"
|
||||||
},
|
},
|
||||||
@@ -987,9 +987,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "url_launcher_ios",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.4"
|
"languageVersion": "3.7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "url_launcher_linux",
|
"name": "url_launcher_linux",
|
||||||
@@ -999,9 +999,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "url_launcher_macos",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.3"
|
"languageVersion": "3.7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "url_launcher_platform_interface",
|
"name": "url_launcher_platform_interface",
|
||||||
@@ -1035,15 +1035,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vector_graphics_codec",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.4"
|
"languageVersion": "2.17"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vector_graphics_compiler",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.6"
|
"languageVersion": "2.19"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vector_math",
|
"name": "vector_math",
|
||||||
@@ -1101,9 +1101,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "xml",
|
"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/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.2"
|
"languageVersion": "3.8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "yaml",
|
"name": "yaml",
|
||||||
|
|||||||
@@ -142,10 +142,6 @@ equatable
|
|||||||
2.12
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/equatable-2.0.7/lib/
|
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
|
fake_async
|
||||||
3.3
|
3.3
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/fake_async-1.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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/lib/
|
||||||
file_selector_macos
|
file_selector_macos
|
||||||
3.6
|
3.7
|
||||||
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+4/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+3/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+4/lib/
|
||||||
file_selector_platform_interface
|
file_selector_platform_interface
|
||||||
3.0
|
3.0
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_platform_interface-2.6.2/
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.29/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.29/lib/
|
||||||
flutter_svg
|
flutter_svg
|
||||||
3.6
|
3.4
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.2.0/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.0.13/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.2.0/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.0.13/lib/
|
||||||
frontend_server_client
|
frontend_server_client
|
||||||
3.0
|
3.0
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/glob-2.1.3/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/glob-2.1.3/lib/
|
||||||
go_router
|
go_router
|
||||||
3.6
|
3.7
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.1.0/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-16.2.0/lib/
|
||||||
google_fonts
|
google_fonts
|
||||||
2.14
|
2.14
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/google_fonts-6.3.0/
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image-4.5.4/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image-4.5.4/lib/
|
||||||
image_picker
|
image_picker
|
||||||
3.3
|
3.6
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.2.0/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.2.0/lib/
|
||||||
image_picker_android
|
image_picker_android
|
||||||
3.6
|
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.13/
|
||||||
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/lib/
|
||||||
image_picker_for_web
|
image_picker_for_web
|
||||||
3.4
|
3.6
|
||||||
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.1.0/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/lib/
|
||||||
image_picker_ios
|
image_picker_ios
|
||||||
3.4
|
3.6
|
||||||
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.13/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13/lib/
|
||||||
image_picker_linux
|
image_picker_linux
|
||||||
3.4
|
3.6
|
||||||
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.2/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/lib/
|
||||||
image_picker_macos
|
image_picker_macos
|
||||||
3.4
|
3.6
|
||||||
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.2/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2/lib/
|
||||||
image_picker_platform_interface
|
image_picker_platform_interface
|
||||||
3.6
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.11.0/lib/
|
||||||
image_picker_windows
|
image_picker_windows
|
||||||
2.19
|
3.6
|
||||||
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.2/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2/lib/
|
||||||
intl
|
intl
|
||||||
3.3
|
3.3
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/
|
||||||
@@ -422,10 +418,6 @@ mime
|
|||||||
3.2
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/
|
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
|
nm
|
||||||
2.12
|
2.12
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/nm-0.5.0/
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/lib/
|
||||||
path_provider_foundation
|
path_provider_foundation
|
||||||
3.3
|
3.7
|
||||||
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.2/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/lib/
|
||||||
path_provider_linux
|
path_provider_linux
|
||||||
2.19
|
2.19
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/lib/
|
||||||
petitparser
|
petitparser
|
||||||
3.5
|
3.8
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-6.1.0/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-7.0.1/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-6.1.0/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-7.0.1/lib/
|
||||||
platform
|
platform
|
||||||
3.2
|
3.2
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/platform-3.1.6/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/platform-3.1.6/
|
||||||
@@ -510,6 +502,14 @@ retry
|
|||||||
3.0
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/
|
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
|
shared_preferences
|
||||||
3.5
|
3.5
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/
|
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/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/
|
||||||
syncfusion_flutter_charts
|
syncfusion_flutter_charts
|
||||||
3.7
|
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.6/
|
||||||
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/lib/
|
||||||
syncfusion_flutter_core
|
syncfusion_flutter_core
|
||||||
3.7
|
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.6/
|
||||||
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/lib/
|
||||||
synchronized
|
synchronized
|
||||||
3.8
|
3.8
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/synchronized-3.4.0/
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.17/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.17/lib/
|
||||||
url_launcher_ios
|
url_launcher_ios
|
||||||
3.4
|
3.7
|
||||||
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.4/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.3/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.4/lib/
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
3.3
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/lib/
|
||||||
url_launcher_macos
|
url_launcher_macos
|
||||||
3.3
|
3.7
|
||||||
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.3/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.3/lib/
|
||||||
url_launcher_platform_interface
|
url_launcher_platform_interface
|
||||||
3.1
|
3.1
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_platform_interface-2.3.2/
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics-1.1.19/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics-1.1.19/lib/
|
||||||
vector_graphics_codec
|
vector_graphics_codec
|
||||||
3.4
|
2.17
|
||||||
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.11+1/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.13/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.11+1/lib/
|
||||||
vector_graphics_compiler
|
vector_graphics_compiler
|
||||||
3.6
|
2.19
|
||||||
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.11+1/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.17/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.11+1/lib/
|
||||||
vector_math
|
vector_math
|
||||||
2.14
|
2.14
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/
|
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/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0/lib/
|
||||||
xml
|
xml
|
||||||
3.2
|
3.8
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.5.0/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.6.1/
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.5.0/lib/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.6.1/lib/
|
||||||
yaml
|
yaml
|
||||||
3.4
|
3.4
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/
|
file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3/
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "geosector_app",
|
"name": "geosector_app",
|
||||||
"version": "3.0.8+308",
|
"version": "3.1.4+314",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"connectivity_plus",
|
"connectivity_plus",
|
||||||
"cupertino_icons",
|
"cupertino_icons",
|
||||||
@@ -26,15 +26,16 @@
|
|||||||
"image_picker",
|
"image_picker",
|
||||||
"intl",
|
"intl",
|
||||||
"latlong2",
|
"latlong2",
|
||||||
"mqtt5_client",
|
|
||||||
"package_info_plus",
|
"package_info_plus",
|
||||||
"path_provider",
|
"path_provider",
|
||||||
"retry",
|
"retry",
|
||||||
|
"sensors_plus",
|
||||||
"shared_preferences",
|
"shared_preferences",
|
||||||
"syncfusion_flutter_charts",
|
"syncfusion_flutter_charts",
|
||||||
"universal_html",
|
"universal_html",
|
||||||
"url_launcher",
|
"url_launcher",
|
||||||
"uuid"
|
"uuid",
|
||||||
|
"yaml"
|
||||||
],
|
],
|
||||||
"devDependencies": [
|
"devDependencies": [
|
||||||
"build_runner",
|
"build_runner",
|
||||||
@@ -104,17 +105,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker",
|
"name": "sensors_plus",
|
||||||
"version": "1.1.2",
|
"version": "6.1.2",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"flutter",
|
"flutter",
|
||||||
"image_picker_android",
|
"flutter_web_plugins",
|
||||||
"image_picker_for_web",
|
"sensors_plus_platform_interface"
|
||||||
"image_picker_ios",
|
|
||||||
"image_picker_linux",
|
|
||||||
"image_picker_macos",
|
|
||||||
"image_picker_platform_interface",
|
|
||||||
"image_picker_windows"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -390,6 +386,28 @@
|
|||||||
"meta"
|
"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",
|
"name": "universal_io",
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
@@ -411,6 +429,14 @@
|
|||||||
"package_info_plus"
|
"package_info_plus"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "geoclue",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"dependencies": [
|
||||||
|
"dbus",
|
||||||
|
"meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "equatable",
|
"name": "equatable",
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
@@ -425,11 +451,94 @@
|
|||||||
"dependencies": []
|
"dependencies": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "geoclue",
|
"name": "yaml",
|
||||||
"version": "0.1.1",
|
"version": "3.1.3",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"dbus",
|
"collection",
|
||||||
"meta"
|
"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"
|
"synchronized"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sprintf",
|
||||||
|
"version": "7.0.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "flutter_map_cache",
|
"name": "flutter_map_cache",
|
||||||
"version": "2.0.0+1",
|
"version": "2.0.0+1",
|
||||||
@@ -465,6 +579,16 @@
|
|||||||
"url_launcher_windows"
|
"url_launcher_windows"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"version": "4.5.1",
|
||||||
|
"dependencies": [
|
||||||
|
"crypto",
|
||||||
|
"fixnum",
|
||||||
|
"meta",
|
||||||
|
"sprintf"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "package_info_plus",
|
"name": "package_info_plus",
|
||||||
"version": "8.3.1",
|
"version": "8.3.1",
|
||||||
@@ -490,43 +614,11 @@
|
|||||||
"plugin_platform_interface"
|
"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",
|
"name": "cupertino_icons",
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "yaml",
|
|
||||||
"version": "3.1.3",
|
|
||||||
"dependencies": [
|
|
||||||
"collection",
|
|
||||||
"source_span",
|
|
||||||
"string_scanner"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "cli_util",
|
"name": "cli_util",
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
@@ -545,15 +637,17 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_windows",
|
"name": "plugin_platform_interface",
|
||||||
"version": "0.2.1+1",
|
"version": "2.1.8",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"file_selector_platform_interface",
|
"meta"
|
||||||
"file_selector_windows",
|
|
||||||
"flutter",
|
|
||||||
"image_picker_platform_interface"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "logging",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "typed_data",
|
"name": "typed_data",
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
@@ -570,21 +664,6 @@
|
|||||||
"uuid"
|
"uuid"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "uuid",
|
|
||||||
"version": "4.5.1",
|
|
||||||
"dependencies": [
|
|
||||||
"crypto",
|
|
||||||
"fixnum",
|
|
||||||
"meta",
|
|
||||||
"sprintf"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "sprintf",
|
|
||||||
"version": "7.0.0",
|
|
||||||
"dependencies": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "fixnum",
|
"name": "fixnum",
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -680,24 +759,6 @@
|
|||||||
"meta"
|
"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",
|
"name": "connectivity_plus",
|
||||||
"version": "6.1.5",
|
"version": "6.1.5",
|
||||||
@@ -728,46 +789,20 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "args",
|
"name": "go_router",
|
||||||
"version": "2.7.0",
|
"version": "16.2.0",
|
||||||
"dependencies": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "image_picker_platform_interface",
|
|
||||||
"version": "2.11.0",
|
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"cross_file",
|
"collection",
|
||||||
"flutter",
|
"flutter",
|
||||||
"http",
|
"flutter_web_plugins",
|
||||||
"plugin_platform_interface"
|
"logging",
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "plugin_platform_interface",
|
|
||||||
"version": "2.1.8",
|
|
||||||
"dependencies": [
|
|
||||||
"meta"
|
"meta"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_macos",
|
"name": "args",
|
||||||
"version": "0.2.1+2",
|
"version": "2.7.0",
|
||||||
"dependencies": [
|
"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": "csslib",
|
"name": "csslib",
|
||||||
@@ -794,14 +829,6 @@
|
|||||||
"web"
|
"web"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "url_launcher_platform_interface",
|
|
||||||
"version": "2.3.2",
|
|
||||||
"dependencies": [
|
|
||||||
"flutter",
|
|
||||||
"plugin_platform_interface"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -817,6 +844,14 @@
|
|||||||
"web"
|
"web"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "url_launcher_platform_interface",
|
||||||
|
"version": "2.3.2",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"plugin_platform_interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "path_provider_windows",
|
"name": "path_provider_windows",
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
@@ -837,11 +872,21 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "path_provider_foundation",
|
"name": "syncfusion_flutter_charts",
|
||||||
"version": "2.4.1",
|
"version": "30.2.6",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"flutter",
|
"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"
|
"xml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mime",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "dio_cache_interceptor",
|
"name": "dio_cache_interceptor",
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
@@ -906,19 +956,19 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "url_launcher_macos",
|
"name": "url_launcher_linux",
|
||||||
"version": "3.2.2",
|
"version": "3.2.1",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"flutter",
|
"flutter",
|
||||||
"url_launcher_platform_interface"
|
"url_launcher_platform_interface"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "url_launcher_linux",
|
"name": "path_provider_foundation",
|
||||||
"version": "3.2.1",
|
"version": "2.4.2",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"flutter",
|
"flutter",
|
||||||
"url_launcher_platform_interface"
|
"path_provider_platform_interface"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -937,6 +987,14 @@
|
|||||||
"url_launcher_platform_interface"
|
"url_launcher_platform_interface"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "url_launcher_macos",
|
||||||
|
"version": "3.2.3",
|
||||||
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"url_launcher_platform_interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "flutter_local_notifications",
|
"name": "flutter_local_notifications",
|
||||||
"version": "19.4.0",
|
"version": "19.4.0",
|
||||||
@@ -980,15 +1038,6 @@
|
|||||||
"xdg_directories"
|
"xdg_directories"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "xml",
|
|
||||||
"version": "6.5.0",
|
|
||||||
"dependencies": [
|
|
||||||
"collection",
|
|
||||||
"meta",
|
|
||||||
"petitparser"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "timezone",
|
"name": "timezone",
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
@@ -1002,14 +1051,59 @@
|
|||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "xml",
|
||||||
|
"version": "6.6.1",
|
||||||
|
"dependencies": [
|
||||||
|
"collection",
|
||||||
|
"meta",
|
||||||
|
"petitparser"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "petitparser",
|
"name": "petitparser",
|
||||||
"version": "6.1.0",
|
"version": "7.0.1",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"collection",
|
"collection",
|
||||||
"meta"
|
"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",
|
"name": "geolocator_apple",
|
||||||
"version": "2.3.13",
|
"version": "2.3.13",
|
||||||
@@ -1018,22 +1112,6 @@
|
|||||||
"geolocator_platform_interface"
|
"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",
|
"name": "file_selector_linux",
|
||||||
"version": "0.9.3+2",
|
"version": "0.9.3+2",
|
||||||
@@ -1063,6 +1141,14 @@
|
|||||||
"typed_data"
|
"typed_data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "xdg_directories",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"dependencies": [
|
||||||
|
"meta",
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "crypto",
|
"name": "crypto",
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
@@ -1071,9 +1157,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "logging",
|
"name": "url_launcher_ios",
|
||||||
"version": "1.3.0",
|
"version": "6.3.4",
|
||||||
"dependencies": []
|
"dependencies": [
|
||||||
|
"flutter",
|
||||||
|
"url_launcher_platform_interface"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shared_preferences",
|
"name": "shared_preferences",
|
||||||
@@ -1166,45 +1255,25 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "image_picker_for_web",
|
"name": "source_helper",
|
||||||
"version": "3.0.6",
|
"version": "1.3.5",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"flutter",
|
"analyzer",
|
||||||
"flutter_web_plugins",
|
"collection",
|
||||||
"image_picker_platform_interface",
|
"source_gen"
|
||||||
"mime",
|
|
||||||
"web"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mime",
|
"name": "glob",
|
||||||
"version": "2.0.0",
|
"version": "2.1.3",
|
||||||
"dependencies": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mqtt5_client",
|
|
||||||
"version": "4.14.0",
|
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"characters",
|
"async",
|
||||||
"crypto",
|
"collection",
|
||||||
"event_bus",
|
"file",
|
||||||
"meta",
|
|
||||||
"path",
|
"path",
|
||||||
"typed_data",
|
"string_scanner"
|
||||||
"universal_html",
|
|
||||||
"web"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "event_bus",
|
|
||||||
"version": "2.0.1",
|
|
||||||
"dependencies": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "platform",
|
|
||||||
"version": "3.1.6",
|
|
||||||
"dependencies": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "file",
|
"name": "file",
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
@@ -1214,31 +1283,17 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "url_launcher_web",
|
"name": "platform",
|
||||||
"version": "2.4.1",
|
"version": "3.1.6",
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vector_graphics",
|
||||||
|
"version": "1.1.19",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"flutter",
|
"flutter",
|
||||||
"flutter_web_plugins",
|
"http",
|
||||||
"url_launcher_platform_interface",
|
"vector_graphics_codec"
|
||||||
"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"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1256,14 +1311,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "glob",
|
"name": "url_launcher_web",
|
||||||
"version": "2.1.3",
|
"version": "2.4.1",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"async",
|
"flutter",
|
||||||
"collection",
|
"flutter_web_plugins",
|
||||||
"file",
|
"url_launcher_platform_interface",
|
||||||
"path",
|
"web"
|
||||||
"string_scanner"
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "file_selector_macos",
|
||||||
|
"version": "0.9.4+4",
|
||||||
|
"dependencies": [
|
||||||
|
"cross_file",
|
||||||
|
"file_selector_platform_interface",
|
||||||
|
"flutter"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1273,51 +1336,6 @@
|
|||||||
"meta"
|
"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",
|
"name": "path_provider_android",
|
||||||
"version": "2.2.17",
|
"version": "2.2.17",
|
||||||
@@ -1713,21 +1731,6 @@
|
|||||||
"stream_channel"
|
"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",
|
"name": "file_selector_windows",
|
||||||
"version": "0.9.3+4",
|
"version": "0.9.3+4",
|
||||||
@@ -1745,6 +1748,13 @@
|
|||||||
"web"
|
"web"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "win32",
|
||||||
|
"version": "5.14.0",
|
||||||
|
"dependencies": [
|
||||||
|
"ffi"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "url_launcher_android",
|
"name": "url_launcher_android",
|
||||||
"version": "6.3.17",
|
"version": "6.3.17",
|
||||||
@@ -1753,15 +1763,6 @@
|
|||||||
"url_launcher_platform_interface"
|
"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",
|
"name": "flutter_plugin_android_lifecycle",
|
||||||
"version": "2.0.29",
|
"version": "2.0.29",
|
||||||
|
|||||||
339
app/ANDROID-GUIDE.md
Normal 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*
|
||||||
@@ -1106,11 +1106,97 @@ Cette architecture garantit une gestion des membres robuste, sécurisée et intu
|
|||||||
|
|
||||||
## 🗺️ Cartes et géolocalisation
|
## 🗺️ Cartes et géolocalisation
|
||||||
|
|
||||||
Flutter Map : Rendu cartographique haute performance
|
### 🎯 Architecture cartographique
|
||||||
Tuiles Mapbox : Cartographie détaillée et personnalisable
|
|
||||||
Géolocalisation temps réel : Suivi GPS des équipes
|
**Flutter Map** : Rendu cartographique haute performance
|
||||||
Secteurs géographiques : Visualisation et attribution dynamique
|
**Providers de tuiles** : Solution hybride Mapbox/OpenStreetMap
|
||||||
Passages géolocalisés : Enregistrement précis des distributions
|
**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é
|
## 🔄 Synchronisation et réactivité
|
||||||
|
|
||||||
|
|||||||
617
app/TODO-APP.md
@@ -1,194 +1,7 @@
|
|||||||
# TODO-APP.md
|
## TODO-APP.md
|
||||||
|
|
||||||
## 📋 Liste des tâches à effectuer sur l'application GEOSECTOR
|
## 📋 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
|
## 🧪 Tests unitaires Flutter à implémenter
|
||||||
|
|
||||||
### 📁 Structure des tests
|
### 📁 Structure des tests
|
||||||
@@ -197,95 +10,108 @@ Le dossier `/test` doit contenir des tests unitaires pour valider le comportemen
|
|||||||
|
|
||||||
### 📝 Tests prioritaires à créer
|
### 📝 Tests prioritaires à créer
|
||||||
|
|
||||||
#### 1. **Tests des Repositories** (`test/repositories/`)
|
#### 1\. **Tests des Repositories** (`test/repositories/`)
|
||||||
|
|
||||||
##### `test/repositories/membre_repository_test.dart`
|
`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/user_repository_test.dart`
|
- Test de création d'un membre avec succès
|
||||||
- [ ] Test de connexion réussie
|
- Test de création avec email déjà existant (409)
|
||||||
- [ ] Test de connexion avec mauvais identifiants
|
- Test de mise à jour d'un membre
|
||||||
- [ ] Test de mise à jour du profil utilisateur
|
- Test de suppression d'un membre
|
||||||
- [ ] Test de synchronisation des données
|
- Test de gestion des erreurs réseau
|
||||||
- [ ] Test de gestion de session
|
- Test du cache Hive local
|
||||||
|
|
||||||
##### `test/repositories/operation_repository_test.dart`
|
`test/repositories/user_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
|
|
||||||
|
|
||||||
#### 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/repositories/operation_repository_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/services/logger_service_test.dart`
|
- Test de création d'opération
|
||||||
- [ ] Test de désactivation des logs en PROD
|
- Test d'activation/désactivation
|
||||||
- [ ] Test des différents niveaux de log
|
- Test de récupération des opérations par amicale
|
||||||
- [ ] Test du formatage des messages
|
- Test de suppression avec transfert de passages
|
||||||
|
|
||||||
##### `test/services/hive_service_test.dart`
|
#### 2\. **Tests des Services** (`test/services/`)
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
#### 3. **Tests des Models** (`test/models/`)
|
`test/services/api_service_test.dart`
|
||||||
|
|
||||||
##### `test/models/membre_model_test.dart`
|
- Test de détection d'environnement (DEV/REC/PROD)
|
||||||
- [ ] Test de sérialisation JSON
|
- Test de gestion des sessions
|
||||||
- [ ] Test de désérialisation JSON
|
- Test de conversion DioException vers ApiException
|
||||||
- [ ] Test de conversion vers UserModel
|
- Test des différents codes d'erreur HTTP
|
||||||
- [ ] Test des valeurs par défaut
|
- Test du retry automatique
|
||||||
|
|
||||||
##### `test/models/amicale_model_test.dart`
|
`test/services/logger_service_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 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/services/hive_service_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'initialisation des boîtes Hive
|
||||||
- [ ] Test d'affichage responsive (mobile vs web)
|
- Test de sauvegarde et récupération
|
||||||
- [ ] Test de masquage des colonnes sur mobile
|
- Test de suppression de données
|
||||||
- [ ] Test des icônes de statut
|
- Test de migration de schéma
|
||||||
- [ ] Test du tri et filtrage
|
|
||||||
|
|
||||||
#### 5. **Tests d'intégration** (`test/integration/`)
|
#### 3\. **Tests des Models** (`test/models/`)
|
||||||
|
|
||||||
##### `test/integration/auth_flow_test.dart`
|
`test/models/membre_model_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 sérialisation JSON
|
||||||
- [ ] Test de création complète d'un membre
|
- Test de désérialisation JSON
|
||||||
- [ ] Test de modification avec validation
|
- Test de conversion vers UserModel
|
||||||
- [ ] Test de suppression avec transfert
|
- Test des valeurs par défaut
|
||||||
- [ ] Test de gestion des erreurs
|
|
||||||
|
`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
|
### 🛠️ Configuration des tests
|
||||||
|
|
||||||
#### Fichier `test/test_helpers.dart`
|
#### Fichier `test/test_helpers.dart`
|
||||||
```dart
|
|
||||||
|
```plaintext
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -293,7 +119,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||||||
|
|
||||||
// Mocks nécessaires
|
// Mocks nécessaires
|
||||||
class MockDio extends Mock implements Dio {}
|
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 {}
|
class MockApiService extends Mock implements ApiService {}
|
||||||
|
|
||||||
// Helper pour initialiser Hive en tests
|
// Helper pour initialiser Hive en tests
|
||||||
@@ -332,7 +158,7 @@ class TestDataFactory {
|
|||||||
|
|
||||||
### 📋 Commandes de test
|
### 📋 Commandes de test
|
||||||
|
|
||||||
```bash
|
```plaintext
|
||||||
# Lancer tous les tests
|
# Lancer tous les tests
|
||||||
flutter test
|
flutter test
|
||||||
|
|
||||||
@@ -349,16 +175,17 @@ open coverage/html/index.html
|
|||||||
|
|
||||||
### 🎯 Objectifs de couverture
|
### 🎯 Objectifs de couverture
|
||||||
|
|
||||||
- [ ] **Repositories** : 80% minimum
|
- **Repositories** : 80% minimum
|
||||||
- [ ] **Services** : 90% minimum
|
- **Services** : 90% minimum
|
||||||
- [ ] **Models** : 95% minimum
|
- **Models** : 95% minimum
|
||||||
- [ ] **Widgets critiques** : 70% minimum
|
- **Widgets critiques** : 70% minimum
|
||||||
- [ ] **Couverture globale** : 75% minimum
|
- **Couverture globale** : 75% minimum
|
||||||
|
|
||||||
### 📚 Dépendances de test à ajouter
|
### 📚 Dépendances de test à ajouter
|
||||||
|
|
||||||
Dans `pubspec.yaml` :
|
Dans `pubspec.yaml` :
|
||||||
```yaml
|
|
||||||
|
```plaintext
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
@@ -372,7 +199,8 @@ dev_dependencies:
|
|||||||
### 🔄 Intégration CI/CD
|
### 🔄 Intégration CI/CD
|
||||||
|
|
||||||
Ajouter dans le pipeline CI :
|
Ajouter dans le pipeline CI :
|
||||||
```yaml
|
|
||||||
|
```plaintext
|
||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
@@ -391,9 +219,7 @@ test:
|
|||||||
3. **Nommage** : Utiliser des noms descriptifs (test_should_xxx_when_yyy)
|
3. **Nommage** : Utiliser des noms descriptifs (test_should_xxx_when_yyy)
|
||||||
4. **AAA** : Suivre le pattern Arrange-Act-Assert
|
4. **AAA** : Suivre le pattern Arrange-Act-Assert
|
||||||
5. **Edge cases** : Tester les cas limites et erreurs
|
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
|
## 💬 Module Chat en ligne GEOSECTOR
|
||||||
|
|
||||||
@@ -403,7 +229,7 @@ Le module chat est partiellement implémenté avec une architecture MQTT pour le
|
|||||||
|
|
||||||
### 🏗️ Architecture existante
|
### 🏗️ Architecture existante
|
||||||
|
|
||||||
```
|
```plaintext
|
||||||
lib/chat/
|
lib/chat/
|
||||||
├── models/ # ✅ Modèles créés avec Hive
|
├── models/ # ✅ Modèles créés avec Hive
|
||||||
│ ├── conversation_model.dart # Conversations (one-to-one, groupe, annonce)
|
│ ├── conversation_model.dart # Conversations (one-to-one, groupe, annonce)
|
||||||
@@ -429,60 +255,67 @@ lib/chat/
|
|||||||
|
|
||||||
### 📝 Tâches de développement Chat
|
### 📝 Tâches de développement Chat
|
||||||
|
|
||||||
#### 1. **Finalisation des modèles Hive**
|
#### 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
|
|
||||||
|
|
||||||
#### 2. **Repository ChatRepository**
|
- Compléter les adaptateurs Hive pour tous les modèles
|
||||||
- [ ] Implémenter `createConversation()` avec participants
|
- Ajouter la gestion des pièces jointes (images, documents)
|
||||||
- [ ] Implémenter `sendMessage()` avec queue hors ligne
|
- Implémenter le modèle `AnonymousUserModel` pour les utilisateurs temporaires
|
||||||
- [ ] Implémenter `getConversations()` avec pagination
|
- Ajouter les timestamps et statuts de lecture
|
||||||
- [ ] Implémenter `getMessages()` avec lazy loading
|
|
||||||
- [ ] Ajouter la gestion des participants (ajout/suppression)
|
|
||||||
- [ ] Implémenter les annonces ciblées par groupe
|
|
||||||
|
|
||||||
#### 3. **Services**
|
#### 2\. **Repository ChatRepository**
|
||||||
- [ ] 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**
|
- Implémenter `createConversation()` avec participants
|
||||||
- [ ] Installer et configurer Mosquitto dans le container Incus
|
- Implémenter `sendMessage()` avec queue hors ligne
|
||||||
- [ ] Configurer SSL/TLS pour MQTT (port 8883)
|
- Implémenter `getConversations()` avec pagination
|
||||||
- [ ] Implémenter l'authentification par token JWT
|
- Implémenter `getMessages()` avec lazy loading
|
||||||
- [ ] Créer les topics par utilisateur/groupe/conversation
|
- Ajouter la gestion des participants (ajout/suppression)
|
||||||
- [ ] Implémenter le système de présence (online/offline/typing)
|
- Implémenter les annonces ciblées par groupe
|
||||||
- [ ] Ajouter les ACLs pour sécuriser les topics
|
|
||||||
|
|
||||||
#### 5. **Interface utilisateur**
|
#### 3\. **Services**
|
||||||
- [ ] 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**
|
- Compléter `ChatApiService` avec endpoints REST
|
||||||
- [ ] Notifications push locales via `flutter_local_notifications`
|
- Implémenter la synchronisation bidirectionnelle
|
||||||
- [ ] Chiffrement end-to-end des messages sensibles
|
- Configurer `OfflineQueueService` pour messages en attente
|
||||||
- [ ] Réactions aux messages (emojis)
|
- Implémenter le retry automatique avec exponential backoff
|
||||||
- [ ] Messages éphémères avec auto-suppression
|
- Ajouter la compression des images avant envoi
|
||||||
- [ ] Partage de localisation en temps réel
|
|
||||||
- [ ] Appels audio/vidéo via WebRTC (phase 2)
|
#### 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
|
### 🔧 Configuration backend requise
|
||||||
|
|
||||||
#### Base de données
|
#### Base de données
|
||||||
```sql
|
|
||||||
|
```plaintext
|
||||||
-- Tables à créer (voir chat_tables.sql)
|
-- Tables à créer (voir chat_tables.sql)
|
||||||
CREATE TABLE chat_conversations (
|
CREATE TABLE chat_conversations (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
@@ -512,7 +345,8 @@ CREATE TABLE chat_participants (
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Configuration MQTT
|
#### Configuration MQTT
|
||||||
```bash
|
|
||||||
|
```plaintext
|
||||||
# Installation Mosquitto
|
# Installation Mosquitto
|
||||||
apt-get install mosquitto mosquitto-clients
|
apt-get install mosquitto mosquitto-clients
|
||||||
|
|
||||||
@@ -528,7 +362,7 @@ password_file /etc/mosquitto/passwd
|
|||||||
|
|
||||||
### 📦 Dépendances à ajouter
|
### 📦 Dépendances à ajouter
|
||||||
|
|
||||||
```yaml
|
```plaintext
|
||||||
dependencies:
|
dependencies:
|
||||||
# MQTT et notifications
|
# MQTT et notifications
|
||||||
mqtt5_client: ^4.0.0
|
mqtt5_client: ^4.0.0
|
||||||
@@ -548,13 +382,12 @@ dependencies:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 🧪 Tests à implémenter
|
### 🧪 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
|
## 💳 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
|
### 📝 Tâches de développement Stripe
|
||||||
|
|
||||||
#### 1. **Configuration initiale 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
|
|
||||||
|
|
||||||
#### 2. **Onboarding des amicales**
|
- Créer un compte Stripe Connect Platform
|
||||||
- [ ] Créer un workflow d'inscription Stripe pour les amicales
|
- Configurer les webhooks Stripe
|
||||||
- [ ] Implémenter Stripe Connect Onboarding
|
- Mettre en place l'environnement de test (sandbox)
|
||||||
- [ ] Gérer le KYC (Know Your Customer) requis par Stripe
|
- Configurer les clés API (publishable/secret)
|
||||||
- [ ] Stocker de manière sécurisée le `stripe_account_id`
|
- Implémenter la gestion sécurisée des clés
|
||||||
- [ ] Créer une page de statut du compte Stripe
|
|
||||||
- [ ] Gérer les documents requis (RIB, statuts, etc.)
|
|
||||||
|
|
||||||
#### 3. **Modèles de données**
|
#### 2\. **Onboarding des amicales**
|
||||||
- [ ] 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**
|
- Créer un workflow d'inscription Stripe pour les amicales
|
||||||
```dart
|
- 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 {
|
class StripeService {
|
||||||
// Compte Connect
|
// Compte Connect
|
||||||
Future<String> createConnectAccount(AmicaleModel amicale);
|
Future<string> createConnectAccount(AmicaleModel amicale);
|
||||||
Future<void> updateAccountStatus(String accountId);
|
Future<void> updateAccountStatus(String accountId);
|
||||||
Future<String> createAccountLink(String accountId);
|
Future<string> createAccountLink(String accountId);
|
||||||
|
|
||||||
// Paiements
|
// Paiements
|
||||||
Future<PaymentIntent> createPaymentIntent({
|
Future<paymentintent> createPaymentIntent({
|
||||||
required double amount,
|
required double amount,
|
||||||
required String currency,
|
required String currency,
|
||||||
required String connectedAccountId,
|
required String connectedAccountId,
|
||||||
@@ -616,17 +453,19 @@ class StripeService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5. **Interface de paiement dans PassageForm**
|
#### 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
|
|
||||||
|
|
||||||
#### 6. **Widget StripePaymentSheet**
|
- Détecter si l'amicale accepte Stripe (`chk_stripe`)
|
||||||
```dart
|
- 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 {
|
class StripePaymentSheet extends StatefulWidget {
|
||||||
final double amount;
|
final double amount;
|
||||||
final String currency;
|
final String currency;
|
||||||
@@ -643,15 +482,17 @@ class StripePaymentSheet extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 7. **Tableau de bord financier**
|
#### 7\. **Tableau de bord financier**
|
||||||
- [ ] Page de suivi des transactions Stripe
|
|
||||||
- [ ] Graphiques des paiements par période
|
- Page de suivi des transactions Stripe
|
||||||
- [ ] Export des transactions (CSV/Excel)
|
- Graphiques des paiements par période
|
||||||
- [ ] Calcul automatique des commissions
|
- Export des transactions (CSV/Excel)
|
||||||
- [ ] Rapprochement bancaire
|
- Calcul automatique des commissions
|
||||||
- [ ] Dashboard temps réel des paiements
|
- Rapprochement bancaire
|
||||||
|
- Dashboard temps réel des paiements
|
||||||
|
|
||||||
|
#### 8\. **Webhooks Stripe**
|
||||||
|
|
||||||
#### 8. **Webhooks Stripe**
|
|
||||||
```php
|
```php
|
||||||
// Backend PHP pour gérer les webhooks
|
// Backend PHP pour gérer les webhooks
|
||||||
class StripeWebhookHandler {
|
class StripeWebhookHandler {
|
||||||
@@ -664,26 +505,29 @@ class StripeWebhookHandler {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 9. **Sécurité**
|
#### 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
|
|
||||||
|
|
||||||
#### 10. **Tests et conformité**
|
- Chiffrement des données sensibles
|
||||||
- [ ] Tests avec cartes de test Stripe
|
- Validation PCI DSS
|
||||||
- [ ] Tests des cas d'erreur (carte refusée, etc.)
|
- Audit trail des transactions
|
||||||
- [ ] Tests 3D Secure
|
- Détection de fraude
|
||||||
- [ ] Tests de performance
|
- Rate limiting sur les API
|
||||||
- [ ] Conformité RGPD pour les données de paiement
|
- Tokenisation des cartes
|
||||||
- [ ] Documentation utilisateur
|
|
||||||
|
#### 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
|
### 🔧 Configuration backend requise
|
||||||
|
|
||||||
#### Tables base de données
|
#### Tables base de données
|
||||||
```sql
|
|
||||||
|
```plaintext
|
||||||
CREATE TABLE stripe_accounts (
|
CREATE TABLE stripe_accounts (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
fk_entite INT NOT NULL,
|
fk_entite INT NOT NULL,
|
||||||
@@ -708,7 +552,8 @@ CREATE TABLE stripe_transactions (
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Variables d'environnement
|
#### Variables d'environnement
|
||||||
```env
|
|
||||||
|
```plaintext
|
||||||
STRIPE_PUBLISHABLE_KEY=pk_test_xxx
|
STRIPE_PUBLISHABLE_KEY=pk_test_xxx
|
||||||
STRIPE_SECRET_KEY=sk_test_xxx
|
STRIPE_SECRET_KEY=sk_test_xxx
|
||||||
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
||||||
@@ -717,7 +562,7 @@ STRIPE_APPLICATION_FEE_PERCENT=1.4
|
|||||||
|
|
||||||
### 📦 Dépendances Stripe
|
### 📦 Dépendances Stripe
|
||||||
|
|
||||||
```yaml
|
```plaintext
|
||||||
dependencies:
|
dependencies:
|
||||||
# Stripe
|
# Stripe
|
||||||
flutter_stripe: ^10.1.1
|
flutter_stripe: ^10.1.1
|
||||||
@@ -733,21 +578,25 @@ dependencies:
|
|||||||
### 🚀 Roadmap d'implémentation
|
### 🚀 Roadmap d'implémentation
|
||||||
|
|
||||||
**Phase 1 (2 semaines)**
|
**Phase 1 (2 semaines)**
|
||||||
|
|
||||||
- Configuration Stripe Connect
|
- Configuration Stripe Connect
|
||||||
- Onboarding basique des amicales
|
- Onboarding basique des amicales
|
||||||
- Tests en sandbox
|
- Tests en sandbox
|
||||||
|
|
||||||
**Phase 2 (3 semaines)**
|
**Phase 2 (3 semaines)**
|
||||||
|
|
||||||
- Intégration dans PassageForm
|
- Intégration dans PassageForm
|
||||||
- Gestion des paiements simples
|
- Gestion des paiements simples
|
||||||
- Webhooks essentiels
|
- Webhooks essentiels
|
||||||
|
|
||||||
**Phase 3 (2 semaines)**
|
**Phase 3 (2 semaines)**
|
||||||
|
|
||||||
- Dashboard financier
|
- Dashboard financier
|
||||||
- Export et rapports
|
- Export et rapports
|
||||||
- Tests complets
|
- Tests complets
|
||||||
|
|
||||||
**Phase 4 (1 semaine)**
|
**Phase 4 (1 semaine)**
|
||||||
|
|
||||||
- Mise en production
|
- Mise en production
|
||||||
- Monitoring
|
- Monitoring
|
||||||
- Documentation
|
- Documentation
|
||||||
@@ -759,8 +608,6 @@ dependencies:
|
|||||||
- **Commission application** : 1.4% (reversée à GEOSECTOR)
|
- **Commission application** : 1.4% (reversée à GEOSECTOR)
|
||||||
- **Coût net pour l'amicale** : ~2.8% + 0.25€ par transaction
|
- **Coût net pour l'amicale** : ~2.8% + 0.25€ par transaction
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Date de création** : 2025-08-07
|
**Date de création** : 2025-08-07
|
||||||
**Auteur** : Architecture Team
|
**Auteur** : Architecture Team
|
||||||
**Version** : 1.2.0
|
**Version** : 1.2.0\</string,>\</string,>
|
||||||
|
|||||||
@@ -54,12 +54,24 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
// Optimisations sans ProGuard pour éviter les problèmes
|
||||||
|
isMinifyEnabled = false
|
||||||
|
isShrinkResources = false
|
||||||
|
|
||||||
|
// Configuration de signature
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
} else {
|
} else {
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug {
|
||||||
|
// Mode debug pour le développement
|
||||||
|
isDebuggable = true
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
versionNameSuffix = "-DEBUG"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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 -->
|
<!-- Permissions pour la géolocalisation -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_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
|
<application
|
||||||
android:label="geosector_app"
|
android:label="GeoSector"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/launcher_icon">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 79 KiB |
@@ -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>
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 26 KiB |
4
app/android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -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.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
# Optimisations de build
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.configureondemand=true
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
# Configuration générale
|
# Configuration générale
|
||||||
image_path: "assets/images/icon-geosector.svg"
|
image_path: "assets/images/logo-geosector-1024.png"
|
||||||
image_path_android: "assets/images/icon-geosector.svg"
|
image_path_android: "assets/images/logo-geosector-1024.png"
|
||||||
image_path_ios: "assets/images/icon-geosector.svg"
|
image_path_ios: "assets/images/logo-geosector-1024.png"
|
||||||
|
|
||||||
# Configuration Android
|
# Configuration Android
|
||||||
android: true
|
android: true
|
||||||
min_sdk_android: 21
|
min_sdk_android: 21
|
||||||
adaptive_icon_background: "#FFFFFF"
|
adaptive_icon_background: "#FFFFFF"
|
||||||
adaptive_icon_foreground: "assets/images/icon-geosector.svg"
|
adaptive_icon_foreground: "assets/images/logo-geosector-512.png"
|
||||||
|
|
||||||
# Configuration iOS
|
# Configuration iOS
|
||||||
ios: true
|
ios: true
|
||||||
|
|||||||
@@ -471,7 +471,7 @@
|
|||||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
@@ -566,7 +566,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -624,7 +624,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -680,7 +680,7 @@
|
|||||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
@@ -727,7 +727,7 @@
|
|||||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
|
|||||||
@@ -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":"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"}}
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 2.2 KiB |