2025-12-06 17:34:01 +05:30

256 lines
7.9 KiB
Dart

// lib/helpers/services/http_client.dart
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/api_endpoints.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/utils/encryption_helper.dart';
/// Centralized HTTP client with automatic token management, encryption,
/// and retry logic for OnFieldWork.com API communication.
class HttpClient {
static const Duration _timeout = Duration(seconds: 60);
static const Duration _tokenRefreshThreshold = Duration(minutes: 2);
final http.Client _client = http.Client();
bool _isRefreshing = false;
/// Private constructor - use singleton instance
HttpClient._();
static final HttpClient instance = HttpClient._();
/// Clean headers with JWT token
Map<String, String> _defaultHeaders(String token) => {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
};
/// Ensures valid token with proactive refresh
Future<String?> _getValidToken() async {
String? token = await LocalStorage.getJwtToken();
if (token == null) {
logSafe("No JWT token available", level: LogLevel.error);
await LocalStorage.logout();
return null;
}
try {
if (JwtDecoder.isExpired(token) ||
JwtDecoder.getExpirationDate(token).difference(DateTime.now()) <
_tokenRefreshThreshold) {
logSafe("Token expired/expiring soon. Refreshing...",
level: LogLevel.info);
if (!await _refreshTokenIfPossible()) {
logSafe("Token refresh failed. Logging out.", level: LogLevel.error);
await LocalStorage.logout();
return null;
}
token = await LocalStorage.getJwtToken();
}
} catch (e) {
logSafe("Token validation failed: $e. Logging out.",
level: LogLevel.error);
await LocalStorage.logout();
return null;
}
return token;
}
/// Attempts token refresh with concurrency protection
Future<bool> _refreshTokenIfPossible() async {
if (_isRefreshing) return false;
_isRefreshing = true;
try {
return await AuthService.refreshToken();
} finally {
_isRefreshing = false;
}
}
/// Unified response parser with decryption and validation
dynamic _parseResponse(
http.Response response, {
required String endpoint,
bool fullResponse = false,
}) {
final body = response.body.trim();
if (body.isEmpty &&
response.statusCode >= 200 &&
response.statusCode < 300) {
logSafe("Empty response for $endpoint - returning default structure",
level: LogLevel.info);
return fullResponse ? {'success': true, 'data': []} : [];
}
final decryptedData = decryptResponse(body);
if (decryptedData == null) {
logSafe("❌ Decryption failed for $endpoint", level: LogLevel.error);
return null;
}
final jsonData = decryptedData;
if (response.statusCode >= 200 && response.statusCode < 300) {
if (jsonData is Map && jsonData['success'] == true) {
logSafe("$endpoint: Success (${response.statusCode})",
level: LogLevel.info);
return fullResponse ? jsonData : jsonData['data'];
} else if (jsonData is Map) {
logSafe(
"⚠️ $endpoint: API error - ${jsonData['message'] ?? 'Unknown error'}",
level: LogLevel.warning);
}
}
logSafe("$endpoint: HTTP ${response.statusCode} - $jsonData",
level: LogLevel.error);
return null;
}
/// Generic request executor with 401 retry logic
Future<http.Response?> _execute(
String method,
String endpoint, {
Map<String, String>? queryParams,
Object? body,
Map<String, String>? extraHeaders,
bool hasRetried = false,
}) async {
final token = await _getValidToken();
if (token == null) return null;
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint").replace(
queryParameters:
(method == 'GET' || method == 'DELETE') ? queryParams : null);
final headers = {
..._defaultHeaders(token),
if (extraHeaders != null) ...extraHeaders,
};
final requestBody = body != null ? jsonEncode(body) : null;
logSafe(
"📡 $method $uri${requestBody != null ? ' | Body: ${requestBody.length > 100 ? '${requestBody.substring(0, 100)}...' : requestBody}' : ''}",
level: LogLevel.debug);
try {
final response = switch (method) {
'GET' => await _client.get(uri, headers: headers).timeout(_timeout),
'POST' => await _client
.post(uri, headers: headers, body: requestBody)
.timeout(_timeout),
'PUT' => await _client
.put(uri, headers: headers, body: requestBody)
.timeout(_timeout),
'PATCH' => await _client
.patch(uri, headers: headers, body: requestBody)
.timeout(_timeout),
'DELETE' =>
await _client.delete(uri, headers: headers).timeout(_timeout),
_ => throw HttpException('Unsupported method: $method'),
};
// Handle 401 with single retry
if (response.statusCode == 401 && !hasRetried) {
logSafe("🔄 401 detected for $endpoint - retrying with fresh token",
level: LogLevel.warning);
if (await _refreshTokenIfPossible()) {
return await _execute(method, endpoint,
queryParams: queryParams,
body: body,
extraHeaders: extraHeaders,
hasRetried: true);
}
await LocalStorage.logout();
return null;
}
return response;
} on SocketException catch (e) {
logSafe("🌐 Network error for $endpoint: $e", level: LogLevel.error);
return null;
} catch (e, stackTrace) {
logSafe("💥 HTTP $method error for $endpoint: $e\n$stackTrace",
level: LogLevel.error);
return null;
}
}
// Public API - Clean and consistent
Future<T?> get<T>(
String endpoint, {
Map<String, String>? queryParams,
bool fullResponse = false,
}) async {
final response = await _execute('GET', endpoint, queryParams: queryParams);
return response != null
? _parseResponse(response,
endpoint: endpoint, fullResponse: fullResponse)
: null;
}
Future<T?> post<T>(
String endpoint,
Object? body, {
bool fullResponse = false,
}) async {
final response = await _execute('POST', endpoint, body: body);
return response != null
? _parseResponse(response,
endpoint: endpoint, fullResponse: fullResponse)
: null;
}
Future<T?> put<T>(
String endpoint,
Object? body, {
Map<String, String>? extraHeaders,
bool fullResponse = false,
}) async {
final response =
await _execute('PUT', endpoint, body: body, extraHeaders: extraHeaders);
return response != null
? _parseResponse(response,
endpoint: endpoint, fullResponse: fullResponse)
: null;
}
Future<T?> patch<T>(
String endpoint,
Object? body, {
bool fullResponse = false,
}) async {
final response = await _execute('PATCH', endpoint, body: body);
return response != null
? _parseResponse(response,
endpoint: endpoint, fullResponse: fullResponse)
: null;
}
Future<T?> delete<T>(
String endpoint, {
Map<String, String>? queryParams,
bool fullResponse = false,
}) async {
final response =
await _execute('DELETE', endpoint, queryParams: queryParams);
return response != null
? _parseResponse(response,
endpoint: endpoint, fullResponse: fullResponse)
: null;
}
/// Proper cleanup for long-lived instances
void dispose() {
_client.close();
}
}