# Prompt Flutter - Intégration de la Résolution d'École Sécurisée

## Contexte

Nous avons implémenté une architecture sécurisée pour la résolution d'école et la configuration de l'application. L'objectif est de permettre aux utilisateurs de saisir un code école (ex: E00015) et de récupérer uniquement les informations de cette école spécifique, sans exposer la liste complète des écoles.

## Architecture Implémentée

### Endpoints Sécurisés

1. **GET** `/api/schools/resolve?identifier={CODE_ECOLE}`
   - Public (pas d'authentification)
   - Validation stricte du format E00000
   - Rate limiting : 10 req/min par IP
   - Retourne uniquement l'école demandée

2. **GET** `/api/app/config?schoolIdentifier={CODE_ECOLE}`
   - Public (pas d'authentification)
   - Configuration par défaut + overrides école
   - ETag pour cache efficace
   - Rate limiting : 20 req/min par IP

### Sécurité
- Validation stricte du format du code école
- Rate limiting par IP
- Sanitisation HTML pour prévenir XSS
- Aucune information sensible exposée
- Logs d'audit complets

## Implémentation Flutter

### 1. Service de Résolution d'École

```dart
class SchoolResolutionService {
  static const String baseUrl = 'https://your-backend.com';
  
  /// Résout un code école vers ses informations
  static Future<SchoolInfo?> resolveSchool(String identifier) async {
    try {
      // Validation du format côté client
      if (!_isValidSchoolCode(identifier)) {
        throw SchoolResolutionException('Format de code école invalide');
      }
      
      final response = await http.get(
        Uri.parse('$baseUrl/api/schools/resolve?identifier=$identifier'),
        headers: {
          'Accept': 'application/json',
          'User-Agent': 'CorossolApp/1.0',
        },
      );
      
      if (response.statusCode == 200) {
        final data = json.decode(response.body);
        return SchoolInfo.fromJson(data['data']);
      } else if (response.statusCode == 400) {
        final error = json.decode(response.body);
        throw SchoolResolutionException(error['message']);
      } else if (response.statusCode == 404) {
        throw SchoolNotFoundException('École non trouvée');
      } else if (response.statusCode == 429) {
        final retryAfter = int.tryParse(response.headers['retry-after'] ?? '60');
        throw RateLimitException('Trop de requêtes', retryAfter);
      } else {
        throw SchoolResolutionException('Erreur serveur');
      }
    } catch (e) {
      if (e is SchoolResolutionException) rethrow;
      throw SchoolResolutionException('Erreur de connexion: $e');
    }
  }
  
  /// Valide le format du code école
  static bool _isValidSchoolCode(String code) {
    return RegExp(r'^E\d{5}$').hasMatch(code);
  }
}
```

### 2. Service de Configuration d'Application

```dart
class AppConfigService {
  static const String baseUrl = 'https://your-backend.com';
  
  /// Récupère la configuration de l'app pour une école
  static Future<AppConfig?> getAppConfig(String schoolIdentifier, {String? etag}) async {
    try {
      final headers = {
        'Accept': 'application/json',
        'User-Agent': 'CorossolApp/1.0',
      };
      
      if (etag != null) {
        headers['If-None-Match'] = etag;
      }
      
      final response = await http.get(
        Uri.parse('$baseUrl/api/app/config?schoolIdentifier=$schoolIdentifier'),
        headers: headers,
      );
      
      if (response.statusCode == 200) {
        final data = json.decode(response.body);
        final newEtag = response.headers['etag'];
        
        // Mettre en cache avec le nouvel ETag
        await _cacheConfig(schoolIdentifier, data, newEtag);
        
        return AppConfig.fromJson(data['data']);
      } else if (response.statusCode == 304) {
        // Configuration non modifiée, utiliser le cache
        return await _getCachedConfig(schoolIdentifier);
      } else if (response.statusCode == 400) {
        final error = json.decode(response.body);
        throw AppConfigException(error['message']);
      } else if (response.statusCode == 404) {
        throw SchoolNotFoundException('École non trouvée');
      } else {
        throw AppConfigException('Erreur serveur');
      }
    } catch (e) {
      if (e is AppConfigException) rethrow;
      throw AppConfigException('Erreur de connexion: $e');
    }
  }
  
  /// Cache la configuration avec ETag
  static Future<void> _cacheConfig(String schoolId, Map<String, dynamic> config, String? etag) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('config_$schoolId', json.encode(config));
    if (etag != null) {
      await prefs.setString('etag_$schoolId', etag);
    }
  }
  
  /// Récupère la configuration en cache
  static Future<AppConfig?> _getCachedConfig(String schoolId) async {
    final prefs = await SharedPreferences.getInstance();
    final configJson = prefs.getString('config_$schoolId');
    if (configJson != null) {
      final data = json.decode(configJson);
      return AppConfig.fromJson(data);
    }
    return null;
  }
}
```

### 3. Modèles de Données

```dart
class SchoolInfo {
  final String identifier;
  final String name;
  final String apiBaseUrl;
  final String? logoUrl;
  final String? whatsapp;
  
  SchoolInfo({
    required this.identifier,
    required this.name,
    required this.apiBaseUrl,
    this.logoUrl,
    this.whatsapp,
  });
  
  factory SchoolInfo.fromJson(Map<String, dynamic> json) {
    return SchoolInfo(
      identifier: json['identifier'],
      name: json['name'],
      apiBaseUrl: json['apiBaseUrl'],
      logoUrl: json['logoUrl'],
      whatsapp: json['whatsapp'],
    );
  }
}

class AppConfig {
  final List<SliderItem> sliders;
  final SupportInfo support;
  final ProfileInfo profile;
  final PaymentInfo payment;
  final Map<String, dynamic> i18nOverrides;
  final SchoolInfo school;
  
  AppConfig({
    required this.sliders,
    required this.support,
    required this.profile,
    required this.payment,
    required this.i18nOverrides,
    required this.school,
  });
  
  factory AppConfig.fromJson(Map<String, dynamic> json) {
    return AppConfig(
      sliders: (json['sliders'] as List?)
          ?.map((e) => SliderItem.fromJson(e))
          .toList() ?? [],
      support: SupportInfo.fromJson(json['support'] ?? {}),
      profile: ProfileInfo.fromJson(json['profile'] ?? {}),
      payment: PaymentInfo.fromJson(json['payment'] ?? {}),
      i18nOverrides: json['i18nOverrides'] ?? {},
      school: SchoolInfo.fromJson(json['school'] ?? {}),
    );
  }
}
```

### 4. Écran de Sélection d'École

```dart
class SchoolSelectionScreen extends StatefulWidget {
  @override
  _SchoolSelectionScreenState createState() => _SchoolSelectionScreenState();
}

class _SchoolSelectionScreenState extends State<SchoolSelectionScreen> {
  final _formKey = GlobalKey<FormState>();
  final _schoolCodeController = TextEditingController();
  bool _isLoading = false;
  String? _errorMessage;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Sélection d\'École')),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _schoolCodeController,
                decoration: InputDecoration(
                  labelText: 'Code École',
                  hintText: 'Ex: E00015',
                  prefixIcon: Icon(Icons.school),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Veuillez saisir le code école';
                  }
                  if (!RegExp(r'^E\d{5}$').hasMatch(value)) {
                    return 'Format invalide: E suivi de 5 chiffres';
                  }
                  return null;
                },
                textCapitalize: TextCapitalize.characters,
                maxLength: 6,
              ),
              SizedBox(height: 16),
              if (_errorMessage != null)
                Text(
                  _errorMessage!,
                  style: TextStyle(color: Colors.red),
                ),
              SizedBox(height: 16),
              ElevatedButton(
                onPressed: _isLoading ? null : _resolveSchool,
                child: _isLoading
                    ? CircularProgressIndicator()
                    : Text('Continuer'),
              ),
            ],
          ),
        ),
      ),
    );
  }
  
  Future<void> _resolveSchool() async {
    if (!_formKey.currentState!.validate()) return;
    
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });
    
    try {
      // Résoudre l'école
      final schoolInfo = await SchoolResolutionService.resolveSchool(
        _schoolCodeController.text.trim(),
      );
      
      if (schoolInfo != null) {
        // Récupérer la configuration de l'app
        final appConfig = await AppConfigService.getAppConfig(schoolInfo.identifier);
        
        // Naviguer vers l'écran principal avec les données
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(
            builder: (context) => MainScreen(
              schoolInfo: schoolInfo,
              appConfig: appConfig,
            ),
          ),
        );
      }
    } on SchoolNotFoundException {
      setState(() {
        _errorMessage = 'Code école invalide ou école non trouvée';
      });
    } on RateLimitException catch (e) {
      setState(() {
        _errorMessage = 'Trop de tentatives. Réessayez dans ${e.retryAfter} secondes.';
      });
    } on SchoolResolutionException catch (e) {
      setState(() {
        _errorMessage = e.message;
      });
    } catch (e) {
      setState(() {
        _errorMessage = 'Erreur inattendue: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }
}
```

### 5. Gestion des Erreurs et Retry

```dart
class RetryService {
  static Future<T> retryWithBackoff<T>({
    required Future<T> Function() operation,
    int maxRetries = 3,
    Duration initialDelay = const Duration(seconds: 1),
  }) async {
    int retryCount = 0;
    Duration delay = initialDelay;
    
    while (true) {
      try {
        return await operation();
      } catch (e) {
        retryCount++;
        
        if (retryCount > maxRetries) {
          rethrow;
        }
        
        if (e is RateLimitException) {
          // Attendre le temps spécifié par le serveur
          await Future.delayed(Duration(seconds: e.retryAfter));
        } else {
          // Attendre avec backoff exponentiel
          await Future.delayed(delay);
          delay *= 2;
        }
      }
    }
  }
}

// Utilisation
final schoolInfo = await RetryService.retryWithBackoff(
  operation: () => SchoolResolutionService.resolveSchool(schoolCode),
  maxRetries: 3,
);
```

### 6. Cache et Persistance

```dart
class AppCache {
  static const String _schoolInfoKey = 'school_info';
  static const String _appConfigKey = 'app_config';
  static const String _lastUpdateKey = 'last_update';
  
  /// Sauvegarde les informations d'école
  static Future<void> saveSchoolInfo(SchoolInfo schoolInfo) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_schoolInfoKey, json.encode(schoolInfo.toJson()));
    await prefs.setString(_lastUpdateKey, DateTime.now().toIso8601String());
  }
  
  /// Récupère les informations d'école en cache
  static Future<SchoolInfo?> getSchoolInfo() async {
    final prefs = await SharedPreferences.getInstance();
    final schoolJson = prefs.getString(_schoolInfoKey);
    if (schoolJson != null) {
      return SchoolInfo.fromJson(json.decode(schoolJson));
    }
    return null;
  }
  
  /// Vérifie si le cache est expiré
  static Future<bool> isCacheExpired({Duration maxAge = const Duration(hours: 6)}) async {
    final prefs = await SharedPreferences.getInstance();
    final lastUpdate = prefs.getString(_lastUpdateKey);
    if (lastUpdate != null) {
      final lastUpdateTime = DateTime.parse(lastUpdate);
      return DateTime.now().difference(lastUpdateTime) > maxAge;
    }
    return true;
  }
  
  /// Nettoie le cache
  static Future<void> clearCache() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_schoolInfoKey);
    await prefs.remove(_appConfigKey);
    await prefs.remove(_lastUpdateKey);
  }
}
```

## Tests Flutter

### Test de Validation

```dart
void main() {
  group('School Code Validation', () {
    test('should validate correct school codes', () {
      expect(SchoolResolutionService._isValidSchoolCode('E00001'), true);
      expect(SchoolResolutionService._isValidSchoolCode('E99999'), true);
      expect(SchoolResolutionService._isValidSchoolCode('E12345'), true);
    });
    
    test('should reject invalid school codes', () {
      expect(SchoolResolutionService._isValidSchoolCode('E1234'), false);
      expect(SchoolResolutionService._isValidSchoolCode('E123456'), false);
      expect(SchoolResolutionService._isValidSchoolCode('A12345'), false);
      expect(SchoolResolutionService._isValidSchoolCode('12345'), false);
      expect(SchoolResolutionService._isValidSchoolCode('E12ABC'), false);
    });
  });
}
```

## Intégration dans l'App

### 1. Initialisation au Démarrage

```dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Vérifier le cache au démarrage
  final cachedSchool = await AppCache.getSchoolInfo();
  if (cachedSchool != null && !await AppCache.isCacheExpired()) {
    // Utiliser l'école en cache
    runApp(MyApp(initialSchool: cachedSchool));
  } else {
    // Afficher l'écran de sélection d'école
    runApp(MyApp());
  }
}
```

### 2. Gestion des Mises à Jour

```dart
class AppUpdateService {
  static Future<void> checkForUpdates() async {
    final schoolInfo = await AppCache.getSchoolInfo();
    if (schoolInfo != null) {
      try {
        final newConfig = await AppConfigService.getAppConfig(
          schoolInfo.identifier,
          etag: await _getStoredEtag(schoolInfo.identifier),
        );
        
        if (newConfig != null) {
          // Mettre à jour l'interface utilisateur
          _notifyConfigUpdate(newConfig);
        }
      } catch (e) {
        print('Erreur lors de la vérification des mises à jour: $e');
      }
    }
  }
}
```

## Points d'Attention

1. **Validation Côté Client** : Toujours valider le format E00000 avant l'envoi
2. **Gestion des Erreurs** : Implémenter une gestion robuste des erreurs réseau
3. **Rate Limiting** : Respecter les limites et implémenter un retry intelligent
4. **Cache** : Utiliser l'ETag pour éviter les téléchargements inutiles
5. **Sécurité** : Ne jamais stocker d'informations sensibles localement
6. **UX** : Afficher des messages d'erreur clairs et des indicateurs de chargement

## Déploiement

1. Tester avec des codes école valides et invalides
2. Vérifier la gestion du rate limiting
3. Tester la persistance et la récupération du cache
4. Valider la gestion des erreurs réseau
5. Tester la mise à jour automatique des configurations

Cette implémentation garantit une expérience utilisateur fluide tout en maintenant un niveau de sécurité élevé et en respectant les bonnes pratiques de développement mobile.
