bLOQUE p1 BACKEND Y SEGURIDAD, AUTENTICACION CON SUPABASE. jwt. RBAC CRUD
This commit is contained in:
0
recolecta_app/lib/core/auth/.gitkeep
Normal file
0
recolecta_app/lib/core/auth/.gitkeep
Normal file
0
recolecta_app/lib/core/constants/.gitkeep
Normal file
0
recolecta_app/lib/core/constants/.gitkeep
Normal file
3
recolecta_app/lib/core/constants/auth_constants.dart
Normal file
3
recolecta_app/lib/core/constants/auth_constants.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
const String authTokenStorageKey = 'auth_jwt';
|
||||
const String authUserRoleStorageKey = 'auth_user_role';
|
||||
const String authRouteIdStorageKey = 'auth_route_id';
|
||||
0
recolecta_app/lib/core/models/.gitkeep
Normal file
0
recolecta_app/lib/core/models/.gitkeep
Normal file
22
recolecta_app/lib/core/models/address.dart
Normal file
22
recolecta_app/lib/core/models/address.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'colonia.dart';
|
||||
|
||||
class AddressModel {
|
||||
const AddressModel({
|
||||
required this.label,
|
||||
required this.street,
|
||||
required this.colonia,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String street;
|
||||
final Colonia colonia;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'label': label,
|
||||
'calle': street,
|
||||
'colonia': colonia.nombre,
|
||||
'route_id': colonia.routeId,
|
||||
};
|
||||
}
|
||||
}
|
||||
11
recolecta_app/lib/core/models/auth_session.dart
Normal file
11
recolecta_app/lib/core/models/auth_session.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
class AuthSession {
|
||||
const AuthSession({required this.token, this.userRole, this.routeId});
|
||||
|
||||
final String token;
|
||||
final String? userRole;
|
||||
final String? routeId;
|
||||
|
||||
bool get isCitizen => userRole == 'citizen';
|
||||
bool get isDriver => userRole == 'driver';
|
||||
bool get isAdmin => userRole == 'admin';
|
||||
}
|
||||
28
recolecta_app/lib/core/models/auth_state.dart
Normal file
28
recolecta_app/lib/core/models/auth_state.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
class AuthState {
|
||||
const AuthState({
|
||||
required this.isAuthenticated,
|
||||
this.token,
|
||||
this.userRole,
|
||||
this.routeId,
|
||||
});
|
||||
|
||||
const AuthState.unauthenticated()
|
||||
: isAuthenticated = false,
|
||||
token = null,
|
||||
userRole = null,
|
||||
routeId = null;
|
||||
|
||||
const AuthState.authenticated({
|
||||
required String token,
|
||||
String? userRole,
|
||||
String? routeId,
|
||||
}) : isAuthenticated = true,
|
||||
token = token,
|
||||
userRole = userRole,
|
||||
routeId = routeId;
|
||||
|
||||
final bool isAuthenticated;
|
||||
final String? token;
|
||||
final String? userRole;
|
||||
final String? routeId;
|
||||
}
|
||||
37
recolecta_app/lib/core/models/colonia.dart
Normal file
37
recolecta_app/lib/core/models/colonia.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
class Colonia {
|
||||
const Colonia({
|
||||
required this.id,
|
||||
required this.nombre,
|
||||
this.routeId,
|
||||
this.horarioEstimado,
|
||||
this.turno,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String nombre;
|
||||
final String? routeId;
|
||||
final String? horarioEstimado;
|
||||
final String? turno;
|
||||
|
||||
factory Colonia.fromJson(Map<String, dynamic> json) {
|
||||
final rawId =
|
||||
json['id'] ??
|
||||
json['routeId'] ??
|
||||
json['route_id'] ??
|
||||
json['nombre'] ??
|
||||
json['name'];
|
||||
final rawNombre = json['nombre'] ?? json['name'] ?? rawId;
|
||||
|
||||
return Colonia(
|
||||
id: rawId?.toString() ?? rawNombre?.toString() ?? '',
|
||||
nombre: rawNombre?.toString() ?? '',
|
||||
routeId: (json['routeId'] ?? json['route_id'])?.toString(),
|
||||
horarioEstimado:
|
||||
(json['horario_estimado'] ??
|
||||
json['horarioEstimado'] ??
|
||||
json['schedule'])
|
||||
?.toString(),
|
||||
turno: (json['turno'] ?? json['shift'])?.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
0
recolecta_app/lib/core/network/.gitkeep
Normal file
0
recolecta_app/lib/core/network/.gitkeep
Normal file
34
recolecta_app/lib/core/network/api_client.dart
Normal file
34
recolecta_app/lib/core/network/api_client.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../constants/auth_constants.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
final apiClientProvider = Provider<Dio>((ref) {
|
||||
final baseUrl = dotenv.env['API_BASE_URL'] ?? 'http://10.0.2.2:8000';
|
||||
final secureStorage = ref.read(secureStorageProvider);
|
||||
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
headers: const <String, dynamic>{'Content-Type': 'application/json'},
|
||||
),
|
||||
);
|
||||
|
||||
dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
final token = await secureStorage.read(key: authTokenStorageKey);
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return dio;
|
||||
});
|
||||
0
recolecta_app/lib/core/services/.gitkeep
Normal file
0
recolecta_app/lib/core/services/.gitkeep
Normal file
61
recolecta_app/lib/core/services/auth_controller.dart
Normal file
61
recolecta_app/lib/core/services/auth_controller.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../models/auth_state.dart';
|
||||
import 'auth_service.dart';
|
||||
|
||||
final authControllerProvider = AsyncNotifierProvider<AuthController, AuthState>(
|
||||
AuthController.new,
|
||||
);
|
||||
|
||||
class AuthController extends AsyncNotifier<AuthState> {
|
||||
@override
|
||||
Future<AuthState> build() async {
|
||||
final session = await ref.read(authServiceProvider).restoreSession();
|
||||
if (session == null) {
|
||||
return const AuthState.unauthenticated();
|
||||
}
|
||||
|
||||
return AuthState.authenticated(
|
||||
token: session.token,
|
||||
userRole: session.userRole,
|
||||
routeId: session.routeId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> login({required String email, required String password}) async {
|
||||
state = const AsyncLoading<AuthState>();
|
||||
final session = await ref
|
||||
.read(authServiceProvider)
|
||||
.login(email: email, password: password);
|
||||
state = AsyncData(
|
||||
AuthState.authenticated(
|
||||
token: session.token,
|
||||
userRole: session.userRole,
|
||||
routeId: session.routeId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> register({
|
||||
required String email,
|
||||
required String phone,
|
||||
required String password,
|
||||
}) async {
|
||||
state = const AsyncLoading<AuthState>();
|
||||
final session = await ref
|
||||
.read(authServiceProvider)
|
||||
.register(email: email, phone: phone, password: password);
|
||||
state = AsyncData(
|
||||
AuthState.authenticated(
|
||||
token: session.token,
|
||||
userRole: session.userRole,
|
||||
routeId: session.routeId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await ref.read(authServiceProvider).logout();
|
||||
state = const AsyncData(AuthState.unauthenticated());
|
||||
}
|
||||
}
|
||||
149
recolecta_app/lib/core/services/auth_service.dart
Normal file
149
recolecta_app/lib/core/services/auth_service.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../constants/auth_constants.dart';
|
||||
import '../models/auth_session.dart';
|
||||
import '../network/api_client.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
final authServiceProvider = Provider<AuthService>((ref) {
|
||||
return AuthService(
|
||||
apiClient: ref.read(apiClientProvider),
|
||||
secureStorage: ref.read(secureStorageProvider),
|
||||
);
|
||||
});
|
||||
|
||||
class AuthService {
|
||||
AuthService({
|
||||
required Dio apiClient,
|
||||
required FlutterSecureStorage secureStorage,
|
||||
}) : _apiClient = apiClient,
|
||||
_secureStorage = secureStorage;
|
||||
|
||||
final Dio _apiClient;
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
|
||||
Future<AuthSession?> restoreSession() async {
|
||||
final token = await _secureStorage.read(key: authTokenStorageKey);
|
||||
if (token == null || token.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AuthSession(
|
||||
token: token,
|
||||
userRole: await _secureStorage.read(key: authUserRoleStorageKey),
|
||||
routeId: await _secureStorage.read(key: authRouteIdStorageKey),
|
||||
);
|
||||
}
|
||||
|
||||
Future<AuthSession> register({
|
||||
required String email,
|
||||
required String phone,
|
||||
required String password,
|
||||
}) {
|
||||
return _authenticate(
|
||||
path: '/auth/register',
|
||||
payload: <String, dynamic>{
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<AuthSession> login({required String email, required String password}) {
|
||||
return _authenticate(
|
||||
path: '/auth/login',
|
||||
payload: <String, dynamic>{'email': email, 'password': password},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logout() {
|
||||
return Future.wait(<Future<void>>[
|
||||
_secureStorage.delete(key: authTokenStorageKey),
|
||||
_secureStorage.delete(key: authUserRoleStorageKey),
|
||||
_secureStorage.delete(key: authRouteIdStorageKey),
|
||||
]).then((_) {});
|
||||
}
|
||||
|
||||
Future<AuthSession> _authenticate({
|
||||
required String path,
|
||||
required Map<String, dynamic> payload,
|
||||
}) async {
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: payload,
|
||||
);
|
||||
|
||||
final session = _extractSession(response.data);
|
||||
if (session == null || session.token.isEmpty) {
|
||||
throw StateError('El backend no devolvió un JWT.');
|
||||
}
|
||||
|
||||
await _secureStorage.write(key: authTokenStorageKey, value: session.token);
|
||||
await _writeOptionalString(authUserRoleStorageKey, session.userRole);
|
||||
await _writeOptionalString(authRouteIdStorageKey, session.routeId);
|
||||
return session;
|
||||
}
|
||||
|
||||
Future<void> _writeOptionalString(String key, String? value) async {
|
||||
if (value == null || value.isEmpty) {
|
||||
await _secureStorage.delete(key: key);
|
||||
return;
|
||||
}
|
||||
|
||||
await _secureStorage.write(key: key, value: value);
|
||||
}
|
||||
|
||||
AuthSession? _extractSession(Map<String, dynamic>? data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final dataMap = data['data'] is Map<String, dynamic>
|
||||
? data['data'] as Map<String, dynamic>
|
||||
: data;
|
||||
|
||||
final token = _pickString(<dynamic>[
|
||||
dataMap['access_token'],
|
||||
dataMap['token'],
|
||||
dataMap['jwt'],
|
||||
data['access_token'],
|
||||
data['token'],
|
||||
data['jwt'],
|
||||
]);
|
||||
|
||||
if (token == null || token.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AuthSession(
|
||||
token: token,
|
||||
userRole: _pickString(<dynamic>[
|
||||
dataMap['userRole'],
|
||||
dataMap['user_role'],
|
||||
dataMap['role'],
|
||||
data['userRole'],
|
||||
data['user_role'],
|
||||
data['role'],
|
||||
]),
|
||||
routeId: _pickString(<dynamic>[
|
||||
dataMap['routeId'],
|
||||
dataMap['route_id'],
|
||||
data['routeId'],
|
||||
data['route_id'],
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
String? _pickString(Iterable<dynamic> candidates) {
|
||||
for (final candidate in candidates) {
|
||||
if (candidate is String && candidate.isNotEmpty) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
39
recolecta_app/lib/core/services/colonias_service.dart
Normal file
39
recolecta_app/lib/core/services/colonias_service.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../models/colonia.dart';
|
||||
import '../network/api_client.dart';
|
||||
|
||||
final coloniasServiceProvider = Provider<ColoniasService>((ref) {
|
||||
return ColoniasService(ref.read(apiClientProvider));
|
||||
});
|
||||
|
||||
class ColoniasService {
|
||||
ColoniasService(this._apiClient);
|
||||
|
||||
final Dio _apiClient;
|
||||
|
||||
Future<List<Colonia>> getColonias() async {
|
||||
final response = await _apiClient.get<dynamic>('/colonias');
|
||||
final data = response.data;
|
||||
|
||||
if (data == null) {
|
||||
return const <Colonia>[];
|
||||
}
|
||||
|
||||
final rawList = switch (data) {
|
||||
List<dynamic> value => value,
|
||||
Map<String, dynamic> value when value['data'] is List<dynamic> =>
|
||||
value['data'] as List<dynamic>,
|
||||
Map<String, dynamic> value when value['colonias'] is List<dynamic> =>
|
||||
value['colonias'] as List<dynamic>,
|
||||
_ => const <dynamic>[],
|
||||
};
|
||||
|
||||
return rawList
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(Colonia.fromJson)
|
||||
.where((colonia) => colonia.id.isNotEmpty && colonia.nombre.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
0
recolecta_app/lib/core/storage/.gitkeep
Normal file
0
recolecta_app/lib/core/storage/.gitkeep
Normal file
6
recolecta_app/lib/core/storage/secure_storage.dart
Normal file
6
recolecta_app/lib/core/storage/secure_storage.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
|
||||
return const FlutterSecureStorage();
|
||||
});
|
||||
0
recolecta_app/lib/core/theme/.gitkeep
Normal file
0
recolecta_app/lib/core/theme/.gitkeep
Normal file
0
recolecta_app/lib/core/utils/.gitkeep
Normal file
0
recolecta_app/lib/core/utils/.gitkeep
Normal file
Reference in New Issue
Block a user