fill
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:dio/dio.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../errors/exceptions.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
/// API client for making HTTP requests using Dio
|
||||
/// Includes token management, request/response logging, and error handling
|
||||
class ApiClient {
|
||||
late final Dio _dio;
|
||||
final SecureStorage _secureStorage;
|
||||
|
||||
ApiClient() {
|
||||
// Callback for 401 unauthorized errors (e.g., to navigate to login)
|
||||
void Function()? onUnauthorized;
|
||||
|
||||
ApiClient(this._secureStorage, {this.onUnauthorized}) {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: AppConstants.apiBaseUrl,
|
||||
@@ -20,21 +27,45 @@ class ApiClient {
|
||||
),
|
||||
);
|
||||
|
||||
// Add request/response interceptors for logging and error handling
|
||||
_setupInterceptors();
|
||||
}
|
||||
|
||||
/// Setup all Dio interceptors
|
||||
void _setupInterceptors() {
|
||||
// Request interceptor - adds auth token and logs requests
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
// Log request details in debug mode
|
||||
handler.next(options);
|
||||
onRequest: (options, handler) async {
|
||||
// Add AccessToken header if available
|
||||
final token = await _secureStorage.getAccessToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['AccessToken'] = token;
|
||||
}
|
||||
|
||||
// Add AppID header
|
||||
options.headers['AppID'] = AppConstants.appId;
|
||||
|
||||
// Log request in debug mode
|
||||
_logRequest(options);
|
||||
|
||||
return handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
// Log response details in debug mode
|
||||
handler.next(response);
|
||||
// Log response in debug mode
|
||||
_logResponse(response);
|
||||
|
||||
return handler.next(response);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
// Handle different types of errors
|
||||
_handleDioError(error);
|
||||
handler.next(error);
|
||||
onError: (error, handler) async {
|
||||
// Log error in debug mode
|
||||
_logError(error);
|
||||
|
||||
// Handle 401 unauthorized errors
|
||||
if (error.response?.statusCode == 401) {
|
||||
await _handle401Error();
|
||||
}
|
||||
|
||||
return handler.next(error);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -122,6 +153,23 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle 401 Unauthorized errors
|
||||
Future<void> _handle401Error() async {
|
||||
developer.log(
|
||||
'401 Unauthorized - Clearing tokens and triggering logout',
|
||||
name: 'ApiClient',
|
||||
level: 900,
|
||||
);
|
||||
|
||||
// Clear all tokens from secure storage
|
||||
await _secureStorage.clearTokens();
|
||||
|
||||
// Trigger the unauthorized callback (e.g., navigate to login)
|
||||
if (onUnauthorized != null) {
|
||||
onUnauthorized!();
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle Dio errors and convert them to custom exceptions
|
||||
Exception _handleDioError(DioException error) {
|
||||
switch (error.type) {
|
||||
@@ -132,13 +180,47 @@ class ApiClient {
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = error.response?.statusCode;
|
||||
final message = error.response?.data?['message'] ?? 'Server error occurred';
|
||||
|
||||
// Try to extract error message from API response
|
||||
String message = 'Server error occurred';
|
||||
|
||||
if (error.response?.data is Map) {
|
||||
final data = error.response?.data as Map<String, dynamic>;
|
||||
|
||||
// Check for standard API error format
|
||||
if (data['Errors'] != null && data['Errors'] is List && (data['Errors'] as List).isNotEmpty) {
|
||||
message = (data['Errors'] as List).first.toString();
|
||||
} else if (data['message'] != null) {
|
||||
message = data['message'].toString();
|
||||
} else if (data['error'] != null) {
|
||||
message = data['error'].toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (statusCode != null) {
|
||||
if (statusCode >= 400 && statusCode < 500) {
|
||||
return ServerException('Client error: $message (Status: $statusCode)');
|
||||
} else if (statusCode >= 500) {
|
||||
return ServerException('Server error: $message (Status: $statusCode)');
|
||||
// Handle specific status codes
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
return const ServerException('Unauthorized. Please login again.', code: '401');
|
||||
case 403:
|
||||
return const ServerException('Forbidden. You do not have permission.', code: '403');
|
||||
case 404:
|
||||
return ServerException('Resource not found: $message', code: '404');
|
||||
case 422:
|
||||
return ServerException('Validation error: $message', code: '422');
|
||||
case 429:
|
||||
return const ServerException('Too many requests. Please try again later.', code: '429');
|
||||
case 500:
|
||||
case 501:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
return ServerException('Server error: $message (Status: $statusCode)', code: statusCode.toString());
|
||||
default:
|
||||
if (statusCode >= 400 && statusCode < 500) {
|
||||
return ServerException('Client error: $message (Status: $statusCode)', code: statusCode.toString());
|
||||
}
|
||||
return ServerException('HTTP error: $message (Status: $statusCode)', code: statusCode.toString());
|
||||
}
|
||||
}
|
||||
return ServerException('HTTP error: $message');
|
||||
@@ -153,23 +235,141 @@ class ApiClient {
|
||||
return const NetworkException('Certificate verification failed');
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
default:
|
||||
return ServerException('An unexpected error occurred: ${error.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Add authorization header
|
||||
void addAuthorizationHeader(String token) {
|
||||
_dio.options.headers['Authorization'] = 'Bearer $token';
|
||||
/// Log request details
|
||||
void _logRequest(RequestOptions options) {
|
||||
developer.log(
|
||||
'REQUEST[${options.method}] => ${options.uri}',
|
||||
name: 'ApiClient',
|
||||
level: 800,
|
||||
);
|
||||
|
||||
if (options.headers.isNotEmpty) {
|
||||
developer.log(
|
||||
'Headers: ${_sanitizeHeaders(options.headers)}',
|
||||
name: 'ApiClient',
|
||||
level: 800,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.queryParameters.isNotEmpty) {
|
||||
developer.log(
|
||||
'Query Params: ${options.queryParameters}',
|
||||
name: 'ApiClient',
|
||||
level: 800,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.data != null) {
|
||||
developer.log(
|
||||
'Body: ${options.data}',
|
||||
name: 'ApiClient',
|
||||
level: 800,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove authorization header
|
||||
void removeAuthorizationHeader() {
|
||||
_dio.options.headers.remove('Authorization');
|
||||
/// Log response details
|
||||
void _logResponse(Response response) {
|
||||
developer.log(
|
||||
'RESPONSE[${response.statusCode}] => ${response.requestOptions.uri}',
|
||||
name: 'ApiClient',
|
||||
level: 800,
|
||||
);
|
||||
|
||||
developer.log(
|
||||
'Data: ${response.data}',
|
||||
name: 'ApiClient',
|
||||
level: 800,
|
||||
);
|
||||
}
|
||||
|
||||
/// Log error details
|
||||
void _logError(DioException error) {
|
||||
developer.log(
|
||||
'ERROR[${error.response?.statusCode}] => ${error.requestOptions.uri}',
|
||||
name: 'ApiClient',
|
||||
level: 1000,
|
||||
error: error,
|
||||
);
|
||||
|
||||
if (error.response?.data != null) {
|
||||
developer.log(
|
||||
'Error Data: ${error.response?.data}',
|
||||
name: 'ApiClient',
|
||||
level: 1000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitize headers to hide sensitive data in logs
|
||||
Map<String, dynamic> _sanitizeHeaders(Map<String, dynamic> headers) {
|
||||
final sanitized = Map<String, dynamic>.from(headers);
|
||||
|
||||
// Hide Authorization token
|
||||
if (sanitized.containsKey('Authorization')) {
|
||||
sanitized['Authorization'] = '***REDACTED***';
|
||||
}
|
||||
|
||||
// Hide any other sensitive headers
|
||||
final sensitiveKeys = ['api-key', 'x-api-key', 'token'];
|
||||
for (final key in sensitiveKeys) {
|
||||
if (sanitized.containsKey(key)) {
|
||||
sanitized[key] = '***REDACTED***';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/// Get the Dio instance (use carefully, prefer using the methods above)
|
||||
Dio get dio => _dio;
|
||||
|
||||
/// Update base URL (useful for different environments)
|
||||
void updateBaseUrl(String newBaseUrl) {
|
||||
_dio.options.baseUrl = newBaseUrl;
|
||||
developer.log(
|
||||
'Base URL updated to: $newBaseUrl',
|
||||
name: 'ApiClient',
|
||||
level: 800,
|
||||
);
|
||||
}
|
||||
|
||||
/// Test connection to the API
|
||||
Future<bool> testConnection() async {
|
||||
try {
|
||||
final response = await _dio.get('/health');
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Connection test failed: $e',
|
||||
name: 'ApiClient',
|
||||
level: 900,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current access token
|
||||
Future<String?> getAccessToken() async {
|
||||
return await _secureStorage.getAccessToken();
|
||||
}
|
||||
|
||||
/// Check if user is authenticated
|
||||
Future<bool> isAuthenticated() async {
|
||||
return await _secureStorage.isAuthenticated();
|
||||
}
|
||||
|
||||
/// Clear all authentication data
|
||||
Future<void> clearAuth() async {
|
||||
await _secureStorage.clearAll();
|
||||
developer.log(
|
||||
'Authentication data cleared',
|
||||
name: 'ApiClient',
|
||||
level: 800,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user