first commit
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
class AuthSessionModel {
|
||||
final String accessToken;
|
||||
final String? refreshToken;
|
||||
final int? expires;
|
||||
final String userId;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String email;
|
||||
final String? roleName;
|
||||
|
||||
const AuthSessionModel({
|
||||
required this.accessToken,
|
||||
this.refreshToken,
|
||||
this.expires,
|
||||
required this.userId,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.email,
|
||||
this.roleName,
|
||||
});
|
||||
|
||||
String get fullName {
|
||||
final name = '$firstName $lastName'.trim();
|
||||
return name.isNotEmpty ? name : 'Dr. Luis A. Castro Sosa';
|
||||
}
|
||||
|
||||
/// Indica si el usuario actual tiene rol de Secretaria, Asistente o Recepción
|
||||
bool get isSecretary {
|
||||
final normalized = (roleName ?? '').trim().toLowerCase();
|
||||
return normalized.contains('secretari') ||
|
||||
normalized.contains('asistent') ||
|
||||
normalized.contains('recepc');
|
||||
}
|
||||
|
||||
/// Indica si el usuario actual es Doctor o Administrador
|
||||
bool get isDoctorOrAdmin => !isSecretary;
|
||||
|
||||
AuthSessionModel copyWith({
|
||||
String? accessToken,
|
||||
String? refreshToken,
|
||||
int? expires,
|
||||
String? userId,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? email,
|
||||
String? roleName,
|
||||
}) {
|
||||
return AuthSessionModel(
|
||||
accessToken: accessToken ?? this.accessToken,
|
||||
refreshToken: refreshToken ?? this.refreshToken,
|
||||
expires: expires ?? this.expires,
|
||||
userId: userId ?? this.userId,
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
email: email ?? this.email,
|
||||
roleName: roleName ?? this.roleName,
|
||||
);
|
||||
}
|
||||
|
||||
factory AuthSessionModel.fromLoginAndUser({
|
||||
required Map<String, dynamic> loginData,
|
||||
required Map<String, dynamic> userData,
|
||||
}) {
|
||||
return AuthSessionModel(
|
||||
accessToken: loginData['access_token'] as String? ?? '',
|
||||
refreshToken: loginData['refresh_token'] as String?,
|
||||
expires: loginData['expires'] as int?,
|
||||
userId: userData['id'] as String? ?? '',
|
||||
firstName: userData['first_name'] as String? ?? '',
|
||||
lastName: userData['last_name'] as String? ?? '',
|
||||
email: userData['email'] as String? ?? '',
|
||||
roleName: (userData['role'] is Map)
|
||||
? userData['role']['name'] as String?
|
||||
: userData['role']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
factory AuthSessionModel.fromJson(Map<String, dynamic> json) {
|
||||
return AuthSessionModel(
|
||||
accessToken: json['access_token'] as String? ?? '',
|
||||
refreshToken: json['refresh_token'] as String?,
|
||||
expires: json['expires'] as int?,
|
||||
userId: json['user_id'] as String? ?? '',
|
||||
firstName: json['first_name'] as String? ?? '',
|
||||
lastName: json['last_name'] as String? ?? '',
|
||||
email: json['email'] as String? ?? '',
|
||||
roleName: json['role_name'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'access_token': accessToken,
|
||||
'refresh_token': refreshToken,
|
||||
'expires': expires,
|
||||
'user_id': userId,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'email': email,
|
||||
'role_name': roleName,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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