From 218ad8499135d1de07a175cd834ad9e41be30210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20M=C3=A9ndez?= Date: Sat, 23 May 2026 08:32:45 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20agregar=20clase=20PasswordHasher=20para?= =?UTF-8?q?=20el=20hash=20de=20contrase=C3=B1as?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 +- lib/core/security/password_hasher.dart | 13 ++ .../screens/home_screen_placeholder.dart | 160 +++++++++++++++++- 3 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 lib/core/security/password_hasher.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index 2fff7ef..81b52e7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ ], "yaml.schemas": { "https://raw.githubusercontent.com/doanthuanthanh88/testapi6/main/schema.json": "*.yaml" - } + }, + "cmake.sourceDirectory": "C:/Users/Mendez/Documents/Practicas de programacion/Proyecto-Hackathon (ITC)/hackathon-sfc-a9a4cee23109413188ee35ceac01dc07/windows" } \ No newline at end of file diff --git a/lib/core/security/password_hasher.dart b/lib/core/security/password_hasher.dart new file mode 100644 index 0000000..76b4ebb --- /dev/null +++ b/lib/core/security/password_hasher.dart @@ -0,0 +1,13 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + +class PasswordHasher { + static const String _salt = 'waste-notify-local-salt-v1'; + + static String hash(String password) { + final normalized = password.trim(); + final bytes = utf8.encode('$_salt:$normalized'); + return sha256.convert(bytes).toString(); + } +} \ No newline at end of file diff --git a/lib/features/auth/presentation/screens/home_screen_placeholder.dart b/lib/features/auth/presentation/screens/home_screen_placeholder.dart index 1e62f87..7dfce03 100644 --- a/lib/features/auth/presentation/screens/home_screen_placeholder.dart +++ b/lib/features/auth/presentation/screens/home_screen_placeholder.dart @@ -148,7 +148,7 @@ class _HomeScreenPlaceholderState extends State { }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("🔔 Push: \${newAlert.pushPayload.title}"), + content: Text('🔔 Push: ${newAlert.pushPayload.title}'), backgroundColor: pId == 4 ? Colors.amber.shade900 : (pId == 8 ? Colors.blue.shade700 : Colors.green.shade700), duration: const Duration(seconds: 2), ), @@ -230,9 +230,157 @@ class _HomeScreenPlaceholderState extends State { const SizedBox(height: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(12)),child: const Text('Ciudadano', style: TextStyle(color: Colors.white, fontSize: 12)),)],)],),),const SizedBox(height: 16),// --- 2. RELOJ DE TIEMPO ESTIMADO INTERACTIVO ---Card(color: Colors.white,shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),elevation: 1,child: Padding(padding: const EdgeInsets.all(20.0),child: Column(children: [Row(children: const [Icon(Icons.access_time, color: Color(0xFF2E7D32)),SizedBox(width: 10),Text('Tiempo estimado de llegada', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),],),const SizedBox(height: 16),Container(width: 110,height: 110,decoration: BoxDecoration(shape: BoxShape.circle,border: Border.all(color: const Color(0xFFE0E0E0), width: 3),),child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Text('${currentPositionData["mins"]}',style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.black),),Text('— min',style: TextStyle(fontSize: 14, color: Colors.grey.shade600),)],),),const SizedBox(height: 12),Text('Estado GPS: ${currentPositionData["desc"]}',textAlign: TextAlign.center,style: TextStyle(fontSize: 13, color: Colors.grey.shade600, fontStyle: FontStyle.italic),),],),),),const SizedBox(height: 16),// --- 3. BOTÓN DE SIMULACIÓN PARA EL MVP ---ElevatedButton.icon(style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF2E7D32),padding: const EdgeInsets.symmetric(vertical: 14),shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),),onPressed: _activePositionIndex < (telemetry["positions"] as List).length - 1 ? _nextSimulationStep : null,icon: const Icon(Icons.play_arrow, color: Colors.white),label: const Text('Simular Avance del Camión (GPS)', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),),const SizedBox(height: 16),// --- 4. BANDEJA DE ALERTAS REALES RECIBIDAS (PARSED) ---if (_pushNotificationsLog.isNotEmpty) ...[const Text('🔔 Alertas Push en Vivo', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.black)),const SizedBox(height: 8),..._pushNotificationsLog.map((log) {final bool isWarning = log.triggerEvent == "TRUCK_PROXIMITY";final bool isDone = log.triggerEvent == "ROUTE_COMPLETED";final Color cardColor = isWarning ? Colors.amber.shade900 : (isDone ? Colors.blue.shade700 : Colors.green.shade700);return Card(margin: const EdgeInsets.symmetric(vertical: 4),shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),child: ListTile(leading: CircleAvatar(backgroundColor: cardColor.withOpacity(0.12),child: Icon(isWarning ? Icons.notification_important : (isDone ? Icons.check_circle : Icons.local_shipping),color: cardColor)),title: Text(log.pushPayload.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),subtitle: Text(log.pushPayload.body, style: const TextStyle(fontSize: 12)),),);}),const SizedBox(height: 16),],// --- 5. PANEL DE RECOMENDACIONES ---Container(padding: const EdgeInsets.all(16),decoration: BoxDecoration(color: const Color(0xFFFFF3E0),borderRadius: BorderRadius.circular(16),border: Border.all(color: const Color(0xFFFFE0B2)),),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Row(children: const [Icon(Icons.campaign, color: Colors.orange, size: 22),SizedBox(width: 8),Text('Recuerda siempre', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.brown, fontSize: 14)),],),const SizedBox(height: 12),_buildBulletRow(Icons.delete_outline, 'Saca la basura SOLO cuando recibas la alerta de "próxima llegada".'),const SizedBox(height: 8),_buildBulletRow(Icons.block, 'Nunca persigas ni te acerques al camión. El sistema te avisará a tiempo.'),const SizedBox(height: 8),_buildBulletRow(Icons.eco_outlined, 'Separar tus residuos hace más eficiente la recolección. ¡Gracias!'),],),),const SizedBox(height: 20),],),),);}Widget _buildBulletRow(IconData icon, String text) {return Row(crossAxisAlignment: CrossAxisAlignment.start,children: [Icon(icon, size: 16, color: Colors.brown.shade700),const SizedBox(width: 10),Expanded(child: Text(text, style: TextStyle(fontSize: 13, color: Colors.brown.shade900, height: 1.2))),],);}}// =========================================================================// 📍 MODELO 1: Catálogo de Colonias (Renombrado para evitar duplicados)// =========================================================================class ColoniaModel {String colonia;String routeId;String horarioEstimado;ColoniaModel({required this.colonia,required this.routeId,required this.horarioEstimado,});factory ColoniaModel.fromJson(Map json) => ColoniaModel(colonia: json["colonia"],routeId: json["routeId"],horarioEstimado: json["horarioEstimado"],);Map toJson() => {"colonia": colonia,"routeId": routeId,"horarioEstimado": horarioEstimado,};}// =========================================================================// 📍 MODELO 2: Sistema de Notificaciones Alertas Push// =========================================================================class NotificationModel {String triggerEvent;String condition;PushPayloadModel pushPayload;NotificationModel({required this.triggerEvent,required this.condition,required this.pushPayload,});factory NotificationModel.fromJson(Map json) => NotificationModel(triggerEvent: json["triggerEvent"],condition: json["condition"],pushPayload: PushPayloadModel.fromJson(json["pushPayload"]),);Map toJson() => {"triggerEvent": triggerEvent,"condition": condition,"pushPayload": pushPayload.toJson(),};}class PushPayloadModel {String title;String body;PushPayloadModel({required this.title,required this.body,});factory PushPayloadModel.fromJson(Map json) => PushPayloadModel(title: json["title"],body: json["body"],);Map toJson() => {"title": title,"body": body,};} - ]) + decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(12)), + child: const Text('Ciudadano', style: TextStyle(color: Colors.white, fontSize: 12)), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + + // --- 2. RELOJ DE TIEMPO ESTIMADO INTERACTIVO --- + Card( + color: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + Row( + children: const [ + Icon(Icons.access_time, color: Color(0xFF2E7D32)), + SizedBox(width: 10), + Text('Tiempo estimado de llegada', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)), + ], + ), + const SizedBox(height: 16), + Container( + width: 110, + height: 110, + decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: const Color(0xFFE0E0E0), width: 3)), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('${currentPositionData["mins"]}', style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.black)), + Text('— min', style: TextStyle(fontSize: 14, color: Colors.grey.shade600)), + ], + ), + ), + const SizedBox(height: 12), + Text('Estado GPS: ${currentPositionData["desc"]}', textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: Colors.grey.shade600, fontStyle: FontStyle.italic)), + ], + ), + ), + ), + const SizedBox(height: 16), + + // --- 3. BOTÓN DE SIMULACIÓN PARA EL MVP --- + ElevatedButton.icon( + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF2E7D32), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14))), + onPressed: _activePositionIndex < (telemetry['positions'] as List).length - 1 ? _nextSimulationStep : null, + icon: const Icon(Icons.play_arrow, color: Colors.white), + label: const Text('Simular Avance del Camión (GPS)', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 16), + + // --- 4. BANDEJA DE ALERTAS REALES RECIBIDAS (PARSED) --- + if (_pushNotificationsLog.isNotEmpty) ...[ + const Text('🔔 Alertas Push en Vivo', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.black)), + const SizedBox(height: 8), + ..._pushNotificationsLog.map((log) { + final bool isWarning = log.triggerEvent == 'TRUCK_PROXIMITY'; + final bool isDone = log.triggerEvent == 'ROUTE_COMPLETED'; + final Color cardColor = isWarning ? Colors.amber.shade900 : (isDone ? Colors.blue.shade700 : Colors.green.shade700); + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListTile( + leading: CircleAvatar(backgroundColor: cardColor.withOpacity(0.12), child: Icon(isWarning ? Icons.notification_important : (isDone ? Icons.check_circle : Icons.local_shipping), color: cardColor)), + title: Text(log.pushPayload.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), + subtitle: Text(log.pushPayload.body, style: const TextStyle(fontSize: 12)), + ), + ); + }).toList(), + const SizedBox(height: 16), + ], + + // --- 5. PANEL DE RECOMENDACIONES --- + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFE0B2))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: const [Icon(Icons.campaign, color: Colors.orange, size: 22), SizedBox(width: 8), Text('Recuerda siempre', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.brown, fontSize: 14))]), + const SizedBox(height: 12), + _buildBulletRow(Icons.delete_outline, 'Saca la basura SOLO cuando recibas la alerta de "próxima llegada".'), + const SizedBox(height: 8), + _buildBulletRow(Icons.block, 'Nunca persigas ni te acerques al camión. El sistema te avisará a tiempo.'), + const SizedBox(height: 8), + _buildBulletRow(Icons.eco_outlined, 'Separar tus residuos hace más eficiente la recolección. ¡Gracias!'), + ], + ), + ), + + const SizedBox(height: 20), + ], ), - ); - } -} \ No newline at end of file + ), + ); + } + + Widget _buildBulletRow(IconData icon, String text) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 16, color: Colors.brown.shade700), + const SizedBox(width: 10), + Expanded(child: Text(text, style: TextStyle(fontSize: 13, color: Colors.brown.shade900, height: 1.2))), + ], + ); + } +} + +// ========================================================================= +// 📍 MODELO 1: Catálogo de Colonias +// ========================================================================= +class ColoniaModel { + String colonia; + String routeId; + String horarioEstimado; + + ColoniaModel({required this.colonia, required this.routeId, required this.horarioEstimado}); + + factory ColoniaModel.fromJson(Map json) => ColoniaModel(colonia: json['colonia'], routeId: json['routeId'], horarioEstimado: json['horarioEstimado']); + + Map toJson() => {'colonia': colonia, 'routeId': routeId, 'horarioEstimado': horarioEstimado}; +} + +// ========================================================================= +// 📍 MODELO 2: Sistema de Notificaciones Alertas Push +// ========================================================================= +class NotificationModel { + String triggerEvent; + String condition; + PushPayloadModel pushPayload; + + NotificationModel({required this.triggerEvent, required this.condition, required this.pushPayload}); + + factory NotificationModel.fromJson(Map json) => NotificationModel(triggerEvent: json['triggerEvent'], condition: json['condition'], pushPayload: PushPayloadModel.fromJson(json['pushPayload'])); + + Map toJson() => {'triggerEvent': triggerEvent, 'condition': condition, 'pushPayload': pushPayload.toJson()}; +} + +class PushPayloadModel { + String title; + String body; + + PushPayloadModel({required this.title, required this.body}); + + factory PushPayloadModel.fromJson(Map json) => PushPayloadModel(title: json['title'], body: json['body']); + + Map toJson() => {'title': title, 'body': body}; +}