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

@@ -1,69 +1,61 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:go_router/go_router.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import '../firebase_options.dart';
final bootstrapProvider = FutureProvider<void>((ref) async {
await dotenv.load(fileName: 'assets/.env');
// Inicializar Firebase (si hay DefaultFirebaseOptions, úsalas; sino, intenta initializeApp() y espera que haya google-services/Info.plist)
final FirebaseOptions? options = DefaultFirebaseOptions.currentPlatform;
if (options != null) {
await Firebase.initializeApp(options: options);
} else {
await Firebase.initializeApp();
}
// Registrar handler para mensajes en background
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
});
// Handler top-level requerido por FCM
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// Asegurar Firebase inicializado en background isolate
try {
await Firebase.initializeApp();
} catch (_) {
// ignore if already initialized
}
// Aquí puedes procesar y guardar la notificación si hace falta
debugPrint(
'FCM background message received: ${message.messageId} | data: ${message.data}',
);
}
final apiClientProvider = Provider<Dio>((ref) {
final baseUrl = dotenv.env['API_BASE_URL'] ?? 'http://10.0.2.2:8000';
return Dio(
BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
headers: const <String, dynamic>{'Content-Type': 'application/json'},
),
);
});
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
return const FlutterSecureStorage();
});
import '../core/network/api_client.dart';
import '../core/services/auth_controller.dart';
import '../core/storage/secure_storage.dart';
import 'bootstrap.dart' as bootstrap;
import '../features/auth/login_page.dart';
import '../features/auth/register_page.dart';
import '../features/addresses/new_address_page.dart';
final routerProvider = Provider<GoRouter>((ref) {
final authSnapshot = ref.watch(authControllerProvider);
final isAuthenticated = authSnapshot.asData?.value.isAuthenticated ?? false;
return GoRouter(
initialLocation: '/home',
redirect: (context, state) {
final location = state.matchedLocation;
final isAuthRoute = location == '/login' || location == '/register';
if (authSnapshot.isLoading) {
return location == '/login' ? null : '/login';
}
if (!isAuthenticated) {
return isAuthRoute ? null : '/login';
}
if (isAuthenticated && isAuthRoute) {
return '/home';
}
return null;
},
routes: <RouteBase>[
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: '/register',
name: 'register',
builder: (context, state) => const RegisterPage(),
),
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/addresses/new',
name: 'addresses-new',
builder: (context, state) => const NewAddressPage(),
),
GoRoute(
path: '/status',
name: 'status',
@@ -78,9 +70,9 @@ class RecolectaApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final bootstrap = ref.watch(bootstrapProvider);
final bootstrapState = ref.watch(bootstrap.bootstrapProvider);
return bootstrap.when(
return bootstrapState.when(
loading: () => const MaterialApp(
debugShowCheckedModeBanner: false,
home: BootstrapLoadingPage(),
@@ -154,11 +146,24 @@ class HomePage extends ConsumerWidget {
final dio = ref.read(apiClientProvider);
final storage = ref.read(secureStorageProvider);
final baseUrl = dio.options.baseUrl;
final authState = ref.watch(authControllerProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Recolecta'),
actions: [
IconButton(
onPressed: authState.isLoading
? null
: () async {
await ref.read(authControllerProvider.notifier).logout();
if (context.mounted) {
context.go('/login');
}
},
icon: const Icon(Icons.logout),
tooltip: 'Salir',
),
IconButton(
onPressed: () => context.goNamed('status'),
icon: const Icon(Icons.route),
@@ -179,6 +184,11 @@ class HomePage extends ConsumerWidget {
'La app ya carga .env, Riverpod y GoRouter para la base del MVP.',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.go('/addresses/new'),
child: const Text('Agregar domicilio'),
),
const SizedBox(height: 24),
_InfoCard(title: 'API base URL', value: baseUrl, icon: Icons.cloud),
const SizedBox(height: 16),

View File

@@ -0,0 +1,36 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../firebase_options.dart';
final bootstrapProvider = FutureProvider<void>((ref) async {
await dotenv.load(fileName: 'assets/.env');
await _initializeFirebase();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
});
Future<void> _initializeFirebase() async {
try {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
} on UnsupportedError {
await Firebase.initializeApp();
}
}
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
try {
await _initializeFirebase();
} catch (_) {
// Ignore reinitialization errors in the background isolate.
}
debugPrint(
'FCM background message received: ${message.messageId} | data: ${message.data}',
);
}