fix api
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
// Barrel export file for constants
|
// Barrel export file for constants
|
||||||
export 'app_constants.dart';
|
export 'app_constants.dart';
|
||||||
|
export 'environment_config.dart';
|
||||||
export 'storage_constants.dart';
|
export 'storage_constants.dart';
|
||||||
140
lib/core/constants/environment_config.dart
Normal file
140
lib/core/constants/environment_config.dart
Normal file
@@ -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<String, dynamic> 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
377
lib/core/debug/api_debug_page.dart
Normal file
377
lib/core/debug/api_debug_page.dart
Normal file
@@ -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<String, dynamic> 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<String, dynamic> 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<Map<String, dynamic>> 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<String, dynamic> 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,22 @@
|
|||||||
|
import '../constants/environment_config.dart';
|
||||||
|
|
||||||
/// API constants for network configuration
|
/// API constants for network configuration
|
||||||
class ApiConstants {
|
class ApiConstants {
|
||||||
// Private constructor to prevent instantiation
|
// Private constructor to prevent instantiation
|
||||||
const ApiConstants._();
|
const ApiConstants._();
|
||||||
|
|
||||||
// Base URLs for different environments
|
// Environment-based configuration
|
||||||
static const String baseUrlDev = 'https://api-dev.example.com';
|
static String get baseUrl => EnvironmentConfig.baseUrl;
|
||||||
static const String baseUrlStaging = 'https://api-staging.example.com';
|
static String get apiPath => EnvironmentConfig.apiPath;
|
||||||
static const String baseUrlProd = 'https://api.example.com';
|
|
||||||
|
|
||||||
// Current environment base URL
|
// Timeout configurations (environment-specific)
|
||||||
// In a real app, this would be determined by build configuration
|
static int get connectTimeout => EnvironmentConfig.connectTimeout;
|
||||||
static const String baseUrl = baseUrlDev;
|
static int get receiveTimeout => EnvironmentConfig.receiveTimeout;
|
||||||
|
static int get sendTimeout => EnvironmentConfig.sendTimeout;
|
||||||
|
|
||||||
// API versioning
|
// Retry configurations (environment-specific)
|
||||||
static const String apiVersion = 'v1';
|
static int get maxRetries => EnvironmentConfig.maxRetries;
|
||||||
static const String apiPath = '/api/$apiVersion';
|
static Duration get retryDelay => EnvironmentConfig.retryDelay;
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
static const String contentType = 'application/json';
|
static const String contentType = 'application/json';
|
||||||
@@ -35,11 +28,12 @@ class ApiConstants {
|
|||||||
static const String bearerPrefix = 'Bearer';
|
static const String bearerPrefix = 'Bearer';
|
||||||
static const String apiKeyHeaderKey = 'X-API-Key';
|
static const String apiKeyHeaderKey = 'X-API-Key';
|
||||||
|
|
||||||
// Common API endpoints
|
// Authentication endpoints (from environment config)
|
||||||
static const String authEndpoint = '/auth';
|
static String get authEndpoint => EnvironmentConfig.authEndpoint;
|
||||||
static const String loginEndpoint = '$authEndpoint/login';
|
static String get loginEndpoint => EnvironmentConfig.loginEndpoint;
|
||||||
static const String refreshEndpoint = '$authEndpoint/refresh';
|
static String get registerEndpoint => EnvironmentConfig.registerEndpoint;
|
||||||
static const String logoutEndpoint = '$authEndpoint/logout';
|
static String get refreshEndpoint => EnvironmentConfig.refreshEndpoint;
|
||||||
|
static String get logoutEndpoint => EnvironmentConfig.logoutEndpoint;
|
||||||
static const String userEndpoint = '/user';
|
static const String userEndpoint = '/user';
|
||||||
static const String profileEndpoint = '$userEndpoint/profile';
|
static const String profileEndpoint = '$userEndpoint/profile';
|
||||||
|
|
||||||
@@ -68,9 +62,10 @@ class ApiConstants {
|
|||||||
// Example: 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
// Example: 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
||||||
];
|
];
|
||||||
|
|
||||||
// Development flags
|
// Development flags (environment-specific)
|
||||||
static const bool enableLogging = true;
|
static bool get enableLogging => EnvironmentConfig.enableLogging;
|
||||||
static const bool enableCertificatePinning = false; // Disabled for development
|
static bool get enableCertificatePinning => EnvironmentConfig.enableCertificatePinning;
|
||||||
|
static bool get enableDetailedLogging => EnvironmentConfig.enableDetailedLogging;
|
||||||
|
|
||||||
// API rate limiting
|
// API rate limiting
|
||||||
static const int maxRequestsPerMinute = 100;
|
static const int maxRequestsPerMinute = 100;
|
||||||
|
|||||||
242
lib/core/network/auth_service_example.dart
Normal file
242
lib/core/network/auth_service_example.dart
Normal file
@@ -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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<String, dynamic> 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
74
lib/core/network/test_api_connection.dart
Normal file
74
lib/core/network/test_api_connection.dart
Normal file
@@ -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<void> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../constants/environment_config.dart';
|
||||||
import '../database/hive_service.dart';
|
import '../database/hive_service.dart';
|
||||||
import '../database/models/app_settings.dart';
|
import '../database/models/app_settings.dart';
|
||||||
import '../database/providers/database_providers.dart';
|
import '../database/providers/database_providers.dart';
|
||||||
@@ -267,15 +268,33 @@ class AppConfiguration extends _$AppConfiguration {
|
|||||||
final buildMode = ref.watch(appBuildModeProvider);
|
final buildMode = ref.watch(appBuildModeProvider);
|
||||||
|
|
||||||
return {
|
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
|
'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
|
'maxCacheSize': 100 * 1024 * 1024, // 100MB
|
||||||
'imageQuality': 85,
|
'imageQuality': 85,
|
||||||
'compressionEnabled': true,
|
'compressionEnabled': true,
|
||||||
|
|
||||||
|
// Environment flags
|
||||||
|
'isDevelopment': EnvironmentConfig.isDevelopment,
|
||||||
|
'isStaging': EnvironmentConfig.isStaging,
|
||||||
|
'isProduction': EnvironmentConfig.isProduction,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,4 +364,69 @@ class ErrorTracker extends _$ErrorTracker {
|
|||||||
List<Map<String, dynamic>> getRecentErrors({int count = 10}) {
|
List<Map<String, dynamic>> getRecentErrors({int count = 10}) {
|
||||||
return state.reversed.take(count).toList();
|
return state.reversed.take(count).toList();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Environment debug information provider
|
||||||
|
@riverpod
|
||||||
|
Map<String, dynamic> environmentDebugInfo(EnvironmentDebugInfoRef ref) {
|
||||||
|
return EnvironmentConfig.debugInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// API connectivity test provider
|
||||||
|
@riverpod
|
||||||
|
class ApiConnectivityTest extends _$ApiConnectivityTest {
|
||||||
|
@override
|
||||||
|
Future<Map<String, dynamic>> build() async {
|
||||||
|
return _testConnectivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> _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<void> retry() async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
state = AsyncValue.data(await _testConnectivity());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -89,6 +89,25 @@ final isAppReadyProvider = AutoDisposeProvider<bool>.internal(
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef IsAppReadyRef = AutoDisposeProviderRef<bool>;
|
typedef IsAppReadyRef = AutoDisposeProviderRef<bool>;
|
||||||
|
String _$environmentDebugInfoHash() =>
|
||||||
|
r'8a936abed2173bd1539eec64231e8d970ed7382a';
|
||||||
|
|
||||||
|
/// Environment debug information provider
|
||||||
|
///
|
||||||
|
/// Copied from [environmentDebugInfo].
|
||||||
|
@ProviderFor(environmentDebugInfo)
|
||||||
|
final environmentDebugInfoProvider =
|
||||||
|
AutoDisposeProvider<Map<String, dynamic>>.internal(
|
||||||
|
environmentDebugInfo,
|
||||||
|
name: r'environmentDebugInfoProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$environmentDebugInfoHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef EnvironmentDebugInfoRef = AutoDisposeProviderRef<Map<String, dynamic>>;
|
||||||
String _$appInitializationHash() => r'cdf86e2d6985c6dcee80f618bc032edf81011fc9';
|
String _$appInitializationHash() => r'cdf86e2d6985c6dcee80f618bc032edf81011fc9';
|
||||||
|
|
||||||
/// App initialization provider
|
/// App initialization provider
|
||||||
@@ -142,7 +161,7 @@ final featureFlagsProvider =
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef _$FeatureFlags = AutoDisposeNotifier<Map<String, bool>>;
|
typedef _$FeatureFlags = AutoDisposeNotifier<Map<String, bool>>;
|
||||||
String _$appConfigurationHash() => r'115fff1ac67a37ff620bbd15ea142a7211e9dc9c';
|
String _$appConfigurationHash() => r'7699bbd57d15b91cd520a876454368e5b97342bd';
|
||||||
|
|
||||||
/// App configuration provider
|
/// App configuration provider
|
||||||
///
|
///
|
||||||
@@ -196,5 +215,24 @@ final errorTrackerProvider = AutoDisposeNotifierProvider<ErrorTracker,
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef _$ErrorTracker = AutoDisposeNotifier<List<Map<String, dynamic>>>;
|
typedef _$ErrorTracker = AutoDisposeNotifier<List<Map<String, dynamic>>>;
|
||||||
|
String _$apiConnectivityTestHash() =>
|
||||||
|
r'19c63d75d09ad8f95452afb1a409528fcdd5cbaa';
|
||||||
|
|
||||||
|
/// API connectivity test provider
|
||||||
|
///
|
||||||
|
/// Copied from [ApiConnectivityTest].
|
||||||
|
@ProviderFor(ApiConnectivityTest)
|
||||||
|
final apiConnectivityTestProvider = AutoDisposeAsyncNotifierProvider<
|
||||||
|
ApiConnectivityTest, Map<String, dynamic>>.internal(
|
||||||
|
ApiConnectivityTest.new,
|
||||||
|
name: r'apiConnectivityTestProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$apiConnectivityTestHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$ApiConnectivityTest = AutoDisposeAsyncNotifier<Map<String, dynamic>>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:connectivity_plus/connectivity_plus.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
|
import '../network/auth_service_example.dart';
|
||||||
import '../network/dio_client.dart';
|
import '../network/dio_client.dart';
|
||||||
import '../network/network_info.dart';
|
import '../network/network_info.dart';
|
||||||
|
|
||||||
@@ -48,4 +49,16 @@ final isConnectedProvider = FutureProvider<bool>((ref) async {
|
|||||||
final networkConnectionDetailsProvider = FutureProvider((ref) async {
|
final networkConnectionDetailsProvider = FutureProvider((ref) async {
|
||||||
final networkInfo = ref.watch(networkInfoProvider) as NetworkInfoImpl;
|
final networkInfo = ref.watch(networkInfoProvider) as NetworkInfoImpl;
|
||||||
return await networkInfo.getConnectionDetails();
|
return await networkInfo.getConnectionDetails();
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider for AuthServiceExample - demonstrates localhost:3000 API usage
|
||||||
|
final authServiceExampleProvider = Provider<AuthServiceExample>((ref) {
|
||||||
|
final dioClient = ref.watch(dioClientProvider);
|
||||||
|
return AuthServiceExample(dioClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider for environment information debugging
|
||||||
|
final environmentInfoProvider = Provider<Map<String, dynamic>>((ref) {
|
||||||
|
final authService = ref.watch(authServiceExampleProvider);
|
||||||
|
return authService.getEnvironmentInfo();
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import 'package:base_flutter/core/utils/utils.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import '../../../../core/errors/exceptions.dart';
|
|
||||||
import '../../../../core/network/dio_client.dart';
|
import '../../../../core/network/dio_client.dart';
|
||||||
|
import '../../../../core/network/api_constants.dart';
|
||||||
|
import '../../../../core/services/api_service.dart';
|
||||||
import '../models/user_model.dart';
|
import '../models/user_model.dart';
|
||||||
|
|
||||||
abstract class AuthRemoteDataSource {
|
abstract class AuthRemoteDataSource {
|
||||||
@@ -34,58 +36,43 @@ abstract class AuthRemoteDataSource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
class AuthRemoteDataSourceImpl extends BaseApiService implements AuthRemoteDataSource {
|
||||||
final DioClient dioClient;
|
AuthRemoteDataSourceImpl({required DioClient dioClient}) : super(dioClient);
|
||||||
|
|
||||||
AuthRemoteDataSourceImpl({required this.dioClient});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<UserModel> login({
|
Future<UserModel> login({
|
||||||
required String email,
|
required String email,
|
||||||
required String password,
|
required String password,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
return executeRequest(
|
||||||
// Using JSONPlaceholder as a mock API
|
() => dioClient.post(
|
||||||
// In real app, this would be your actual auth endpoint
|
ApiConstants.loginEndpoint,
|
||||||
final response = await dioClient.dio.post(
|
|
||||||
'https://jsonplaceholder.typicode.com/posts',
|
|
||||||
data: {
|
data: {
|
||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
|
(data) {
|
||||||
|
final responseData = data as DataMap;
|
||||||
|
|
||||||
// Mock validation - accept any email/password for demo
|
// If the backend returns user data in a 'user' field
|
||||||
// In real app, the server would validate credentials
|
if (responseData.containsKey('user')) {
|
||||||
if (email.isEmpty || password.isEmpty) {
|
final userData = DataMap.from(responseData['user']);
|
||||||
throw const ServerException('Invalid credentials');
|
// Add token if it's returned separately
|
||||||
}
|
if (responseData.containsKey('token')) {
|
||||||
|
userData['token'] = responseData['token'];
|
||||||
// Mock response for demonstration
|
}
|
||||||
// In real app, parse actual API response
|
if (responseData.containsKey('tokenExpiry')) {
|
||||||
final mockUser = {
|
userData['tokenExpiry'] = responseData['tokenExpiry'];
|
||||||
'id': '1',
|
}
|
||||||
'email': email,
|
return UserModel.fromJson(userData);
|
||||||
'name': email.split('@').first,
|
}
|
||||||
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
|
// If the backend returns everything in the root
|
||||||
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
|
else {
|
||||||
};
|
return UserModel.fromJson(responseData);
|
||||||
|
}
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -94,73 +81,76 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
|||||||
required String password,
|
required String password,
|
||||||
required String name,
|
required String name,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
return executeRequest(
|
||||||
// Mock API call
|
() => dioClient.post(
|
||||||
final response = await dioClient.dio.post(
|
ApiConstants.registerEndpoint,
|
||||||
'https://jsonplaceholder.typicode.com/users',
|
|
||||||
data: {
|
data: {
|
||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
'name': name,
|
'name': name,
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
|
(data) {
|
||||||
|
final responseData = data as Map<String, dynamic>;
|
||||||
|
|
||||||
// Mock response
|
// If the backend returns user data in a 'user' field
|
||||||
final mockUser = {
|
if (responseData.containsKey('user')) {
|
||||||
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
final userData = Map<String, dynamic>.from(responseData['user']);
|
||||||
'email': email,
|
// Add token if it's returned separately
|
||||||
'name': name,
|
if (responseData.containsKey('token')) {
|
||||||
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
|
userData['token'] = responseData['token'];
|
||||||
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
|
}
|
||||||
};
|
if (responseData.containsKey('tokenExpiry')) {
|
||||||
|
userData['tokenExpiry'] = responseData['tokenExpiry'];
|
||||||
return UserModel.fromJson(mockUser);
|
}
|
||||||
} on DioException catch (e) {
|
return UserModel.fromJson(userData);
|
||||||
if (e.response?.statusCode == 409) {
|
}
|
||||||
throw const ServerException('Email already exists');
|
// If the backend returns everything in the root
|
||||||
} else {
|
else {
|
||||||
throw ServerException(e.message ?? 'Registration failed');
|
return UserModel.fromJson(responseData);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
},
|
||||||
throw ServerException(e.toString());
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
try {
|
await executeRequest(
|
||||||
// Mock API call
|
() => dioClient.post(ApiConstants.logoutEndpoint),
|
||||||
await dioClient.dio.post('https://jsonplaceholder.typicode.com/posts');
|
(_) {}, // No return value needed for logout
|
||||||
// In real app, you might call a logout endpoint to invalidate token
|
);
|
||||||
} catch (e) {
|
|
||||||
throw ServerException(e.toString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<UserModel> refreshToken(String token) async {
|
Future<UserModel> refreshToken(String token) async {
|
||||||
try {
|
return executeRequest(
|
||||||
// Mock API call
|
() => dioClient.post(
|
||||||
final response = await dioClient.dio.post(
|
ApiConstants.refreshEndpoint,
|
||||||
'https://jsonplaceholder.typicode.com/users',
|
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {'Authorization': 'Bearer $token'},
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
(data) {
|
||||||
|
final responseData = data as Map<String, dynamic>;
|
||||||
|
|
||||||
// Mock response
|
// If the backend returns user data in a 'user' field
|
||||||
final mockUser = {
|
if (responseData.containsKey('user')) {
|
||||||
'id': '1',
|
final userData = Map<String, dynamic>.from(responseData['user']);
|
||||||
'email': 'user@example.com',
|
// Add new token if it's returned separately
|
||||||
'name': 'User',
|
if (responseData.containsKey('token')) {
|
||||||
'token': 'refreshed_token_${DateTime.now().millisecondsSinceEpoch}',
|
userData['token'] = responseData['token'];
|
||||||
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
|
}
|
||||||
};
|
if (responseData.containsKey('tokenExpiry')) {
|
||||||
|
userData['tokenExpiry'] = responseData['tokenExpiry'];
|
||||||
return UserModel.fromJson(mockUser);
|
}
|
||||||
} catch (e) {
|
return UserModel.fromJson(userData);
|
||||||
throw ServerException(e.toString());
|
}
|
||||||
}
|
// If the backend returns everything in the root
|
||||||
|
else {
|
||||||
|
return UserModel.fromJson(responseData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -168,30 +158,18 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
|||||||
required String name,
|
required String name,
|
||||||
String? avatarUrl,
|
String? avatarUrl,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
// For now, keeping this as mock since profile endpoints are not specified
|
||||||
// Mock API call
|
// When you have the actual endpoint, update this to use executeRequest
|
||||||
final response = await dioClient.dio.put(
|
return executeRequest(
|
||||||
'https://jsonplaceholder.typicode.com/users/1',
|
() => dioClient.put(
|
||||||
|
'/user/profile', // Replace with actual endpoint when available
|
||||||
data: {
|
data: {
|
||||||
'name': name,
|
'name': name,
|
||||||
'avatarUrl': avatarUrl,
|
'avatarUrl': avatarUrl,
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
|
(data) => UserModel.fromJson(data as Map<String, dynamic>),
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -199,34 +177,30 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
|||||||
required String oldPassword,
|
required String oldPassword,
|
||||||
required String newPassword,
|
required String newPassword,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
await executeRequest(
|
||||||
// Mock API call
|
() => dioClient.post(
|
||||||
await dioClient.dio.post(
|
'/auth/change-password', // Replace with actual endpoint when available
|
||||||
'https://jsonplaceholder.typicode.com/posts',
|
|
||||||
data: {
|
data: {
|
||||||
'oldPassword': oldPassword,
|
'oldPassword': oldPassword,
|
||||||
'newPassword': newPassword,
|
'newPassword': newPassword,
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
} catch (e) {
|
(_) {}, // No return value needed
|
||||||
throw ServerException(e.toString());
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> resetPassword({
|
Future<void> resetPassword({
|
||||||
required String email,
|
required String email,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
await executeRequest(
|
||||||
// Mock API call
|
() => dioClient.post(
|
||||||
await dioClient.dio.post(
|
'/auth/reset-password', // Replace with actual endpoint when available
|
||||||
'https://jsonplaceholder.typicode.com/posts',
|
|
||||||
data: {
|
data: {
|
||||||
'email': email,
|
'email': email,
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
} catch (e) {
|
(_) {}, // No return value needed
|
||||||
throw ServerException(e.toString());
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user