feat: Implémentation authentification NIST SP 800-63B v3.0.8

- Ajout du service PasswordSecurityService conforme NIST SP 800-63B
- Vérification des mots de passe contre la base Have I Been Pwned
- Validation : minimum 8 caractères, maximum 64 caractères
- Pas d'exigences de composition obligatoires (conforme NIST)
- Intégration dans LoginController et UserController
- Génération de mots de passe sécurisés non compromis

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-15 15:31:23 +02:00
parent 0e98a94374
commit 268444d6e8
44 changed files with 77937 additions and 75359 deletions

View File

@@ -258,88 +258,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
// Afficher le contenu seulement si les données sont chargées ou en cours de mise à jour
if (isDataLoaded || isLoading) ...[
// Cartes de synthèse
isDesktop
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: _buildSummaryCard(
context,
'Passages totaux',
totalPassages.toString(),
Icons.map_outlined,
AppTheme.primaryColor,
),
),
const SizedBox(width: AppTheme.spacingM),
Expanded(
flex: 2,
child: _buildSummaryCard(
context,
'Montant collecté',
'${totalAmounts.toStringAsFixed(2)}',
Icons.euro_outlined,
AppTheme.buttonSuccessColor,
),
),
const SizedBox(width: AppTheme.spacingM),
Expanded(
flex: 3,
child: SectorDistributionCard(
key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
height: 200,
),
),
],
)
: Column(
children: [
_buildSummaryCard(
context,
'Passages totaux',
totalPassages.toString(),
Icons.map_outlined,
AppTheme.primaryColor,
),
const SizedBox(height: AppTheme.spacingM),
_buildSummaryCard(
context,
'Montant collecté',
'${totalAmounts.toStringAsFixed(2)}',
Icons.euro_outlined,
AppTheme.buttonSuccessColor,
),
const SizedBox(height: AppTheme.spacingM),
SectorDistributionCard(
key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
height: 200,
),
],
),
const SizedBox(height: AppTheme.spacingL),
// Graphique d'activité
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
child: ActivityChart(
key: ValueKey('activity_chart_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
height: 350,
showAllPassages: true, // Tous les passages, pas seulement ceux de l'utilisateur courant
title: 'Passages réalisés par jour (15 derniers jours)',
daysToShow: 15,
),
),
const SizedBox(height: AppTheme.spacingL),
// Graphiques de répartition
// LIGNE 1 : Graphiques de répartition (type de passage et mode de paiement)
isDesktop
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -363,6 +282,33 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
const SizedBox(height: AppTheme.spacingL),
// LIGNE 2 : Carte de répartition par secteur (pleine largeur)
SectorDistributionCard(
key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
title: 'Répartition sur les 31 secteurs',
height: 500, // Hauteur maximale pour afficher tous les secteurs
),
const SizedBox(height: AppTheme.spacingL),
// LIGNE 3 : Graphique d'activité
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
child: ActivityChart(
key: ValueKey('activity_chart_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
height: 350,
showAllPassages: true, // Tous les passages, pas seulement ceux de l'utilisateur courant
title: 'Passages réalisés par jour (15 derniers jours)',
daysToShow: 15,
),
),
const SizedBox(height: AppTheme.spacingL),
// Actions rapides - uniquement visible sur le web
if (kIsWeb) ...[
Container(
@@ -415,61 +361,6 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
]);
}
Widget _buildSummaryCard(
BuildContext context,
String label,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(AppTheme.spacingM),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppTheme.borderRadiusSmall),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(width: AppTheme.spacingM),
Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
const SizedBox(height: AppTheme.spacingM),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24,
color: color,
),
),
],
),
);
}
// Construit la carte de répartition par type de passage avec liste
Widget _buildPassageTypeCard(BuildContext context) {
return PassageSummaryCard(

View File

@@ -1,5 +1,6 @@
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
@@ -51,6 +52,9 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
// Référence à la boîte Hive pour les paramètres
late Box _settingsBox;
// Listener pour les changements de paramètres
late ValueListenable<Box<dynamic>> _settingsListenable;
// Liste des éléments de navigation de base (toujours visibles)
final List<_NavigationItem> _baseNavigationItems = [
@@ -226,7 +230,11 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
// Les pages seront construites dynamiquement dans build()
// Initialiser et charger les paramètres
_initSettings();
_initSettings().then((_) {
// Écouter les changements de la boîte de paramètres après l'initialisation
_settingsListenable = _settingsBox.listenable(keys: ['adminSelectedPageIndex']);
_settingsListenable.addListener(_onSettingsChanged);
});
// Vérifier si des données sont en cours de chargement
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -241,6 +249,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
void dispose() {
WidgetsBinding.instance.removeObserver(this);
userRepository.removeListener(_handleUserRepositoryChanges);
_settingsListenable.removeListener(_onSettingsChanged);
super.dispose();
}
@@ -248,6 +257,16 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
void _handleUserRepositoryChanges() {
_checkLoadingState();
}
// Méthode pour gérer les changements de paramètres
void _onSettingsChanged() {
final newIndex = _settingsBox.get('adminSelectedPageIndex');
if (newIndex != null && newIndex is int && newIndex != _selectedIndex) {
setState(() {
_selectedIndex = newIndex;
});
}
}
// Méthode pour vérifier l'état de chargement (barre de progression désactivée)
void _checkLoadingState() {

View File

@@ -1,5 +1,6 @@
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
@@ -84,6 +85,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
super.initState();
// Initialiser les filtres
_initializeFilters();
// Charger les filtres présélectionnés depuis Hive si disponibles
_loadPreselectedFilters();
}
@override
@@ -172,6 +175,40 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
selectedDateRange = null;
}
// Charger les filtres présélectionnés depuis Hive
void _loadPreselectedFilters() {
try {
// Utiliser Hive directement sans async
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
// Charger le secteur présélectionné
final int? preselectedSectorId = settingsBox.get('history_selectedSectorId');
final String? preselectedSectorName = settingsBox.get('history_selectedSectorName');
final int? preselectedTypeId = settingsBox.get('history_selectedTypeId');
if (preselectedSectorId != null && preselectedSectorName != null) {
selectedSectorId = preselectedSectorId;
selectedSector = preselectedSectorName;
debugPrint('Secteur présélectionné: $preselectedSectorName (ID: $preselectedSectorId)');
}
if (preselectedTypeId != null) {
selectedType = preselectedTypeId.toString();
debugPrint('Type de passage présélectionné: $preselectedTypeId');
}
// Nettoyer les valeurs après utilisation pour ne pas les réutiliser la prochaine fois
settingsBox.delete('history_selectedSectorId');
settingsBox.delete('history_selectedSectorName');
settingsBox.delete('history_selectedTypeId');
}
} catch (e) {
debugPrint('Erreur lors du chargement des filtres présélectionnés: $e');
}
}
@override
void dispose() {
_searchController.dispose();

View File

@@ -85,6 +85,9 @@ class _AdminMapPageState extends State<AdminMapPage> {
// Référence à la boîte Hive pour les paramètres
late Box _settingsBox;
// Listener pour les changements de paramètres
late ValueListenable<Box<dynamic>> _settingsListenable;
@override
void initState() {
@@ -92,6 +95,11 @@ class _AdminMapPageState extends State<AdminMapPage> {
_initSettings().then((_) {
_loadSectors();
_loadPassages();
// Écouter les changements du secteur sélectionné
_settingsListenable = _settingsBox.listenable(keys: ['admin_selectedSectorId']);
_settingsListenable.addListener(_onSectorSelectionChanged);
// Centrer la carte une seule fois après le chargement initial
Future.delayed(const Duration(milliseconds: 100), () {
if (_selectedSectorId != null && _sectors.any((s) => s['id'] == _selectedSectorId)) {
@@ -128,7 +136,34 @@ class _AdminMapPageState extends State<AdminMapPage> {
_currentZoom = savedZoom;
}
}
// Méthode pour gérer les changements de sélection de secteur
void _onSectorSelectionChanged() {
final newSectorId = _settingsBox.get('admin_selectedSectorId');
if (newSectorId != null && newSectorId != _selectedSectorId) {
setState(() {
_selectedSectorId = newSectorId;
});
// Recharger les passages pour le nouveau secteur
_loadPassages();
// Attendre que le build soit fait puis centrer sur le secteur
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_sectors.any((s) => s['id'] == _selectedSectorId)) {
_centerMapOnSpecificSector(_selectedSectorId!);
}
});
}
}
@override
void dispose() {
_settingsListenable.removeListener(_onSectorSelectionChanged);
_mapController.dispose();
super.dispose();
}
// Sauvegarder les paramètres utilisateur
void _saveSettings() {
// Sauvegarder le secteur sélectionné