first commit
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user