first commit

This commit is contained in:
=
2026-09-21 16:33:51 -04:00
commit a899479dc9
204 changed files with 21132 additions and 0 deletions
@@ -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();
}
}
}