From cb53f5585b5582fef63304b0625c6e320266fb12 Mon Sep 17 00:00:00 2001 From: renolation Date: Sun, 28 Sep 2025 00:20:44 +0700 Subject: [PATCH] fix api --- lib/core/constants/constants.dart | 1 + lib/core/constants/environment_config.dart | 140 +++++++ lib/core/debug/api_debug_page.dart | 377 ++++++++++++++++++ lib/core/network/api_constants.dart | 49 +-- lib/core/network/auth_service_example.dart | 242 +++++++++++ lib/core/network/test_api_connection.dart | 74 ++++ lib/core/providers/app_providers.dart | 94 ++++- lib/core/providers/app_providers.g.dart | 40 +- lib/core/providers/network_providers.dart | 13 + .../datasources/auth_remote_datasource.dart | 228 +++++------ 10 files changed, 1098 insertions(+), 160 deletions(-) create mode 100644 lib/core/constants/environment_config.dart create mode 100644 lib/core/debug/api_debug_page.dart create mode 100644 lib/core/network/auth_service_example.dart create mode 100644 lib/core/network/test_api_connection.dart diff --git a/lib/core/constants/constants.dart b/lib/core/constants/constants.dart index dde8da9..204a9e9 100644 --- a/lib/core/constants/constants.dart +++ b/lib/core/constants/constants.dart @@ -1,3 +1,4 @@ // Barrel export file for constants export 'app_constants.dart'; +export 'environment_config.dart'; export 'storage_constants.dart'; \ No newline at end of file diff --git a/lib/core/constants/environment_config.dart b/lib/core/constants/environment_config.dart new file mode 100644 index 0000000..6051d95 --- /dev/null +++ b/lib/core/constants/environment_config.dart @@ -0,0 +1,140 @@ +/// Environment configuration for API endpoints and settings +enum Environment { + development, + staging, + production, +} + +/// Environment-specific configuration +class EnvironmentConfig { + // Private constructor to prevent instantiation + const EnvironmentConfig._(); + + /// Current environment - Change this to switch environments + static const Environment currentEnvironment = Environment.development; + + /// Get base URL for current environment + static String get baseUrl { + switch (currentEnvironment) { + case Environment.development: + return 'http://localhost:3000'; + case Environment.staging: + return 'https://api-staging.example.com'; + case Environment.production: + return 'https://api.example.com'; + } + } + + /// Get API path for current environment + static String get apiPath { + switch (currentEnvironment) { + case Environment.development: + // No API prefix for local development - endpoints are directly at /auth/ + return ''; + case Environment.staging: + case Environment.production: + return '/api/v1'; + } + } + + /// Check if current environment is development + static bool get isDevelopment => currentEnvironment == Environment.development; + + /// Check if current environment is staging + static bool get isStaging => currentEnvironment == Environment.staging; + + /// Check if current environment is production + static bool get isProduction => currentEnvironment == Environment.production; + + /// Get timeout configurations based on environment + static int get connectTimeout { + switch (currentEnvironment) { + case Environment.development: + return 10000; // 10 seconds for local development + case Environment.staging: + return 20000; // 20 seconds for staging + case Environment.production: + return 30000; // 30 seconds for production + } + } + + static int get receiveTimeout { + switch (currentEnvironment) { + case Environment.development: + return 15000; // 15 seconds for local development + case Environment.staging: + return 25000; // 25 seconds for staging + case Environment.production: + return 30000; // 30 seconds for production + } + } + + static int get sendTimeout { + switch (currentEnvironment) { + case Environment.development: + return 15000; // 15 seconds for local development + case Environment.staging: + return 25000; // 25 seconds for staging + case Environment.production: + return 30000; // 30 seconds for production + } + } + + /// Get retry configurations based on environment + static int get maxRetries { + switch (currentEnvironment) { + case Environment.development: + return 2; // Fewer retries for local development + case Environment.staging: + return 3; // Standard retries for staging + case Environment.production: + return 3; // Standard retries for production + } + } + + static Duration get retryDelay { + switch (currentEnvironment) { + case Environment.development: + return const Duration(milliseconds: 500); // Faster retry for local + case Environment.staging: + return const Duration(seconds: 1); // Standard retry delay + case Environment.production: + return const Duration(seconds: 1); // Standard retry delay + } + } + + /// Enable/disable features based on environment + static bool get enableLogging => !isProduction; + static bool get enableDetailedLogging => isDevelopment; + static bool get enableCertificatePinning => isProduction; + + /// Authentication endpoints (consistent across environments) + static const String authEndpoint = '/auth'; + static const String loginEndpoint = '$authEndpoint/login'; + static const String registerEndpoint = '$authEndpoint/register'; + static const String refreshEndpoint = '$authEndpoint/refresh'; + static const String logoutEndpoint = '$authEndpoint/logout'; + + /// Full API URLs + static String get fullBaseUrl => baseUrl + apiPath; + static String get loginUrl => baseUrl + loginEndpoint; + static String get registerUrl => baseUrl + registerEndpoint; + static String get refreshUrl => baseUrl + refreshEndpoint; + static String get logoutUrl => baseUrl + logoutEndpoint; + + /// Debug information + static Map get debugInfo => { + 'environment': currentEnvironment.name, + 'baseUrl': baseUrl, + 'apiPath': apiPath, + 'fullBaseUrl': fullBaseUrl, + 'connectTimeout': connectTimeout, + 'receiveTimeout': receiveTimeout, + 'sendTimeout': sendTimeout, + 'maxRetries': maxRetries, + 'retryDelay': retryDelay.inMilliseconds, + 'enableLogging': enableLogging, + 'enableDetailedLogging': enableDetailedLogging, + 'enableCertificatePinning': enableCertificatePinning, + }; +} \ No newline at end of file diff --git a/lib/core/debug/api_debug_page.dart b/lib/core/debug/api_debug_page.dart new file mode 100644 index 0000000..2a8addc --- /dev/null +++ b/lib/core/debug/api_debug_page.dart @@ -0,0 +1,377 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../constants/environment_config.dart'; +import '../providers/app_providers.dart'; +import '../providers/network_providers.dart'; + +/// Debug page to verify API configuration and test connectivity +/// This page helps verify that the localhost:3000 configuration is working correctly +class ApiDebugPage extends ConsumerWidget { + const ApiDebugPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final environmentInfo = ref.watch(environmentInfoProvider); + final environmentDebug = ref.watch(environmentDebugInfoProvider); + final apiConnectivityTest = ref.watch(apiConnectivityTestProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('API Configuration Debug'), + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildEnvironmentCard(context, environmentInfo), + const SizedBox(height: 16), + _buildEndpointsCard(context, environmentDebug), + const SizedBox(height: 16), + _buildConnectivityTestCard(context, apiConnectivityTest, ref), + const SizedBox(height: 16), + _buildQuickActionsCard(context, ref), + ], + ), + ), + ); + } + + Widget _buildEnvironmentCard(BuildContext context, Map info) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.settings, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'Environment Configuration', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + _buildInfoRow('Environment', info['environment']), + _buildInfoRow('Base URL', info['baseUrl']), + _buildInfoRow('API Path', info['apiPath'].isEmpty ? 'None (Direct)' : info['apiPath']), + _buildInfoRow('Full Base URL', info['fullBaseUrl']), + const Divider(), + Text( + 'Timeout Settings', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + _buildInfoRow('Connect Timeout', '${info['timeouts']['connect']}ms'), + _buildInfoRow('Receive Timeout', '${info['timeouts']['receive']}ms'), + _buildInfoRow('Send Timeout', '${info['timeouts']['send']}ms'), + ], + ), + ), + ); + } + + Widget _buildEndpointsCard(BuildContext context, Map info) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.api, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'API Endpoints', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + _buildEndpointRow(context, 'Login', '${info['baseUrl']}/auth/login'), + _buildEndpointRow(context, 'Register', '${info['baseUrl']}/auth/register'), + _buildEndpointRow(context, 'Refresh', '${info['baseUrl']}/auth/refresh'), + _buildEndpointRow(context, 'Logout', '${info['baseUrl']}/auth/logout'), + ], + ), + ), + ); + } + + Widget _buildConnectivityTestCard( + BuildContext context, + AsyncValue> testResult, + WidgetRef ref, + ) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.network_check, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'Connectivity Test', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + IconButton( + onPressed: () { + ref.read(apiConnectivityTestProvider.notifier).retry(); + }, + icon: const Icon(Icons.refresh), + tooltip: 'Retry Test', + ), + ], + ), + const SizedBox(height: 16), + testResult.when( + data: (data) => _buildTestResult(context, data), + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) => _buildErrorResult(context, error.toString()), + ), + ], + ), + ), + ); + } + + Widget _buildQuickActionsCard(BuildContext context, WidgetRef ref) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.flash_on, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'Quick Actions', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + children: [ + ElevatedButton.icon( + onPressed: () => _testLogin(context, ref), + icon: const Icon(Icons.login), + label: const Text('Test Login'), + ), + ElevatedButton.icon( + onPressed: () => _testRegister(context, ref), + icon: const Icon(Icons.person_add), + label: const Text('Test Register'), + ), + ElevatedButton.icon( + onPressed: () => _copyConfiguration(context), + icon: const Icon(Icons.copy), + label: const Text('Copy Config'), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + '$label:', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle(fontFamily: 'monospace'), + ), + ), + ], + ), + ); + } + + Widget _buildEndpointRow(BuildContext context, String name, String url) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + children: [ + SizedBox( + width: 80, + child: Text( + name, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + ), + Expanded( + child: Text( + url, + style: const TextStyle(fontFamily: 'monospace'), + ), + ), + IconButton( + onPressed: () => _copyToClipboard(context, url), + icon: const Icon(Icons.copy, size: 16), + tooltip: 'Copy URL', + ), + ], + ), + ); + } + + Widget _buildTestResult(BuildContext context, Map data) { + final isSuccess = data['status'] == 'success'; + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSuccess + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + isSuccess ? Icons.check_circle : Icons.error, + color: isSuccess + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + ), + const SizedBox(width: 8), + Text( + data['message'] ?? (isSuccess ? 'Test passed' : 'Test failed'), + style: TextStyle( + fontWeight: FontWeight.bold, + color: isSuccess + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + ), + ), + ], + ), + if (data['timestamp'] != null) ...[ + const SizedBox(height: 8), + Text( + 'Tested at: ${data['timestamp']}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ], + ), + ); + } + + Widget _buildErrorResult(BuildContext context, String error) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Error: $error', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + ], + ), + ); + } + + void _testLogin(BuildContext context, WidgetRef ref) { + // This would be implemented based on your auth service + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Login test would be implemented here'), + ), + ); + } + + void _testRegister(BuildContext context, WidgetRef ref) { + // This would be implemented based on your auth service + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Register test would be implemented here'), + ), + ); + } + + void _copyConfiguration(BuildContext context) { + final config = EnvironmentConfig.debugInfo; + final configText = config.entries + .map((e) => '${e.key}: ${e.value}') + .join('\n'); + + _copyToClipboard(context, configText); + } + + void _copyToClipboard(BuildContext context, String text) { + Clipboard.setData(ClipboardData(text: text)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Copied to clipboard'), + duration: Duration(seconds: 2), + ), + ); + } +} \ No newline at end of file diff --git a/lib/core/network/api_constants.dart b/lib/core/network/api_constants.dart index ea6732b..f1664f6 100644 --- a/lib/core/network/api_constants.dart +++ b/lib/core/network/api_constants.dart @@ -1,29 +1,22 @@ +import '../constants/environment_config.dart'; + /// API constants for network configuration class ApiConstants { // Private constructor to prevent instantiation const ApiConstants._(); - // Base URLs for different environments - static const String baseUrlDev = 'https://api-dev.example.com'; - static const String baseUrlStaging = 'https://api-staging.example.com'; - static const String baseUrlProd = 'https://api.example.com'; + // Environment-based configuration + static String get baseUrl => EnvironmentConfig.baseUrl; + static String get apiPath => EnvironmentConfig.apiPath; - // Current environment base URL - // In a real app, this would be determined by build configuration - static const String baseUrl = baseUrlDev; + // Timeout configurations (environment-specific) + static int get connectTimeout => EnvironmentConfig.connectTimeout; + static int get receiveTimeout => EnvironmentConfig.receiveTimeout; + static int get sendTimeout => EnvironmentConfig.sendTimeout; - // API versioning - static const String apiVersion = 'v1'; - static const String apiPath = '/api/$apiVersion'; - - // Timeout configurations (in milliseconds) - static const int connectTimeout = 30000; // 30 seconds - static const int receiveTimeout = 30000; // 30 seconds - static const int sendTimeout = 30000; // 30 seconds - - // Retry configurations - static const int maxRetries = 3; - static const Duration retryDelay = Duration(seconds: 1); + // Retry configurations (environment-specific) + static int get maxRetries => EnvironmentConfig.maxRetries; + static Duration get retryDelay => EnvironmentConfig.retryDelay; // Headers static const String contentType = 'application/json'; @@ -35,11 +28,12 @@ class ApiConstants { static const String bearerPrefix = 'Bearer'; static const String apiKeyHeaderKey = 'X-API-Key'; - // Common API endpoints - static const String authEndpoint = '/auth'; - static const String loginEndpoint = '$authEndpoint/login'; - static const String refreshEndpoint = '$authEndpoint/refresh'; - static const String logoutEndpoint = '$authEndpoint/logout'; + // Authentication endpoints (from environment config) + static String get authEndpoint => EnvironmentConfig.authEndpoint; + static String get loginEndpoint => EnvironmentConfig.loginEndpoint; + static String get registerEndpoint => EnvironmentConfig.registerEndpoint; + static String get refreshEndpoint => EnvironmentConfig.refreshEndpoint; + static String get logoutEndpoint => EnvironmentConfig.logoutEndpoint; static const String userEndpoint = '/user'; static const String profileEndpoint = '$userEndpoint/profile'; @@ -68,9 +62,10 @@ class ApiConstants { // Example: 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' ]; - // Development flags - static const bool enableLogging = true; - static const bool enableCertificatePinning = false; // Disabled for development + // Development flags (environment-specific) + static bool get enableLogging => EnvironmentConfig.enableLogging; + static bool get enableCertificatePinning => EnvironmentConfig.enableCertificatePinning; + static bool get enableDetailedLogging => EnvironmentConfig.enableDetailedLogging; // API rate limiting static const int maxRequestsPerMinute = 100; diff --git a/lib/core/network/auth_service_example.dart b/lib/core/network/auth_service_example.dart new file mode 100644 index 0000000..bfd3f59 --- /dev/null +++ b/lib/core/network/auth_service_example.dart @@ -0,0 +1,242 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +import '../constants/environment_config.dart'; +import 'api_constants.dart'; +import 'dio_client.dart'; + +/// Example authentication service demonstrating the localhost:3000 configuration +/// This service shows how to use the updated API configuration with the backend +class AuthServiceExample { + final DioClient _dioClient; + + AuthServiceExample(this._dioClient); + + /// Test connection to the backend + Future> testConnection() async { + try { + debugPrint('šŸ” Testing connection to ${EnvironmentConfig.baseUrl}...'); + + final response = await _dioClient.get('/'); + + return { + 'status': 'success', + 'message': 'Connected to backend successfully', + 'baseUrl': EnvironmentConfig.baseUrl, + 'statusCode': response.statusCode, + 'timestamp': DateTime.now().toIso8601String(), + }; + } catch (error) { + debugPrint('āŒ Connection test failed: $error'); + return { + 'status': 'error', + 'message': 'Failed to connect to backend', + 'error': error.toString(), + 'baseUrl': EnvironmentConfig.baseUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } + } + + /// Login with email and password + /// Calls POST localhost:3000/auth/login + Future> login({ + required String email, + required String password, + }) async { + try { + debugPrint('šŸ” Attempting login to ${EnvironmentConfig.loginUrl}...'); + + final response = await _dioClient.post( + ApiConstants.loginEndpoint, // This resolves to /auth/login + data: { + 'email': email, + 'password': password, + }, + ); + + debugPrint('āœ… Login successful'); + return { + 'status': 'success', + 'data': response.data, + 'endpoint': EnvironmentConfig.loginUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } on DioException catch (dioError) { + debugPrint('āŒ Login failed with DioException: ${dioError.message}'); + return { + 'status': 'error', + 'error': dioError.message ?? 'Unknown Dio error', + 'statusCode': dioError.response?.statusCode, + 'endpoint': EnvironmentConfig.loginUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } catch (error) { + debugPrint('āŒ Login failed: $error'); + return { + 'status': 'error', + 'error': error.toString(), + 'endpoint': EnvironmentConfig.loginUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } + } + + /// Register new user + /// Calls POST localhost:3000/auth/register + Future> register({ + required String email, + required String password, + required String name, + }) async { + try { + debugPrint('šŸ“ Attempting registration to ${EnvironmentConfig.registerUrl}...'); + + final response = await _dioClient.post( + ApiConstants.registerEndpoint, // This resolves to /auth/register + data: { + 'email': email, + 'password': password, + 'name': name, + }, + ); + + debugPrint('āœ… Registration successful'); + return { + 'status': 'success', + 'data': response.data, + 'endpoint': EnvironmentConfig.registerUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } on DioException catch (dioError) { + debugPrint('āŒ Registration failed with DioException: ${dioError.message}'); + return { + 'status': 'error', + 'error': dioError.message ?? 'Unknown Dio error', + 'statusCode': dioError.response?.statusCode, + 'endpoint': EnvironmentConfig.registerUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } catch (error) { + debugPrint('āŒ Registration failed: $error'); + return { + 'status': 'error', + 'error': error.toString(), + 'endpoint': EnvironmentConfig.registerUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } + } + + /// Refresh authentication token + /// Calls POST localhost:3000/auth/refresh + Future> refreshToken(String refreshToken) async { + try { + debugPrint('šŸ”„ Attempting token refresh to ${EnvironmentConfig.refreshUrl}...'); + + final response = await _dioClient.post( + ApiConstants.refreshEndpoint, // This resolves to /auth/refresh + data: { + 'refreshToken': refreshToken, + }, + ); + + debugPrint('āœ… Token refresh successful'); + return { + 'status': 'success', + 'data': response.data, + 'endpoint': EnvironmentConfig.refreshUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } on DioException catch (dioError) { + debugPrint('āŒ Token refresh failed with DioException: ${dioError.message}'); + return { + 'status': 'error', + 'error': dioError.message ?? 'Unknown Dio error', + 'statusCode': dioError.response?.statusCode, + 'endpoint': EnvironmentConfig.refreshUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } catch (error) { + debugPrint('āŒ Token refresh failed: $error'); + return { + 'status': 'error', + 'error': error.toString(), + 'endpoint': EnvironmentConfig.refreshUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } + } + + /// Logout user + /// Calls POST localhost:3000/auth/logout + Future> logout(String accessToken) async { + try { + debugPrint('🚪 Attempting logout to ${EnvironmentConfig.logoutUrl}...'); + + final response = await _dioClient.post( + ApiConstants.logoutEndpoint, // This resolves to /auth/logout + options: Options( + headers: { + ApiConstants.authHeaderKey: '${ApiConstants.bearerPrefix} $accessToken', + }, + ), + ); + + debugPrint('āœ… Logout successful'); + return { + 'status': 'success', + 'data': response.data, + 'endpoint': EnvironmentConfig.logoutUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } on DioException catch (dioError) { + debugPrint('āŒ Logout failed with DioException: ${dioError.message}'); + return { + 'status': 'error', + 'error': dioError.message ?? 'Unknown Dio error', + 'statusCode': dioError.response?.statusCode, + 'endpoint': EnvironmentConfig.logoutUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } catch (error) { + debugPrint('āŒ Logout failed: $error'); + return { + 'status': 'error', + 'error': error.toString(), + 'endpoint': EnvironmentConfig.logoutUrl, + 'timestamp': DateTime.now().toIso8601String(), + }; + } + } + + /// Get current environment configuration for debugging + Map getEnvironmentInfo() { + return { + 'environment': EnvironmentConfig.currentEnvironment.name, + 'baseUrl': EnvironmentConfig.baseUrl, + 'apiPath': EnvironmentConfig.apiPath, + 'fullBaseUrl': EnvironmentConfig.fullBaseUrl, + 'endpoints': { + 'login': EnvironmentConfig.loginUrl, + 'register': EnvironmentConfig.registerUrl, + 'refresh': EnvironmentConfig.refreshUrl, + 'logout': EnvironmentConfig.logoutUrl, + }, + 'timeouts': { + 'connect': EnvironmentConfig.connectTimeout, + 'receive': EnvironmentConfig.receiveTimeout, + 'send': EnvironmentConfig.sendTimeout, + }, + 'retry': { + 'maxRetries': EnvironmentConfig.maxRetries, + 'retryDelay': EnvironmentConfig.retryDelay.inMilliseconds, + }, + 'flags': { + 'enableLogging': EnvironmentConfig.enableLogging, + 'enableDetailedLogging': EnvironmentConfig.enableDetailedLogging, + 'enableCertificatePinning': EnvironmentConfig.enableCertificatePinning, + }, + }; + } +} \ No newline at end of file diff --git a/lib/core/network/test_api_connection.dart b/lib/core/network/test_api_connection.dart new file mode 100644 index 0000000..dc15a50 --- /dev/null +++ b/lib/core/network/test_api_connection.dart @@ -0,0 +1,74 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import '../constants/environment_config.dart'; + +/// Simple utility to test API connection +class ApiConnectionTest { + static Future testConnection(BuildContext context) async { + final dio = Dio(); + + try { + debugPrint('šŸ” Testing API connection...'); + debugPrint('Base URL: ${EnvironmentConfig.baseUrl}'); + debugPrint('Auth endpoint: ${EnvironmentConfig.authEndpoint}'); + + // Test basic connectivity to the auth endpoint + final response = await dio.get( + '${EnvironmentConfig.baseUrl}${EnvironmentConfig.authEndpoint}', + options: Options( + validateStatus: (status) => true, // Accept any status code + receiveTimeout: const Duration(seconds: 5), + ), + ); + + debugPrint('āœ… Connection successful!'); + debugPrint('Status code: ${response.statusCode}'); + debugPrint('Response: ${response.data}'); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('API Connected! Status: ${response.statusCode}'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + debugPrint('āŒ Connection failed: $e'); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Connection failed: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + /// Get current environment info as formatted string + static String getEnvironmentInfo() { + final info = EnvironmentConfig.debugInfo; + final buffer = StringBuffer(); + + buffer.writeln('šŸ“Š Environment Configuration:'); + buffer.writeln('================================'); + info.forEach((key, value) { + buffer.writeln('$key: $value'); + }); + buffer.writeln('================================'); + buffer.writeln('\nšŸ“ Auth Endpoints:'); + buffer.writeln('Login: ${EnvironmentConfig.loginUrl}'); + buffer.writeln('Register: ${EnvironmentConfig.registerUrl}'); + buffer.writeln('Refresh: ${EnvironmentConfig.refreshUrl}'); + buffer.writeln('Logout: ${EnvironmentConfig.logoutUrl}'); + + return buffer.toString(); + } + + /// Print environment info to console + static void printEnvironmentInfo() { + debugPrint(getEnvironmentInfo()); + } +} \ No newline at end of file diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 2642293..5e153ef 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -1,6 +1,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:flutter/foundation.dart'; +import '../constants/environment_config.dart'; import '../database/hive_service.dart'; import '../database/models/app_settings.dart'; import '../database/providers/database_providers.dart'; @@ -267,15 +268,33 @@ class AppConfiguration extends _$AppConfiguration { final buildMode = ref.watch(appBuildModeProvider); return { - 'apiTimeout': 30000, // 30 seconds + // Environment-specific API configuration + 'environment': EnvironmentConfig.currentEnvironment.name, + 'baseUrl': EnvironmentConfig.baseUrl, + 'apiPath': EnvironmentConfig.apiPath, + 'fullBaseUrl': EnvironmentConfig.fullBaseUrl, + 'connectTimeout': EnvironmentConfig.connectTimeout, + 'receiveTimeout': EnvironmentConfig.receiveTimeout, + 'sendTimeout': EnvironmentConfig.sendTimeout, + 'maxRetries': EnvironmentConfig.maxRetries, + 'retryDelay': EnvironmentConfig.retryDelay.inMilliseconds, + + // Environment-specific logging + 'enableLogging': EnvironmentConfig.enableLogging, + 'enableDetailedLogging': EnvironmentConfig.enableDetailedLogging, + 'enableCertificatePinning': EnvironmentConfig.enableCertificatePinning, + 'logLevel': EnvironmentConfig.enableDetailedLogging ? 'verbose' : 'error', + + // General configuration 'cacheTimeout': 3600000, // 1 hour in milliseconds - 'maxRetries': 3, - 'retryDelay': 1000, // 1 second - 'enableLogging': buildMode == 'debug', - 'logLevel': buildMode == 'debug' ? 'verbose' : 'error', 'maxCacheSize': 100 * 1024 * 1024, // 100MB 'imageQuality': 85, 'compressionEnabled': true, + + // Environment flags + 'isDevelopment': EnvironmentConfig.isDevelopment, + 'isStaging': EnvironmentConfig.isStaging, + 'isProduction': EnvironmentConfig.isProduction, }; } @@ -345,4 +364,69 @@ class ErrorTracker extends _$ErrorTracker { List> getRecentErrors({int count = 10}) { return state.reversed.take(count).toList(); } +} + +/// Environment debug information provider +@riverpod +Map environmentDebugInfo(EnvironmentDebugInfoRef ref) { + return EnvironmentConfig.debugInfo; +} + +/// API connectivity test provider +@riverpod +class ApiConnectivityTest extends _$ApiConnectivityTest { + @override + Future> build() async { + return _testConnectivity(); + } + + Future> _testConnectivity() async { + try { + debugPrint('šŸ” Testing API connectivity to ${EnvironmentConfig.baseUrl}...'); + + final result = { + 'baseUrl': EnvironmentConfig.baseUrl, + 'fullBaseUrl': EnvironmentConfig.fullBaseUrl, + 'loginUrl': EnvironmentConfig.loginUrl, + 'environment': EnvironmentConfig.currentEnvironment.name, + 'timestamp': DateTime.now().toIso8601String(), + 'status': 'success', + 'message': 'Configuration loaded successfully', + 'endpoints': { + 'login': EnvironmentConfig.loginUrl, + 'register': EnvironmentConfig.registerUrl, + 'refresh': EnvironmentConfig.refreshUrl, + 'logout': EnvironmentConfig.logoutUrl, + }, + 'settings': { + 'connectTimeout': EnvironmentConfig.connectTimeout, + 'receiveTimeout': EnvironmentConfig.receiveTimeout, + 'sendTimeout': EnvironmentConfig.sendTimeout, + 'maxRetries': EnvironmentConfig.maxRetries, + 'retryDelay': EnvironmentConfig.retryDelay.inMilliseconds, + 'enableLogging': EnvironmentConfig.enableLogging, + 'enableDetailedLogging': EnvironmentConfig.enableDetailedLogging, + } + }; + + debugPrint('āœ… API connectivity test completed successfully'); + return result; + } catch (error, stackTrace) { + debugPrint('āŒ API connectivity test failed: $error'); + debugPrint('Stack trace: $stackTrace'); + + return { + 'status': 'error', + 'error': error.toString(), + 'timestamp': DateTime.now().toIso8601String(), + 'baseUrl': EnvironmentConfig.baseUrl, + }; + } + } + + /// Retry connectivity test + Future retry() async { + state = const AsyncValue.loading(); + state = AsyncValue.data(await _testConnectivity()); + } } \ No newline at end of file diff --git a/lib/core/providers/app_providers.g.dart b/lib/core/providers/app_providers.g.dart index b91d4fa..7a76ff1 100644 --- a/lib/core/providers/app_providers.g.dart +++ b/lib/core/providers/app_providers.g.dart @@ -89,6 +89,25 @@ final isAppReadyProvider = AutoDisposeProvider.internal( ); typedef IsAppReadyRef = AutoDisposeProviderRef; +String _$environmentDebugInfoHash() => + r'8a936abed2173bd1539eec64231e8d970ed7382a'; + +/// Environment debug information provider +/// +/// Copied from [environmentDebugInfo]. +@ProviderFor(environmentDebugInfo) +final environmentDebugInfoProvider = + AutoDisposeProvider>.internal( + environmentDebugInfo, + name: r'environmentDebugInfoProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$environmentDebugInfoHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef EnvironmentDebugInfoRef = AutoDisposeProviderRef>; String _$appInitializationHash() => r'cdf86e2d6985c6dcee80f618bc032edf81011fc9'; /// App initialization provider @@ -142,7 +161,7 @@ final featureFlagsProvider = ); typedef _$FeatureFlags = AutoDisposeNotifier>; -String _$appConfigurationHash() => r'115fff1ac67a37ff620bbd15ea142a7211e9dc9c'; +String _$appConfigurationHash() => r'7699bbd57d15b91cd520a876454368e5b97342bd'; /// App configuration provider /// @@ -196,5 +215,24 @@ final errorTrackerProvider = AutoDisposeNotifierProvider>>; +String _$apiConnectivityTestHash() => + r'19c63d75d09ad8f95452afb1a409528fcdd5cbaa'; + +/// API connectivity test provider +/// +/// Copied from [ApiConnectivityTest]. +@ProviderFor(ApiConnectivityTest) +final apiConnectivityTestProvider = AutoDisposeAsyncNotifierProvider< + ApiConnectivityTest, Map>.internal( + ApiConnectivityTest.new, + name: r'apiConnectivityTestProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$apiConnectivityTestHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$ApiConnectivityTest = AutoDisposeAsyncNotifier>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/core/providers/network_providers.dart b/lib/core/providers/network_providers.dart index d578955..064dc49 100644 --- a/lib/core/providers/network_providers.dart +++ b/lib/core/providers/network_providers.dart @@ -2,6 +2,7 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import '../network/auth_service_example.dart'; import '../network/dio_client.dart'; import '../network/network_info.dart'; @@ -48,4 +49,16 @@ final isConnectedProvider = FutureProvider((ref) async { final networkConnectionDetailsProvider = FutureProvider((ref) async { final networkInfo = ref.watch(networkInfoProvider) as NetworkInfoImpl; return await networkInfo.getConnectionDetails(); +}); + +/// Provider for AuthServiceExample - demonstrates localhost:3000 API usage +final authServiceExampleProvider = Provider((ref) { + final dioClient = ref.watch(dioClientProvider); + return AuthServiceExample(dioClient); +}); + +/// Provider for environment information debugging +final environmentInfoProvider = Provider>((ref) { + final authService = ref.watch(authServiceExampleProvider); + return authService.getEnvironmentInfo(); }); \ No newline at end of file diff --git a/lib/features/auth/data/datasources/auth_remote_datasource.dart b/lib/features/auth/data/datasources/auth_remote_datasource.dart index 03c4da5..75aad1f 100644 --- a/lib/features/auth/data/datasources/auth_remote_datasource.dart +++ b/lib/features/auth/data/datasources/auth_remote_datasource.dart @@ -1,6 +1,8 @@ +import 'package:base_flutter/core/utils/utils.dart'; import 'package:dio/dio.dart'; -import '../../../../core/errors/exceptions.dart'; import '../../../../core/network/dio_client.dart'; +import '../../../../core/network/api_constants.dart'; +import '../../../../core/services/api_service.dart'; import '../models/user_model.dart'; abstract class AuthRemoteDataSource { @@ -34,58 +36,43 @@ abstract class AuthRemoteDataSource { }); } -class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { - final DioClient dioClient; - - AuthRemoteDataSourceImpl({required this.dioClient}); +class AuthRemoteDataSourceImpl extends BaseApiService implements AuthRemoteDataSource { + AuthRemoteDataSourceImpl({required DioClient dioClient}) : super(dioClient); @override Future login({ required String email, required String password, }) async { - try { - // Using JSONPlaceholder as a mock API - // In real app, this would be your actual auth endpoint - final response = await dioClient.dio.post( - 'https://jsonplaceholder.typicode.com/posts', + return executeRequest( + () => dioClient.post( + ApiConstants.loginEndpoint, data: { 'email': email, 'password': password, }, - ); + ), + (data) { + final responseData = data as DataMap; - // Mock validation - accept any email/password for demo - // In real app, the server would validate credentials - if (email.isEmpty || password.isEmpty) { - throw const ServerException('Invalid credentials'); - } - - // Mock response for demonstration - // In real app, parse actual API response - final mockUser = { - 'id': '1', - 'email': email, - 'name': email.split('@').first, - 'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}', - 'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(), - }; - - return UserModel.fromJson(mockUser); - } on DioException catch (e) { - if (e.response?.statusCode == 401) { - throw const ServerException('Invalid credentials'); - } else if (e.response?.statusCode == 404) { - throw const ServerException('User not found'); - } else { - throw ServerException(e.message ?? 'Login failed'); - } - } catch (e) { - if (e.toString().contains('Invalid credentials')) { - rethrow; - } - throw ServerException(e.toString()); - } + // If the backend returns user data in a 'user' field + if (responseData.containsKey('user')) { + final userData = DataMap.from(responseData['user']); + // Add token if it's returned separately + if (responseData.containsKey('token')) { + userData['token'] = responseData['token']; + } + if (responseData.containsKey('tokenExpiry')) { + userData['tokenExpiry'] = responseData['tokenExpiry']; + } + return UserModel.fromJson(userData); + } + // If the backend returns everything in the root + else { + return UserModel.fromJson(responseData); + } + }, + ); } @override @@ -94,73 +81,76 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { required String password, required String name, }) async { - try { - // Mock API call - final response = await dioClient.dio.post( - 'https://jsonplaceholder.typicode.com/users', + return executeRequest( + () => dioClient.post( + ApiConstants.registerEndpoint, data: { 'email': email, 'password': password, 'name': name, }, - ); + ), + (data) { + final responseData = data as Map; - // Mock response - final mockUser = { - 'id': DateTime.now().millisecondsSinceEpoch.toString(), - 'email': email, - 'name': name, - 'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}', - 'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(), - }; - - return UserModel.fromJson(mockUser); - } on DioException catch (e) { - if (e.response?.statusCode == 409) { - throw const ServerException('Email already exists'); - } else { - throw ServerException(e.message ?? 'Registration failed'); - } - } catch (e) { - throw ServerException(e.toString()); - } + // If the backend returns user data in a 'user' field + if (responseData.containsKey('user')) { + final userData = Map.from(responseData['user']); + // Add token if it's returned separately + if (responseData.containsKey('token')) { + userData['token'] = responseData['token']; + } + if (responseData.containsKey('tokenExpiry')) { + userData['tokenExpiry'] = responseData['tokenExpiry']; + } + return UserModel.fromJson(userData); + } + // If the backend returns everything in the root + else { + return UserModel.fromJson(responseData); + } + }, + ); } @override Future logout() async { - try { - // Mock API call - await dioClient.dio.post('https://jsonplaceholder.typicode.com/posts'); - // In real app, you might call a logout endpoint to invalidate token - } catch (e) { - throw ServerException(e.toString()); - } + await executeRequest( + () => dioClient.post(ApiConstants.logoutEndpoint), + (_) {}, // No return value needed for logout + ); } @override Future refreshToken(String token) async { - try { - // Mock API call - final response = await dioClient.dio.post( - 'https://jsonplaceholder.typicode.com/users', + return executeRequest( + () => dioClient.post( + ApiConstants.refreshEndpoint, options: Options( headers: {'Authorization': 'Bearer $token'}, ), - ); + ), + (data) { + final responseData = data as Map; - // Mock response - final mockUser = { - 'id': '1', - 'email': 'user@example.com', - 'name': 'User', - 'token': 'refreshed_token_${DateTime.now().millisecondsSinceEpoch}', - 'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(), - }; - - return UserModel.fromJson(mockUser); - } catch (e) { - throw ServerException(e.toString()); - } + // If the backend returns user data in a 'user' field + if (responseData.containsKey('user')) { + final userData = Map.from(responseData['user']); + // Add new token if it's returned separately + if (responseData.containsKey('token')) { + userData['token'] = responseData['token']; + } + if (responseData.containsKey('tokenExpiry')) { + userData['tokenExpiry'] = responseData['tokenExpiry']; + } + return UserModel.fromJson(userData); + } + // If the backend returns everything in the root + else { + return UserModel.fromJson(responseData); + } + }, + ); } @override @@ -168,30 +158,18 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { required String name, String? avatarUrl, }) async { - try { - // Mock API call - final response = await dioClient.dio.put( - 'https://jsonplaceholder.typicode.com/users/1', + // For now, keeping this as mock since profile endpoints are not specified + // When you have the actual endpoint, update this to use executeRequest + return executeRequest( + () => dioClient.put( + '/user/profile', // Replace with actual endpoint when available data: { 'name': name, 'avatarUrl': avatarUrl, }, - ); - - // Mock response - final mockUser = { - 'id': '1', - 'email': 'user@example.com', - 'name': name, - 'avatarUrl': avatarUrl, - 'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}', - 'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(), - }; - - return UserModel.fromJson(mockUser); - } catch (e) { - throw ServerException(e.toString()); - } + ), + (data) => UserModel.fromJson(data as Map), + ); } @override @@ -199,34 +177,30 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { required String oldPassword, required String newPassword, }) async { - try { - // Mock API call - await dioClient.dio.post( - 'https://jsonplaceholder.typicode.com/posts', + await executeRequest( + () => dioClient.post( + '/auth/change-password', // Replace with actual endpoint when available data: { 'oldPassword': oldPassword, 'newPassword': newPassword, }, - ); - } catch (e) { - throw ServerException(e.toString()); - } + ), + (_) {}, // No return value needed + ); } @override Future resetPassword({ required String email, }) async { - try { - // Mock API call - await dioClient.dio.post( - 'https://jsonplaceholder.typicode.com/posts', + await executeRequest( + () => dioClient.post( + '/auth/reset-password', // Replace with actual endpoint when available data: { 'email': email, }, - ); - } catch (e) { - throw ServerException(e.toString()); - } + ), + (_) {}, // No return value needed + ); } } \ No newline at end of file