Conecta login Flutter con backend por roles
This commit is contained in:
177
lib/main.dart
177
lib/main.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -681,6 +682,102 @@ class AppCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =======================================================
|
||||||
|
// API SERVICE - CONEXIÓN CON BACKEND FASTAPI
|
||||||
|
// =======================================================
|
||||||
|
|
||||||
|
class ApiSession {
|
||||||
|
static String? token;
|
||||||
|
static Map<String, dynamic>? user;
|
||||||
|
|
||||||
|
static String get role => (user?['role'] ?? '').toString().toLowerCase();
|
||||||
|
static String get name => (user?['name'] ?? 'Usuario').toString();
|
||||||
|
static String get email => (user?['email'] ?? '').toString();
|
||||||
|
|
||||||
|
static void save({required String accessToken, required Map<String, dynamic> userData}) {
|
||||||
|
token = accessToken;
|
||||||
|
user = userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear() {
|
||||||
|
token = null;
|
||||||
|
user = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
// Chrome en la misma computadora donde corre FastAPI.
|
||||||
|
static const String apiBase = 'http://127.0.0.1:8000';
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> login({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse('$apiBase/auth/login');
|
||||||
|
|
||||||
|
final response = await http.post(
|
||||||
|
uri,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({
|
||||||
|
'email': email.trim().toLowerCase(),
|
||||||
|
'password': password.trim(),
|
||||||
|
}),
|
||||||
|
).timeout(const Duration(seconds: 8));
|
||||||
|
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
|
String message = 'No se pudo iniciar sesión.';
|
||||||
|
try {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
message = data['detail']?.toString() ?? message;
|
||||||
|
} catch (_) {}
|
||||||
|
throw Exception(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
final token = data['access_token']?.toString();
|
||||||
|
final user = Map<String, dynamic>.from(data['user'] ?? {});
|
||||||
|
|
||||||
|
if (token == null || token.isEmpty || user.isEmpty) {
|
||||||
|
throw Exception('Respuesta inválida del servidor.');
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiSession.save(accessToken: token, userData: user);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, String> authHeaders() {
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
if (ApiSession.token != null) 'Authorization': 'Bearer ${ApiSession.token}',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> health() async {
|
||||||
|
try {
|
||||||
|
final response = await http
|
||||||
|
.get(Uri.parse('$apiBase/public/health'))
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
return response.statusCode == 200;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> adminDashboard() async {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('$apiBase/admin/dashboard'),
|
||||||
|
headers: authHeaders(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('No se pudo cargar dashboard admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =======================================================
|
// =======================================================
|
||||||
// LOGIN
|
// LOGIN
|
||||||
// =======================================================
|
// =======================================================
|
||||||
@@ -695,8 +792,14 @@ class LoginPage extends StatefulWidget {
|
|||||||
class _LoginPageState extends State<LoginPage> {
|
class _LoginPageState extends State<LoginPage> {
|
||||||
final email = TextEditingController();
|
final email = TextEditingController();
|
||||||
final pass = TextEditingController();
|
final pass = TextEditingController();
|
||||||
|
bool loading = false;
|
||||||
|
|
||||||
void entrar() {
|
bool correoValido(String value) {
|
||||||
|
final regex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
|
||||||
|
return regex.hasMatch(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> entrar() async {
|
||||||
final correo = email.text.trim().toLowerCase();
|
final correo = email.text.trim().toLowerCase();
|
||||||
final password = pass.text.trim();
|
final password = pass.text.trim();
|
||||||
|
|
||||||
@@ -707,7 +810,23 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (correo == 'operador@demo.com' && password == '123456') {
|
if (!correoValido(correo)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Ingresa un correo válido. Ejemplo: demo@correo.com')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => loading = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ApiService.login(email: correo, password: password);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final role = ApiSession.role;
|
||||||
|
|
||||||
|
if (role == 'operador') {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const OperadorPage()),
|
MaterialPageRoute(builder: (_) => const OperadorPage()),
|
||||||
@@ -715,7 +834,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (correo == 'admin@demo.com' && password == '123456') {
|
if (role == 'admin') {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const AdminPage()),
|
MaterialPageRoute(builder: (_) => const AdminPage()),
|
||||||
@@ -727,6 +846,17 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const HomePage()),
|
MaterialPageRoute(builder: (_) => const HomePage()),
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Error de login: ${e.toString().replaceFirst('Exception: ', '')}'),
|
||||||
|
duration: const Duration(seconds: 5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => loading = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -770,6 +900,29 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 17, height: 1.35),
|
style: TextStyle(fontSize: 17, height: 1.35),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: AppColors.green.withOpacity(0.25)),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.api, color: AppColors.green, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'Login conectado al backend FastAPI',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w800),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextField(
|
TextField(
|
||||||
controller: email,
|
controller: email,
|
||||||
@@ -792,9 +945,18 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
height: 56,
|
height: 56,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: entrar,
|
onPressed: loading ? null : () => entrar(),
|
||||||
icon: const Icon(Icons.login),
|
icon: loading
|
||||||
label: const Text('Iniciar sesión', style: TextStyle(fontSize: 18)),
|
? const SizedBox(
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.login),
|
||||||
|
label: Text(
|
||||||
|
loading ? 'Conectando...' : 'Iniciar sesión',
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -902,6 +1064,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Cerrar sesión',
|
tooltip: 'Cerrar sesión',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
ApiSession.clear();
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||||
@@ -1528,6 +1691,7 @@ class _DatosPageState extends State<DatosPage> with SingleTickerProviderStateMix
|
|||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Cerrar sesión',
|
tooltip: 'Cerrar sesión',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
ApiSession.clear();
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||||
@@ -2579,6 +2743,7 @@ class _OperadorPageState extends State<OperadorPage> {
|
|||||||
tooltip: 'Cerrar sesión',
|
tooltip: 'Cerrar sesión',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
|
ApiSession.clear();
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter_map: ^8.3.0
|
flutter_map: ^8.3.0
|
||||||
latlong2: ^0.9.1
|
latlong2: ^0.9.1
|
||||||
|
http: ^1.6.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user