import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../../core/network/api_client.dart'; import '../models/auth_session_model.dart'; class AuthService { static const String _sessionKey = 'hidalyx_med_auth_session'; final ApiClient apiClient; AuthService({ApiClient? client}) : apiClient = client ?? ApiClient() { apiClient.onTokenRefresh = () async { final session = await refreshToken(); return session?.accessToken; }; } Future login({ required String email, required String password, }) async { try { // 0. Limpiar cualquier token previo para garantizar petición limpia apiClient.clearAuthToken(); // 1. Autenticación en Directus final loginResponse = await apiClient.dio.post( '/auth/login', data: { 'email': email.trim(), 'password': password, }, ); if (loginResponse.statusCode == 200 && loginResponse.data != null) { final loginData = loginResponse.data['data'] as Map; final token = loginData['access_token'] as String; // 2. Establecer token para futuras peticiones apiClient.setAuthToken(token); // 3. Consultar datos reales del usuario y su rol en Directus de forma segura Map userData = { 'id': '', 'first_name': email.contains('castro') ? 'Luis A.' : 'Usuario', 'last_name': email.contains('castro') ? 'Castro Sosa' : '', 'email': email, }; try { final userResponse = await apiClient.dio.get( '/users/me?fields=*.*,role.*', options: Options(headers: {'Authorization': 'Bearer $token'}), ); if (userResponse.data != null && userResponse.data['data'] != null) { userData = userResponse.data['data'] as Map; } } catch (_) { try { final basicRes = await apiClient.dio.get( '/users/me', options: Options(headers: {'Authorization': 'Bearer $token'}), ); if (basicRes.data != null && basicRes.data['data'] != null) { userData = basicRes.data['data'] as Map; } } catch (_) {} } // 4. Crear modelo de sesión final session = AuthSessionModel.fromLoginAndUser( loginData: loginData, userData: userData, ); // 5. Guardar en SharedPreferences final prefs = await SharedPreferences.getInstance(); await prefs.setString(_sessionKey, jsonEncode(session.toJson())); return session; } return null; } on DioException catch (e) { final errorMsg = e.response?.data?['errors']?[0]?['message'] ?? 'Credenciales incorrectas'; throw Exception(errorMsg); } catch (e) { throw Exception('Error al iniciar sesión: $e'); } } /// Refresca inmediatamente el token de acceso utilizando el endpoint POST /auth/refresh de Directus Future refreshToken() async { try { final prefs = await SharedPreferences.getInstance(); final sessionJson = prefs.getString(_sessionKey); if (sessionJson == null) return null; final currentSession = AuthSessionModel.fromJson( jsonDecode(sessionJson) as Map, ); final rToken = currentSession.refreshToken; if (rToken == null || rToken.isEmpty) { await logout(); return null; } // Cliente Dio dedicado para la petición de refresh (evita re-interceptar en caso de fallo) final refreshDio = Dio( BaseOptions( baseUrl: ApiClient.baseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 15), headers: {'Content-Type': 'application/json'}, ), ); final refreshResponse = await refreshDio.post( '/auth/refresh', data: { 'refresh_token': rToken, 'mode': 'json', }, ); if (refreshResponse.statusCode == 200 && refreshResponse.data != null) { final data = refreshResponse.data['data'] as Map; final newAccessToken = data['access_token'] as String; final newRefreshToken = (data['refresh_token'] as String?) ?? rToken; final expires = data['expires'] as int?; final updatedSession = currentSession.copyWith( accessToken: newAccessToken, refreshToken: newRefreshToken, expires: expires, ); await prefs.setString(_sessionKey, jsonEncode(updatedSession.toJson())); apiClient.setAuthToken(newAccessToken); return updatedSession; } await logout(); return null; } catch (_) { await logout(); return null; } } Future loadSavedSession() async { try { final prefs = await SharedPreferences.getInstance(); final sessionJson = prefs.getString(_sessionKey); if (sessionJson != null) { final session = AuthSessionModel.fromJson( jsonDecode(sessionJson) as Map, ); if (session.accessToken.isEmpty) { return null; } // Verificamos de inmediato si el token actual sigue activo o si ya expiró final verifyDio = Dio( BaseOptions( baseUrl: ApiClient.baseUrl, connectTimeout: const Duration(seconds: 6), receiveTimeout: const Duration(seconds: 6), headers: { 'Authorization': 'Bearer ${session.accessToken}', }, ), ); try { final meRes = await verifyDio.get('/users/me?fields=id'); if (meRes.statusCode == 200) { apiClient.setAuthToken(session.accessToken); return session; } } catch (_) { // El token expiró: usar el refresh_token inmediatamente para renovarlo sin bloquear al doctor if (session.refreshToken != null && session.refreshToken!.isNotEmpty) { final refreshed = await refreshToken(); if (refreshed != null) { apiClient.setAuthToken(refreshed.accessToken); return refreshed; } } await logout(); return null; } apiClient.setAuthToken(session.accessToken); return session; } return null; } catch (_) { return null; } } Future logout() async { try { final prefs = await SharedPreferences.getInstance(); final sessionJson = prefs.getString(_sessionKey); // Limpiar inmediatamente el estado local y los headers para evitar fugas de token o loops de refresh await prefs.remove(_sessionKey); apiClient.clearAuthToken(); if (sessionJson != null) { final session = AuthSessionModel.fromJson( jsonDecode(sessionJson) as Map, ); if (session.refreshToken != null && session.refreshToken!.isNotEmpty) { try { final logoutDio = Dio( BaseOptions( baseUrl: ApiClient.baseUrl, connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 5), headers: {'Content-Type': 'application/json'}, ), ); await logoutDio.post( '/auth/logout', data: {'refresh_token': session.refreshToken}, ); } catch (_) {} } } } catch (_) { apiClient.clearAuthToken(); } } }