240 lines
7.6 KiB
Dart
240 lines
7.6 KiB
Dart
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<AuthSessionModel?> 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<String, dynamic>;
|
||
|
|
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<String, dynamic> 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<String, dynamic>;
|
||
|
|
}
|
||
|
|
} 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<String, dynamic>;
|
||
|
|
}
|
||
|
|
} 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<AuthSessionModel?> 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<String, dynamic>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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<String, dynamic>;
|
||
|
|
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<AuthSessionModel?> loadSavedSession() async {
|
||
|
|
try {
|
||
|
|
final prefs = await SharedPreferences.getInstance();
|
||
|
|
final sessionJson = prefs.getString(_sessionKey);
|
||
|
|
if (sessionJson != null) {
|
||
|
|
final session = AuthSessionModel.fromJson(
|
||
|
|
jsonDecode(sessionJson) as Map<String, dynamic>,
|
||
|
|
);
|
||
|
|
|
||
|
|
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<void> 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<String, dynamic>,
|
||
|
|
);
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|