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:
@@ -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(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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é
|
||||
|
||||
Reference in New Issue
Block a user