387 lines
13 KiB
Dart
387 lines
13 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:siro_service/constant/box_name.dart';
|
|
import 'package:siro_service/constant/links.dart';
|
|
import 'package:siro_service/controller/functions/encrypt_decrypt.dart';
|
|
import 'package:siro_service/env/env.dart';
|
|
import 'package:siro_service/controller/functions/security_helper.dart';
|
|
import 'package:siro_service/main.dart';
|
|
import 'package:siro_service/print.dart';
|
|
|
|
import '../../constant/api_key.dart';
|
|
|
|
class CRUD {
|
|
static bool _isRefreshingJWT = false;
|
|
static String? _appSignature;
|
|
|
|
static String _lastErrorSignature = '';
|
|
static DateTime _lastErrorTimestamp = DateTime(2000);
|
|
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
|
|
|
// ── JWT Validity Check (No external libs) ──────────────────────
|
|
static bool _isJwtValid(String? token) {
|
|
if (token == null || token.isEmpty) return false;
|
|
try {
|
|
final parts = token.split('.');
|
|
if (parts.length != 3) return false;
|
|
String payload = parts[1];
|
|
switch (payload.length % 4) {
|
|
case 2:
|
|
payload += '==';
|
|
break;
|
|
case 3:
|
|
payload += '=';
|
|
break;
|
|
}
|
|
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
|
|
final exp = decoded['exp'];
|
|
if (exp == null) return false;
|
|
// 30 seconds buffer
|
|
return DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static Future<void> addError(
|
|
String error, String details, String where) async {
|
|
try {
|
|
final currentErrorSignature = '$where-$error';
|
|
final now = DateTime.now();
|
|
|
|
if (currentErrorSignature == _lastErrorSignature &&
|
|
now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) {
|
|
return;
|
|
}
|
|
|
|
_lastErrorSignature = currentErrorSignature;
|
|
_lastErrorTimestamp = now;
|
|
|
|
final userId =
|
|
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
|
|
final userType = 'Service';
|
|
final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
|
|
|
|
CRUD().post(
|
|
link: AppLink.addError,
|
|
payload: {
|
|
'error': error.toString(),
|
|
'userId': userId.toString(),
|
|
'userType': userType,
|
|
'phone': phone.toString(),
|
|
'device': where,
|
|
'details': details,
|
|
},
|
|
);
|
|
} catch (e) {}
|
|
}
|
|
|
|
String _getFpHeader() {
|
|
return box.read(BoxName.fingerPrint)?.toString() ?? '';
|
|
}
|
|
|
|
String _generateHmac(String body, String timestamp, String nonce) {
|
|
// نستخدم المفتاح الخاص بالمستخدم (المخزن في البوكس) كـ HMAC Secret
|
|
final hmacSecret = box.read(BoxName.hmac) ?? '';
|
|
|
|
final payload = body + timestamp + nonce;
|
|
final key = utf8.encode(hmacSecret);
|
|
final bytes = utf8.encode(payload);
|
|
final hmacSha256 = Hmac(sha256, key);
|
|
final result = hmacSha256.convert(bytes).toString();
|
|
Log.print('🔐 [HMAC-DEBUG] Secret: $hmacSecret');
|
|
Log.print('🔐 [HMAC-DEBUG] Body(${body.length}): "$body"');
|
|
Log.print('🔐 [HMAC-DEBUG] TS: $timestamp | Nonce: $nonce');
|
|
Log.print('🔐 [HMAC-DEBUG] Result: $result');
|
|
return result;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// _makeRequest — Central Request Handler
|
|
// ───────────────────────────────────────────────────────────────
|
|
Future<dynamic> _makeRequest({
|
|
required String link,
|
|
Map<String, dynamic>? payload,
|
|
required Map<String, String> headers,
|
|
}) async {
|
|
const totalTimeout = Duration(seconds: 60);
|
|
|
|
// توليد بيانات الـ HMAC للطلب الحالي
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
|
final nonce =
|
|
DateTime.now().microsecondsSinceEpoch.toString(); // Nonce فريد
|
|
|
|
// تحويل الـ payload إلى string لمحاكة ما سيصل للسيرفر (php://input)
|
|
String bodyString = '';
|
|
if (payload != null && payload.isNotEmpty) {
|
|
// الـ http.post يرسل البيانات كـ x-www-form-urlencoded
|
|
bodyString = payload.keys
|
|
.map((key) =>
|
|
"$key=${Uri.encodeQueryComponent(payload[key].toString())}")
|
|
.join("&");
|
|
}
|
|
|
|
final hmacSignature = _generateHmac(bodyString, timestamp, nonce);
|
|
|
|
// إضافة هيدرات الـ HMAC
|
|
headers['X-HMAC-Auth'] = hmacSignature;
|
|
headers['X-Timestamp'] = timestamp;
|
|
headers['X-Nonce'] = nonce;
|
|
|
|
Future<http.Response> doPost() {
|
|
final url = Uri.parse(link);
|
|
return http
|
|
.post(url, body: payload, headers: headers)
|
|
.timeout(totalTimeout);
|
|
}
|
|
|
|
http.Response? response;
|
|
int attempts = 0;
|
|
final requestId =
|
|
DateTime.now().millisecondsSinceEpoch.toString().substring(7);
|
|
|
|
Log.print('🚀 [REQ-$requestId] $link');
|
|
Log.print('🔑 [FP-$requestId] ${headers['X-Device-FP']}');
|
|
Log.print('🔏 [SIGN-$requestId] ${headers['X-App-Signature']}');
|
|
if (payload != null) Log.print('📦 [PAYLOAD-$requestId] $payload');
|
|
|
|
while (attempts < 3) {
|
|
try {
|
|
attempts++;
|
|
response = await doPost();
|
|
break;
|
|
} on SocketException catch (_) {
|
|
Log.print('⚠️ SocketException attempt $attempts — $link');
|
|
if (attempts >= 3) return 'no_internet';
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
} on TimeoutException catch (_) {
|
|
Log.print('⚠️ TimeoutException attempt $attempts — $link');
|
|
if (attempts >= 3) return 'failure';
|
|
} catch (e) {
|
|
if (e.toString().contains('errno = 9') && attempts < 3) {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
continue;
|
|
}
|
|
addError(
|
|
'HTTP Exception: $e', 'Try: $attempts', 'CRUD._makeRequest $link');
|
|
return 'failure';
|
|
}
|
|
}
|
|
|
|
if (response == null) return 'failure';
|
|
|
|
final sc = response.statusCode;
|
|
final body = response.body;
|
|
|
|
Log.print('📥 [RES-$requestId] [$sc] $link');
|
|
Log.print('📄 [BODY-$requestId] $body');
|
|
|
|
if (sc >= 200 && sc < 300) {
|
|
try {
|
|
return jsonDecode(body);
|
|
} catch (e, st) {
|
|
addError(
|
|
'JSON Decode Error', 'Body: $body\n$st', 'CRUD._makeRequest $link');
|
|
return 'failure';
|
|
}
|
|
}
|
|
|
|
if (sc == 401) {
|
|
if (!_isRefreshingJWT && !link.contains('errorApp.php')) {
|
|
_isRefreshingJWT = true;
|
|
try {
|
|
await getJWT();
|
|
} finally {
|
|
_isRefreshingJWT = false;
|
|
}
|
|
}
|
|
return 'token_expired';
|
|
}
|
|
|
|
if (sc >= 500) {
|
|
addError('Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link');
|
|
return 'failure';
|
|
}
|
|
|
|
return 'failure';
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// post — standard POST
|
|
// ═══════════════════════════════════════════════════════════════
|
|
Future<dynamic> post({
|
|
required String link,
|
|
Map<String, dynamic>? payload,
|
|
}) async {
|
|
String token = r(box.read(BoxName.jwt) ?? '').toString().split(Env.addd)[0];
|
|
|
|
if (!_isJwtValid(token) &&
|
|
!_isRefreshingJWT &&
|
|
!link.contains('login.php')) {
|
|
_isRefreshingJWT = true;
|
|
try {
|
|
await getJWT();
|
|
token = r(box.read(BoxName.jwt) ?? '').toString().split(Env.addd)[0];
|
|
} finally {
|
|
_isRefreshingJWT = false;
|
|
}
|
|
}
|
|
|
|
// Initialize app signature if null
|
|
_appSignature ??= await SecurityHelper.getAppSignature();
|
|
|
|
final headers = {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Authorization': 'Bearer $token',
|
|
'X-Device-FP': _getFpHeader(),
|
|
'X-App-Signature': _appSignature ?? '',
|
|
};
|
|
|
|
return await _makeRequest(link: link, payload: payload, headers: headers);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// get — standard GET (uses POST method in this architecture)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
Future<dynamic> get({
|
|
required String link,
|
|
Map<String, dynamic>? payload,
|
|
}) async {
|
|
return await post(link: link, payload: payload);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// getJWT — V1 Login Flow
|
|
// ═══════════════════════════════════════════════════════════════
|
|
Future<void> getJWT() async {
|
|
var payload = {
|
|
'fingerprint': _getFpHeader(),
|
|
'password': box.read(BoxName.password) ?? '',
|
|
'aud': 'service',
|
|
};
|
|
|
|
// Initialize app signature if null
|
|
_appSignature ??= await SecurityHelper.getAppSignature();
|
|
|
|
final headers = {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-App-Signature': _appSignature ?? '',
|
|
};
|
|
|
|
final response = await _makeRequest(
|
|
link: AppLink.login, payload: payload, headers: headers);
|
|
|
|
if (response != 'failure' &&
|
|
response is Map &&
|
|
response['status'] == 'success') {
|
|
final jwt = response['message']['jwt'];
|
|
final hmac = response['message']['hmac'];
|
|
|
|
Log.print('jwt: $jwt');
|
|
Log.print('hmac_key: $hmac');
|
|
|
|
await box.write(BoxName.jwt, c(jwt));
|
|
if (hmac != null) {
|
|
await box.write(BoxName.hmac, hmac);
|
|
final verify = box.read(BoxName.hmac);
|
|
Log.print('✅ Verified stored HMAC: $verify');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Service Specific Methods (Preserved)
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
Future<dynamic> arabicTextExtractByVisionAndAI({
|
|
required String imagePath,
|
|
required String driverID,
|
|
}) async {
|
|
var headers = {
|
|
'Content-Type': 'application/json',
|
|
'Ocp-Apim-Subscription-Key': AK.ocpApimSubscriptionKey
|
|
};
|
|
|
|
String imagePathFull =
|
|
'${AppLink.server}/card_image/$imagePath-$driverID.jpg';
|
|
var request = http.Request(
|
|
'POST',
|
|
Uri.parse(
|
|
'https://eastus.api.cognitive.microsoft.com/computervision/imageanalysis:analyze?features=caption,read&model-version=latest&language=en&api-version=2024-02-01'));
|
|
request.body = json.encode({"url": imagePathFull});
|
|
request.headers.addAll(headers);
|
|
|
|
http.StreamedResponse response = await request.send();
|
|
|
|
if (response.statusCode == 200) {
|
|
return await response.stream.bytesToString();
|
|
}
|
|
return 'failure';
|
|
}
|
|
|
|
Future<dynamic> getAgoraToken({
|
|
required String channelName,
|
|
required String uid,
|
|
}) async {
|
|
var res = await http.get(
|
|
Uri.parse(
|
|
'https://orca-app-b2i85.ondigitalocean.app/token?channelName=$channelName'),
|
|
headers: {'Authorization': 'Bearer '});
|
|
|
|
if (res.statusCode == 200) {
|
|
var response = jsonDecode(res.body);
|
|
return response['token'];
|
|
}
|
|
return 'failure';
|
|
}
|
|
|
|
Future<dynamic> getLlama({
|
|
required String link,
|
|
required String payload,
|
|
required String prompt,
|
|
}) async {
|
|
var url = Uri.parse(link);
|
|
var headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization':
|
|
'Bearer LL-X5lJ0Px9CzKK0HTuVZ3u2u4v3tGWkImLTG7okGRk4t25zrsLqJ0qNoUzZ2x4ciPy'
|
|
};
|
|
var data = json.encode({
|
|
"model": "Llama-3-70b-Inst-FW",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content":
|
|
"Extract the desired information from the following passage as json decoded like $prompt just in this:\n\n$payload"
|
|
}
|
|
],
|
|
"temperature": 0.9
|
|
});
|
|
var response = await http.post(url, body: data, headers: headers);
|
|
|
|
if (response.statusCode == 200) {
|
|
return response.body;
|
|
}
|
|
return response.statusCode;
|
|
}
|
|
|
|
sendEmail(String link, Map<String, String>? payload) async {
|
|
var headers = {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
'Authorization':
|
|
'Basic ${base64Encode(utf8.encode(AK.basicAuthCredentials))}',
|
|
};
|
|
var request = http.Request('POST', Uri.parse(link));
|
|
request.bodyFields = payload!;
|
|
request.headers.addAll(headers);
|
|
|
|
http.StreamedResponse response = await request.send();
|
|
if (response.statusCode == 200) {}
|
|
}
|
|
}
|