added decription
This commit is contained in:
parent
9ec7dee0f1
commit
b1741bbb0c
@ -1,8 +1,8 @@
|
||||
class ApiEndpoints {
|
||||
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
||||
static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://api.onfieldwork.com/api";
|
||||
|
||||
|
||||
|
||||
4051
lib/helpers/services/api_service copy.dart
Normal file
4051
lib/helpers/services/api_service copy.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -47,7 +47,7 @@ import 'package:on_field_work/model/infra_project/infra_project_list.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_project_details.dart';
|
||||
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
||||
|
||||
import 'package:on_field_work/helpers/utils/encryption_helper.dart';
|
||||
|
||||
class ApiService {
|
||||
static const bool enableLogs = true;
|
||||
@ -112,37 +112,70 @@ class ApiService {
|
||||
}
|
||||
|
||||
static dynamic _parseResponse(http.Response response, {String label = ''}) {
|
||||
_log("$label Response: ${response.body}");
|
||||
try {
|
||||
final json = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && json['success'] == true) {
|
||||
_log("$label Encrypted Response: ${response.body}"); // Log encrypted body
|
||||
|
||||
// --- ⚠️ START of Decryption Change ⚠️ ---
|
||||
final decryptedData =
|
||||
decryptResponse(response.body); // Decrypt the Base64 string
|
||||
|
||||
if (decryptedData == null) {
|
||||
_log("Decryption failed for [$label]. Cannot parse response.");
|
||||
return null;
|
||||
}
|
||||
// If decryptedData is a Map/List (JSON), use it directly.
|
||||
// If it's a plain String, you'll need to decode it to JSON.
|
||||
|
||||
// Assuming the decrypted result is a Map/List (JSON), as per your API structure:
|
||||
final json = decryptedData;
|
||||
|
||||
// Now proceed with your existing logic using the decrypted JSON object
|
||||
if (response.statusCode == 200 && json is Map && json['success'] == true) {
|
||||
_log("$label Decrypted Data: ${json['data']}");
|
||||
return json['data'];
|
||||
}
|
||||
|
||||
// Handle error cases using the decrypted data
|
||||
if (json is Map) {
|
||||
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
|
||||
} catch (e) {
|
||||
_log("Response parsing error [$label]: $e");
|
||||
} else {
|
||||
_log("API Error [$label]: Decrypted response not a map: $json");
|
||||
}
|
||||
|
||||
// --- ⚠️ END of Decryption Change ⚠️ ---
|
||||
return null;
|
||||
}
|
||||
|
||||
static dynamic _parseResponseForAllData(http.Response response,
|
||||
{String label = ''}) {
|
||||
_log("$label Response: ${response.body}");
|
||||
_log("$label Encrypted Response: ${response.body}");
|
||||
|
||||
try {
|
||||
// --- ⚠️ START of Decryption Change ⚠️ ---
|
||||
final body = response.body.trim();
|
||||
if (body.isEmpty) throw FormatException("Empty response body");
|
||||
|
||||
final json = jsonDecode(body);
|
||||
if (response.statusCode == 200 && json['success'] == true) {
|
||||
return json;
|
||||
if (body.isEmpty) {
|
||||
_log("Empty response body for [$label]");
|
||||
return null;
|
||||
}
|
||||
|
||||
final json = decryptResponse(body); // Decrypt and auto-decode JSON
|
||||
|
||||
if (json == null) {
|
||||
_log("Decryption failed for [$label]. Cannot parse response.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (json is Map && response.statusCode == 200 && json['success'] == true) {
|
||||
_log("$label Decrypted JSON: $json");
|
||||
return json; // Return the full JSON map
|
||||
}
|
||||
|
||||
// Handle error cases
|
||||
if (json is Map) {
|
||||
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
|
||||
} catch (e) {
|
||||
_log("Response parsing error [$label]: $e");
|
||||
} else {
|
||||
_log("API Error [$label]: Decrypted response not a map: $json");
|
||||
}
|
||||
|
||||
// --- ⚠️ END of Decryption Change ⚠️ ---
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -319,7 +352,6 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// ============================================
|
||||
/// GET PURCHASE INVOICE OVERVIEW (Dashboard)
|
||||
/// ============================================
|
||||
@ -357,6 +389,7 @@ class ApiService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// ============================================
|
||||
/// GET COLLECTION OVERVIEW (Dashboard)
|
||||
/// ============================================
|
||||
@ -1836,6 +1869,7 @@ class ApiService {
|
||||
'Authorization': 'Bearer $token',
|
||||
};
|
||||
|
||||
// Send logs as JSON
|
||||
final response = await http
|
||||
.post(uri, headers: headers, body: jsonEncode(logs))
|
||||
.timeout(ApiService.extendedTimeout);
|
||||
@ -1843,15 +1877,28 @@ class ApiService {
|
||||
logSafe("Post logs response status: ${response.statusCode}");
|
||||
logSafe("Post logs response body: ${response.body}");
|
||||
|
||||
if (response.statusCode == 200 && response.body.isNotEmpty) {
|
||||
final json = jsonDecode(response.body);
|
||||
if (json['success'] == true) {
|
||||
logSafe("Logs posted successfully.");
|
||||
return true;
|
||||
}
|
||||
// --- Decrypt response before parsing ---
|
||||
final decryptedData =
|
||||
decryptResponse(response.body); // returns Map/List or null
|
||||
|
||||
if (decryptedData == null) {
|
||||
logSafe("Decryption failed. Cannot parse logs response.",
|
||||
level: LogLevel.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
logSafe("Failed to post logs: ${response.body}", level: LogLevel.warning);
|
||||
// Expecting decrypted data to be a Map with 'success' field
|
||||
if (response.statusCode == 200 &&
|
||||
decryptedData is Map &&
|
||||
decryptedData['success'] == true) {
|
||||
logSafe("Logs posted successfully.");
|
||||
return true;
|
||||
} else {
|
||||
final errorMsg = decryptedData is Map
|
||||
? decryptedData['message'] ?? 'Unknown error'
|
||||
: 'Decrypted response not a Map: $decryptedData';
|
||||
logSafe("Failed to post logs: $errorMsg", level: LogLevel.warning);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception during postLogsApi: $e", level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:http/http.dart' as http;
|
||||
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/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/utils/encryption_helper.dart';
|
||||
|
||||
class AuthService {
|
||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||
@ -11,16 +12,13 @@ class AuthService {
|
||||
};
|
||||
|
||||
static bool isLoggedIn = false;
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Logout API */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/* -------------------------------------------------------------------------- /
|
||||
/ Logout API /
|
||||
/ -------------------------------------------------------------------------- */
|
||||
static Future<bool> logoutApi(String refreshToken, String fcmToken) async {
|
||||
try {
|
||||
final body = {
|
||||
"refreshToken": refreshToken,
|
||||
"fcmToken": fcmToken,
|
||||
};
|
||||
|
||||
final body = {"refreshToken": refreshToken, "fcmToken": fcmToken};
|
||||
final response = await _post("/auth/logout", body);
|
||||
|
||||
if (response != null && response['statusCode'] == 200) {
|
||||
@ -37,9 +35,9 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Public Methods */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- /
|
||||
/ Public Methods /
|
||||
/ -------------------------------------------------------------------------- */
|
||||
|
||||
static Future<bool> registerDeviceToken(String fcmToken) async {
|
||||
final token = await LocalStorage.getJwtToken();
|
||||
@ -50,18 +48,6 @@ class AuthService {
|
||||
}
|
||||
|
||||
final body = {"fcmToken": fcmToken};
|
||||
final headers = {
|
||||
..._headers,
|
||||
'Authorization': 'Bearer $token',
|
||||
};
|
||||
final endpoint = "$_baseUrl/auth/set/device-token";
|
||||
|
||||
// 🔹 Log request details
|
||||
logSafe("📡 Device Token API Request");
|
||||
logSafe("➡️ Endpoint: $endpoint");
|
||||
logSafe("➡️ Headers: ${jsonEncode(headers)}");
|
||||
logSafe("➡️ Payload: ${jsonEncode(body)}");
|
||||
|
||||
final data = await _post("/auth/set/device-token", body, authToken: token);
|
||||
|
||||
if (data != null && data['success'] == true) {
|
||||
@ -76,9 +62,6 @@ class AuthService {
|
||||
static Future<Map<String, String>?> loginUser(
|
||||
Map<String, dynamic> data) async {
|
||||
logSafe("Attempting login...");
|
||||
logSafe("Login payload (raw): $data");
|
||||
logSafe("Login payload (JSON): ${jsonEncode(data)}");
|
||||
|
||||
final responseData = await _post("/auth/app/login", data);
|
||||
if (responseData == null)
|
||||
return {"error": "Network error. Please check your connection."};
|
||||
@ -110,17 +93,10 @@ class AuthService {
|
||||
await LocalStorage.setLoggedInUser(true);
|
||||
logSafe("Token refreshed successfully.");
|
||||
|
||||
// 🔹 Retry FCM token registration after token refresh
|
||||
final newFcmToken = LocalStorage.getFcmToken();
|
||||
if (newFcmToken?.isNotEmpty ?? false) {
|
||||
final success = await registerDeviceToken(newFcmToken!);
|
||||
logSafe(
|
||||
success
|
||||
? "✅ FCM token re-registered after JWT refresh."
|
||||
: "⚠️ Failed to register FCM token after JWT refresh.",
|
||||
level: success ? LogLevel.info : LogLevel.warning);
|
||||
await registerDeviceToken(newFcmToken!);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
logSafe("Refresh token failed: ${data?['message']}",
|
||||
@ -223,10 +199,13 @@ class AuthService {
|
||||
};
|
||||
final response = await http.post(Uri.parse("$_baseUrl$path"),
|
||||
headers: headers, body: jsonEncode(body));
|
||||
return {
|
||||
...jsonDecode(response.body),
|
||||
"statusCode": response.statusCode,
|
||||
};
|
||||
|
||||
final decrypted = decryptResponse(response.body); // <-- Decrypt here
|
||||
if (decrypted is Map<String, dynamic>) {
|
||||
return {"statusCode": response.statusCode, ...decrypted};
|
||||
} else {
|
||||
return {"statusCode": response.statusCode, "data": decrypted};
|
||||
}
|
||||
} catch (e, st) {
|
||||
_handleError("$path POST error", e, st);
|
||||
return null;
|
||||
@ -245,10 +224,13 @@ class AuthService {
|
||||
};
|
||||
final response =
|
||||
await http.get(Uri.parse("$_baseUrl$path"), headers: headers);
|
||||
return {
|
||||
...jsonDecode(response.body),
|
||||
"statusCode": response.statusCode,
|
||||
};
|
||||
|
||||
final decrypted = decryptResponse(response.body); // <-- Decrypt here
|
||||
if (decrypted is Map<String, dynamic>) {
|
||||
return {"statusCode": response.statusCode, ...decrypted};
|
||||
} else {
|
||||
return {"statusCode": response.statusCode, "data": decrypted};
|
||||
}
|
||||
} catch (e, st) {
|
||||
_handleError("$path GET error", e, st);
|
||||
return null;
|
||||
@ -270,8 +252,6 @@ class AuthService {
|
||||
}
|
||||
|
||||
static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async {
|
||||
logSafe("Processing login success...");
|
||||
|
||||
await LocalStorage.setJwtToken(data['token']);
|
||||
await LocalStorage.setLoggedInUser(true);
|
||||
|
||||
@ -287,6 +267,5 @@ class AuthService {
|
||||
await LocalStorage.removeMpinToken();
|
||||
}
|
||||
isLoggedIn = true;
|
||||
logSafe("✅ Login flow completed and controllers initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
255
lib/helpers/services/http_client.dart
Normal file
255
lib/helpers/services/http_client.dart
Normal file
@ -0,0 +1,255 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
@ -9,21 +8,18 @@ import 'package:on_field_work/model/projects_model.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.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/utils/encryption_helper.dart';
|
||||
|
||||
class PermissionService {
|
||||
// In-memory cache keyed by user token
|
||||
static final Map<String, Map<String, dynamic>> _userDataCache = {};
|
||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||
|
||||
/// Fetches all user-related data (permissions, employee info, projects).
|
||||
/// Uses in-memory cache for repeated token queries during session.
|
||||
static Future<Map<String, dynamic>> fetchAllUserData(
|
||||
String token, {
|
||||
bool hasRetried = false,
|
||||
}) async {
|
||||
logSafe("Fetching user data...");
|
||||
|
||||
// Check for cached data before network request
|
||||
final cached = _userDataCache[token];
|
||||
if (cached != null) {
|
||||
logSafe("User data cache hit.");
|
||||
@ -37,22 +33,38 @@ class PermissionService {
|
||||
final response = await http.get(uri, headers: headers);
|
||||
final statusCode = response.statusCode;
|
||||
|
||||
if (statusCode == 200) {
|
||||
final raw = json.decode(response.body);
|
||||
final data = raw['data'] as Map<String, dynamic>;
|
||||
if (response.body.isEmpty || response.body.trim().isEmpty) {
|
||||
logSafe("❌ Empty user data response — auto logout");
|
||||
await _handleUnauthorized();
|
||||
throw Exception("Empty user data response");
|
||||
}
|
||||
|
||||
final decrypted = decryptResponse(response.body);
|
||||
if (decrypted == null) {
|
||||
logSafe("❌ Failed to decrypt user data — auto logout", level: LogLevel.error);
|
||||
await _handleUnauthorized();
|
||||
throw Exception("Decryption failed for user data");
|
||||
}
|
||||
|
||||
final data = decrypted is Map ? decrypted['data'] ?? decrypted : null;
|
||||
if (data == null || data is! Map<String, dynamic>) {
|
||||
logSafe("❌ Decrypted user data is invalid — auto logout", level: LogLevel.error);
|
||||
await _handleUnauthorized();
|
||||
throw Exception("Invalid decrypted user data");
|
||||
}
|
||||
|
||||
if (statusCode == 200) {
|
||||
final result = {
|
||||
'permissions': _parsePermissions(data['featurePermissions']),
|
||||
'employeeInfo': _parseEmployeeInfo(data['employeeInfo']),
|
||||
'projects': _parseProjectsInfo(data['projects']),
|
||||
};
|
||||
|
||||
_userDataCache[token] = result; // Cache it for future use
|
||||
logSafe("User data fetched successfully.");
|
||||
_userDataCache[token] = result;
|
||||
logSafe("User data fetched and decrypted successfully.");
|
||||
return result;
|
||||
}
|
||||
|
||||
// Token expired, try refresh once then redirect on failure
|
||||
if (statusCode == 401 && !hasRetried) {
|
||||
logSafe("Unauthorized. Attempting token refresh...", level: LogLevel.warning);
|
||||
|
||||
@ -69,16 +81,17 @@ class PermissionService {
|
||||
throw Exception('Unauthorized. Token refresh failed.');
|
||||
}
|
||||
|
||||
final errorMsg = json.decode(response.body)['message'] ?? 'Unknown error';
|
||||
final errorMsg = data['message'] ?? 'Unknown error';
|
||||
logSafe("Failed to fetch user data: $errorMsg", level: LogLevel.warning);
|
||||
throw Exception('Failed to fetch user data: $errorMsg');
|
||||
} catch (e, stacktrace) {
|
||||
logSafe("Exception while fetching user data", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||
rethrow; // Let the caller handle or report
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// Handles unauthorized/user sign out flow
|
||||
static Future<void> _handleUnauthorized() async {
|
||||
logSafe("Clearing tokens and redirecting to login due to unauthorized access.", level: LogLevel.warning);
|
||||
await LocalStorage.removeToken('jwt_token');
|
||||
@ -87,22 +100,18 @@ class PermissionService {
|
||||
Get.offAllNamed('/auth/login-option');
|
||||
}
|
||||
|
||||
/// Robust model parsing for permissions
|
||||
static List<UserPermission> _parsePermissions(List<dynamic> permissions) {
|
||||
static List<UserPermission> _parsePermissions(List<dynamic>? permissions) {
|
||||
logSafe("Parsing user permissions...");
|
||||
return permissions
|
||||
.map((perm) => UserPermission.fromJson({'id': perm}))
|
||||
.toList();
|
||||
if (permissions == null) return [];
|
||||
return permissions.map((perm) => UserPermission.fromJson({'id': perm})).toList();
|
||||
}
|
||||
|
||||
/// Robust model parsing for employee info
|
||||
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic>? data) {
|
||||
logSafe("Parsing employee info...");
|
||||
if (data == null) throw Exception("Employee data missing");
|
||||
return EmployeeInfo.fromJson(data);
|
||||
}
|
||||
|
||||
/// Robust model parsing for projects list
|
||||
static List<ProjectInfo> _parseProjectsInfo(List<dynamic>? projects) {
|
||||
logSafe("Parsing projects info...");
|
||||
if (projects == null) return [];
|
||||
|
||||
@ -2,97 +2,80 @@ import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/project_controller.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/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
||||
import 'package:on_field_work/helpers/utils/encryption_helper.dart';
|
||||
|
||||
/// Abstract interface for tenant service functionality
|
||||
abstract class ITenantService {
|
||||
Future<List<Map<String, dynamic>>?> getTenants({bool hasRetried = false});
|
||||
Future<bool> selectTenant(String tenantId, {bool hasRetried = false});
|
||||
}
|
||||
|
||||
/// Tenant API service
|
||||
class TenantService implements ITenantService {
|
||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||
static const Map<String, String> _headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
/// Currently selected tenant
|
||||
static Tenant? currentTenant;
|
||||
|
||||
/// Set the selected tenant
|
||||
static void setSelectedTenant(Tenant tenant) {
|
||||
currentTenant = tenant;
|
||||
}
|
||||
|
||||
/// Check if tenant is selected
|
||||
static bool get isTenantSelected => currentTenant != null;
|
||||
|
||||
/// Build authorized headers
|
||||
static Future<Map<String, String>> _authorizedHeaders() async {
|
||||
final token = await LocalStorage.getJwtToken();
|
||||
if (token == null || token.isEmpty) {
|
||||
throw Exception('Missing JWT token');
|
||||
}
|
||||
if (token == null || token.isEmpty) throw Exception('Missing JWT token');
|
||||
return {..._headers, 'Authorization': 'Bearer $token'};
|
||||
}
|
||||
|
||||
/// Handle API errors
|
||||
static void _handleApiError(
|
||||
http.Response response, dynamic data, String context) {
|
||||
static void _handleApiError(http.Response response, dynamic data, String context) {
|
||||
final message = data['message'] ?? 'Unknown error';
|
||||
final level =
|
||||
response.statusCode >= 500 ? LogLevel.error : LogLevel.warning;
|
||||
logSafe("❌ $context failed: $message [Status: ${response.statusCode}]",
|
||||
level: level);
|
||||
final level = response.statusCode >= 500 ? LogLevel.error : LogLevel.warning;
|
||||
logSafe("❌ $context failed: $message [Status: ${response.statusCode}]", level: level);
|
||||
}
|
||||
|
||||
/// Log exceptions
|
||||
static void _logException(dynamic e, dynamic st, String context) {
|
||||
logSafe("❌ $context exception",
|
||||
level: LogLevel.error, error: e, stackTrace: st);
|
||||
logSafe("❌ $context exception", level: LogLevel.error, error: e, stackTrace: st);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>?> getTenants(
|
||||
{bool hasRetried = false}) async {
|
||||
Future<List<Map<String, dynamic>>?> getTenants({bool hasRetried = false}) async {
|
||||
try {
|
||||
final headers = await _authorizedHeaders();
|
||||
final response = await http.get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers);
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse("$_baseUrl/auth/get/user/tenants"),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
// ✅ Handle empty response BEFORE decoding
|
||||
if (response.body.isEmpty || response.body.trim().isEmpty) {
|
||||
logSafe("❌ Empty tenant response — auto logout");
|
||||
await LocalStorage.logout();
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> data;
|
||||
try {
|
||||
data = jsonDecode(response.body);
|
||||
} catch (e) {
|
||||
logSafe("❌ Invalid JSON in tenant response — auto logout");
|
||||
final decrypted = decryptResponse(response.body);
|
||||
if (decrypted == null) {
|
||||
logSafe("❌ Tenant response decryption failed — auto logout");
|
||||
await LocalStorage.logout();
|
||||
return null;
|
||||
}
|
||||
|
||||
final data = decrypted is Map ? decrypted : null;
|
||||
if (data == null) {
|
||||
logSafe("❌ Decrypted tenant data is not valid JSON — auto logout");
|
||||
await LocalStorage.logout();
|
||||
return null;
|
||||
}
|
||||
|
||||
// SUCCESS CASE
|
||||
if (response.statusCode == 200 && data['success'] == true) {
|
||||
final list = data['data'];
|
||||
if (list is! List) return null;
|
||||
return List<Map<String, dynamic>>.from(list);
|
||||
}
|
||||
|
||||
// TOKEN EXPIRED
|
||||
if (response.statusCode == 401 && !hasRetried) {
|
||||
final refreshed = await AuthService.refreshToken();
|
||||
if (refreshed) return getTenants(hasRetried: true);
|
||||
@ -105,32 +88,36 @@ class TenantService implements ITenantService {
|
||||
_logException(e, st, "Get Tenants API");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> selectTenant(String tenantId, {bool hasRetried = false}) async {
|
||||
try {
|
||||
final headers = await _authorizedHeaders();
|
||||
logSafe(
|
||||
"➡️ POST $_baseUrl/auth/select-tenant/$tenantId\nHeaders: $headers",
|
||||
level: LogLevel.info);
|
||||
logSafe("➡️ POST $_baseUrl/auth/select-tenant/$tenantId\nHeaders: $headers", level: LogLevel.info);
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/auth/select-tenant/$tenantId"),
|
||||
headers: headers,
|
||||
);
|
||||
final data = jsonDecode(response.body);
|
||||
final response = await http.post(Uri.parse("$_baseUrl/auth/select-tenant/$tenantId"), headers: headers);
|
||||
final decrypted = decryptResponse(response.body);
|
||||
if (decrypted == null) {
|
||||
logSafe("❌ Tenant selection response decryption failed", level: LogLevel.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
logSafe(
|
||||
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
|
||||
level: LogLevel.info);
|
||||
final data = decrypted is Map ? decrypted : null;
|
||||
if (data == null) {
|
||||
logSafe("❌ Decrypted tenant selection data is not valid JSON", level: LogLevel.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
logSafe("⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]", level: LogLevel.info);
|
||||
|
||||
if (response.statusCode == 200 && data['success'] == true) {
|
||||
await LocalStorage.setJwtToken(data['data']['token']);
|
||||
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
|
||||
logSafe("✅ Tenant selected successfully. Tokens updated.");
|
||||
|
||||
// 🔥 Refresh projects when tenant changes
|
||||
try {
|
||||
final projectController = Get.find<ProjectController>();
|
||||
projectController.clearProjects();
|
||||
@ -139,27 +126,20 @@ class TenantService implements ITenantService {
|
||||
logSafe("⚠️ ProjectController not found while refreshing projects");
|
||||
}
|
||||
|
||||
// 🔹 Register FCM token after tenant selection
|
||||
final fcmToken = LocalStorage.getFcmToken();
|
||||
if (fcmToken?.isNotEmpty ?? false) {
|
||||
final success = await AuthService.registerDeviceToken(fcmToken!);
|
||||
logSafe(
|
||||
success
|
||||
? "✅ FCM token registered after tenant selection."
|
||||
: "⚠️ Failed to register FCM token after tenant selection.",
|
||||
level: success ? LogLevel.info : LogLevel.warning);
|
||||
logSafe(success ? "✅ FCM token registered after tenant selection." : "⚠️ Failed to register FCM token.", level: success ? LogLevel.info : LogLevel.warning);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (response.statusCode == 401 && !hasRetried) {
|
||||
logSafe("⚠️ Unauthorized while selecting tenant. Refreshing token...",
|
||||
level: LogLevel.warning);
|
||||
logSafe("⚠️ Unauthorized while selecting tenant. Refreshing token...", level: LogLevel.warning);
|
||||
final refreshed = await AuthService.refreshToken();
|
||||
if (refreshed) return selectTenant(tenantId, hasRetried: true);
|
||||
logSafe("❌ Token refresh failed while selecting tenant.",
|
||||
level: LogLevel.error);
|
||||
logSafe("❌ Token refresh failed while selecting tenant.", level: LogLevel.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -169,5 +149,7 @@ class TenantService implements ITenantService {
|
||||
_logException(e, st, "Select Tenant API");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
75
lib/helpers/utils/encryption_helper.dart
Normal file
75
lib/helpers/utils/encryption_helper.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'dart:convert';
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart'; // <-- for logging
|
||||
|
||||
// 🔑 CONSTANTS
|
||||
// Base64-encoded 32-byte key (256 bits for AES-256)
|
||||
const String _keyBase64 = "u4J7p9Qx2hF5vYtLz8Kq3mN1sG0bRwXyZcD6eH8jFQw=";
|
||||
// IV must be 16 bytes for AES-CBC mode
|
||||
const int _ivLength = 16;
|
||||
|
||||
/// Decrypts a Base64-encoded string that contains the IV prepended to the ciphertext.
|
||||
/// Returns the decoded JSON object, the plain decrypted string, or null on failure.
|
||||
dynamic decryptResponse(String encryptedBase64Str) {
|
||||
try {
|
||||
// 1️⃣ Initialize Key
|
||||
final rawKeyBytes = base64.decode(_keyBase64);
|
||||
if (rawKeyBytes.length != 32) {
|
||||
logSafe("ERROR: Decoded key length is ${rawKeyBytes.length}. Expected 32 bytes for AES-256.", level: LogLevel.error);
|
||||
throw Exception("Invalid key length.");
|
||||
}
|
||||
final key = Key(rawKeyBytes);
|
||||
|
||||
// 2️⃣ Decode incoming encrypted payload (IV + Ciphertext)
|
||||
final fullBytes = base64.decode(encryptedBase64Str);
|
||||
|
||||
if (fullBytes.length < _ivLength + 16) {
|
||||
// Minimum length check (16 bytes IV + 1 block of ciphertext, which is 16 bytes)
|
||||
throw Exception("Encrypted string too short or corrupted.");
|
||||
}
|
||||
|
||||
// 3️⃣ Extract IV & Ciphertext
|
||||
// Assumes the first 16 bytes are the IV
|
||||
final iv = IV(fullBytes.sublist(0, _ivLength));
|
||||
final cipherTextBytes = fullBytes.sublist(_ivLength);
|
||||
|
||||
// 4️⃣ Configure Encrypter with specific parameters
|
||||
// AES-256 with CBC mode and standard PKCS7 padding
|
||||
final encrypter = Encrypter(
|
||||
AES(
|
||||
key,
|
||||
mode: AESMode.cbc,
|
||||
padding: 'PKCS7'
|
||||
)
|
||||
);
|
||||
final encrypted = Encrypted(cipherTextBytes);
|
||||
|
||||
// 5️⃣ Decrypt - This is where the "Invalid or corrupted pad block" error occurs
|
||||
final decryptedBytes = encrypter.decryptBytes(encrypted, iv: iv);
|
||||
final decryptedString = utf8.decode(decryptedBytes);
|
||||
|
||||
if (decryptedString.isEmpty) {
|
||||
throw Exception("Decryption produced empty string (check if padding was correct).");
|
||||
}
|
||||
|
||||
// 🔹 Log decrypted snippet for verification
|
||||
final snippetLength = decryptedString.length > 50 ? 50 : decryptedString.length;
|
||||
logSafe(
|
||||
"Decryption successful. Snippet: ${decryptedString.substring(0, snippetLength)}...",
|
||||
level: LogLevel.info,
|
||||
);
|
||||
|
||||
// 6️⃣ Try parsing JSON
|
||||
try {
|
||||
return jsonDecode(decryptedString);
|
||||
} catch (_) {
|
||||
// return plain string if it's not JSON
|
||||
logSafe("Decrypted data is not JSON. Returning plain string.", level: LogLevel.warning);
|
||||
return decryptedString;
|
||||
}
|
||||
} catch (e, st) {
|
||||
// Catch the specific decryption error (e.g., 'Invalid or corrupted pad block')
|
||||
logSafe("FATAL Decryption failed: $e", level: LogLevel.error, stackTrace: st);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -86,6 +86,7 @@ dependencies:
|
||||
gallery_saver_plus: ^3.2.9
|
||||
share_plus: ^12.0.1
|
||||
timeline_tile: ^2.0.0
|
||||
encrypt: ^5.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user