bLOQUE p1 BACKEND Y SEGURIDAD, AUTENTICACION CON SUPABASE. jwt. RBAC CRUD

This commit is contained in:
shinra32
2026-05-22 19:45:05 -06:00
parent 5dc8390855
commit fc28333e3f
52 changed files with 1605 additions and 109 deletions

View File

View File

@@ -0,0 +1,3 @@
const String authTokenStorageKey = 'auth_jwt';
const String authUserRoleStorageKey = 'auth_user_role';
const String authRouteIdStorageKey = 'auth_route_id';

View File

View 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,
};
}
}

View 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';
}

View 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;
}

View 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(),
);
}
}

View File

View 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;
});

View File

View 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());
}
}

View 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;
}
}

View 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);
}
}

View File

View 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();
});

View File

View File