From 89c1348f0861d0d648ea2430c31e2707f1cc137c Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 25 Jun 2026 03:28:59 +0300 Subject: [PATCH] Update: 2026-06-25 03:28:59 --- backend/auth/syria/uploadImage.php | 75 +++++++++ .../auth/syria/registration_controller.dart | 146 ++++++++++-------- .../views/auth/syria/registration_view.dart | 40 +++-- 3 files changed, 188 insertions(+), 73 deletions(-) create mode 100644 backend/auth/syria/uploadImage.php diff --git a/backend/auth/syria/uploadImage.php b/backend/auth/syria/uploadImage.php new file mode 100644 index 0000000..c094a5b --- /dev/null +++ b/backend/auth/syria/uploadImage.php @@ -0,0 +1,75 @@ +prepare("SELECT country FROM drivers WHERE id = ?"); + $stmt->execute([$driverID]); + $country = strtolower(trim((string)$stmt->fetchColumn())); + } catch (Exception $e) { + $country = ''; + } +} +if (!in_array($country, ['syria', 'jordan', 'egypt'])) { + $country = 'syria'; +} + +uploadLog("📥 Params: driverID=$driverID, imageType=$imageType, country=$country"); + +// --------- رفع الملف --------- +$targetDir = __DIR__ . "/../../auth/uploads/{$country}/"; +$result = uploadImageSecure('image', $targetDir, $driverID . '_' . $imageType); + +if (!$result['success']) { + uploadLog("❌ Upload failed: {$result['error']}", 'ERROR', [ + 'driverID' => $driverID, + 'imageType' => $imageType, + 'country' => $country, + ]); + jsonError($result['error']); +} + +// --------- بناء الرابط العام --------- +$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; +$host = getenv('APP_DOMAIN') ?: ($_SERVER['HTTP_HOST'] ?? 'api.siromove.com'); + +$basePath = rtrim(dirname(dirname(dirname($_SERVER['SCRIPT_NAME']))), '/'); +$url = "$protocol://$host{$basePath}/auth/uploads/{$country}/{$result['filename']}"; + +uploadLog("✅ Uploaded: {$result['path']} -> $url", 'INFO', [ + 'driverID' => $driverID, + 'imageType' => $imageType, + 'country' => $country, +]); + +jsonSuccess([ + 'url' => $url, + 'file_link' => $url, + 'filename' => $result['filename'], + 'driverID' => $driverID, + 'imageType' => $imageType, + 'country' => $country, +]); diff --git a/siro_driver/lib/controller/auth/syria/registration_controller.dart b/siro_driver/lib/controller/auth/syria/registration_controller.dart index e33aad1..f3d671e 100644 --- a/siro_driver/lib/controller/auth/syria/registration_controller.dart +++ b/siro_driver/lib/controller/auth/syria/registration_controller.dart @@ -282,13 +282,21 @@ class RegistrationController extends GetxController { } /// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type - Future choosImage(String link, String imageType) async { + Future choosImage(String link, String imageType, {String? backupLink}) async { + if (isloading) return; try { + isloading = true; + update(); + final pickedImage = await picker.pickImage( source: ImageSource.camera, preferredCameraDevice: CameraDevice.rear, ); - if (pickedImage == null) return; + if (pickedImage == null) { + isloading = false; + update(); + return; + } image = File(pickedImage.path); @@ -305,14 +313,15 @@ class RegistrationController extends GetxController { IOSUiSettings(title: 'Cropper'.tr), ], ); - if (croppedFile == null) return; + if (croppedFile == null) { + isloading = false; + update(); + return; + } // صورة للمعاينة داخل التطبيق myImage = File(croppedFile.path); - isloading = true; - update(); - // ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت) final File compressedImage = await compressImage(File(croppedFile.path)); @@ -320,12 +329,14 @@ class RegistrationController extends GetxController { final driverId = box.read(BoxName.driverID); final payload = { - 'driverID': driverId, - 'imageType': imageType, // مثال: driver_license_front + 'driverID': driverId?.toString() ?? '', + 'imageType': imageType, + 'country': box.read(BoxName.countryCode) ?? '', }; - // الرفع وإرجاع الرابط - final String imageUrl = await uploadImage(compressedImage, payload, link); + // الرفع وإرجاع الرابط (يحاول الأساسي ثم الاحتياطي) + final String imageUrl = + await uploadImage(compressedImage, payload, link, backupLink: backupLink); // حفظ الرابط محلياً حسب النوع docUrls[imageType] = imageUrl; @@ -340,68 +351,79 @@ class RegistrationController extends GetxController { } } - /// ترفع الملف وترجع رابط الصورة النهائي كـ String مع إعادة المحاولة في حال فشل الاتصال + /// ترفع الملف وترجع رابط الصورة النهائي كـ String مع إعادة المحاولة. + /// يحاول الرابط الأساسي first، فإن فشل جرب الاحتياطي (إن وُجد). Future uploadImage( - File file, Map data, String link) async { - int maxRetries = 3; - int attempt = 0; - while (attempt < maxRetries) { - attempt++; - try { - final uri = Uri.parse(link); - final request = http.MultipartRequest('POST', uri); + File file, + Map data, + String link, { + String? backupLink, + }) async { + const int maxRetries = 3; - final _jwt = box.read(BoxName.jwt); - final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : ''; - final headers = { - 'Authorization': 'Bearer $_token', - 'X-HMAC-Auth': '${box.read(BoxName.hmac)}', - }; - request.headers.addAll(headers); + final List urlsToTry = [link, if (backupLink != null && backupLink.trim().isNotEmpty) backupLink]; - final forcedName = '${box.read(BoxName.driverID) ?? 'image'}.jpg'; + for (final currentUrl in urlsToTry) { + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + final uri = Uri.parse(currentUrl); + final request = http.MultipartRequest('POST', uri); - request.files.add( - await http.MultipartFile.fromPath( - 'image', - file.path, - filename: forcedName, - ), - ); + final _jwt = box.read(BoxName.jwt); + final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : ''; + final headers = { + 'Authorization': 'Bearer $_token', + 'X-HMAC-Auth': '${box.read(BoxName.hmac)}', + }; + request.headers.addAll(headers); - data.forEach((k, v) => request.fields[k] = v); + final forcedName = '${box.read(BoxName.driverID) ?? 'image'}.jpg'; - // المهلة الزمنية 120 ثانية لتناسب الاتصالات الضعيفة - final streamed = - await request.send().timeout(const Duration(seconds: 120)); - final res = await http.Response.fromStream(streamed); + request.files.add( + await http.MultipartFile.fromPath( + 'image', + file.path, + filename: forcedName, + ), + ); - if (res.statusCode != 200) { - throw Exception( - 'Failed to upload image: ${res.statusCode} - ${res.body}'); + data.forEach((k, v) => request.fields[k] = v); + + final streamed = + await request.send().timeout(const Duration(seconds: 120)); + final res = await http.Response.fromStream(streamed); + + if (res.statusCode != 200) { + throw Exception( + 'Failed to upload image: ${res.statusCode} - ${res.body}'); + } + + final body = jsonDecode(res.body); + final String? url = body['url'] ?? + body['file_link'] ?? + body['image_url'] ?? + (body['data'] is Map ? body['data']['url'] : null); + + if (url == null || url.isEmpty) { + throw Exception( + 'Upload succeeded but no image URL found in response: ${res.body}'); + } + + return url; + } catch (e) { + Log.print("⚠️ [Upload Attempt $attempt/$maxRetries @ $currentUrl] Error: $e"); + if (attempt >= maxRetries) { + if (urlsToTry.last == currentUrl) { + rethrow; + } + Log.print("➡️ Switching to backup URL..."); + } else { + await Future.delayed(Duration(seconds: attempt * 2)); + } } - - final body = jsonDecode(res.body); - final String? url = body['url'] ?? - body['file_link'] ?? - body['image_url'] ?? - (body['data'] is Map ? body['data']['url'] : null); - - if (url == null || url.isEmpty) { - throw Exception( - 'Upload succeeded but no image URL found in response: ${res.body}'); - } - - return url; - } catch (e) { - Log.print("⚠️ [Image Upload Attempt $attempt Failed] Error: $e"); - if (attempt >= maxRetries) { - rethrow; - } - await Future.delayed(Duration(seconds: attempt * 2)); } } - throw Exception('Upload failed after $maxRetries attempts'); + throw Exception('Upload failed after all attempts'); } Future compressImage(File file) async { diff --git a/siro_driver/lib/views/auth/syria/registration_view.dart b/siro_driver/lib/views/auth/syria/registration_view.dart index 6c2b576..ef27668 100644 --- a/siro_driver/lib/views/auth/syria/registration_view.dart +++ b/siro_driver/lib/views/auth/syria/registration_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../constant/box_name.dart'; +import '../../../constant/links.dart'; import '../../../controller/auth/syria/registration_controller.dart'; import '../../../main.dart'; @@ -116,8 +117,17 @@ class RegistrationView extends StatelessWidget { if (v == null || v.isEmpty) { return 'Required field'.tr; } - if (v.length != 11) { - return 'National ID must be 11 digits'.tr; + final country = box.read(BoxName.countryCode) ?? 'Jordan'; + int expected; + if (country == 'Syria') { + expected = 11; + } else if (country == 'Egypt') { + expected = 12; + } else { + expected = 10; // Jordan + } + if (v.length != expected) { + return 'National ID must be $expected digits'.tr; } return null; }, @@ -355,7 +365,9 @@ class RegistrationView extends StatelessWidget { // STEP 3 Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) { - final String linkUpload = + final String primaryLink = + '${AppLink.server}/auth/syria/uploadImage.php'; + const String backupLink = 'https://syria.intaleq.xyz/siro/auth/syria/uploadImage.php'; return GetBuilder( @@ -371,45 +383,51 @@ class RegistrationView extends StatelessWidget { _buildImagePickerBox( 'National ID (Front)'.tr, ctrl.docUrls['id_front'], - () async => await ctrl.choosImage(linkUpload, 'id_front'), + () async => + await ctrl.choosImage(primaryLink, 'id_front', backupLink: backupLink), ), _buildImagePickerBox( 'National ID (Back)'.tr, ctrl.docUrls['id_back'], - () async => await ctrl.choosImage(linkUpload, 'id_back'), + () async => + await ctrl.choosImage(primaryLink, 'id_back', backupLink: backupLink), ), _buildImagePickerBox( 'Driver License (Front)'.tr, ctrl.docUrls['driver_license'], - () async => await ctrl.choosImage(linkUpload, 'driver_license'), + () async => + await ctrl.choosImage(primaryLink, 'driver_license', backupLink: backupLink), ), if (box.read(BoxName.countryCode) == 'Syria') _buildImagePickerBox( 'Driver License (Back)'.tr, ctrl.docUrls['driver_license_back'], () async => - await ctrl.choosImage(linkUpload, 'driver_license_back'), + await ctrl.choosImage(primaryLink, 'driver_license_back', backupLink: backupLink), ), _buildImagePickerBox( 'Profile Picture'.tr, ctrl.docUrls['profile_picture'], - () async => await ctrl.choosImage(linkUpload, 'profile_picture'), + () async => + await ctrl.choosImage(primaryLink, 'profile_picture', backupLink: backupLink), ), _buildImagePickerBox( ctrl.getCriminalRecordTitle(), ctrl.docUrls['criminal_record'], - () async => await ctrl.choosImage(linkUpload, 'criminal_record'), + () async => + await ctrl.choosImage(primaryLink, 'criminal_record', backupLink: backupLink), ), _buildImagePickerBox( 'Car Registration (Front)'.tr, ctrl.docUrls['car_license_front'], () async => - await ctrl.choosImage(linkUpload, 'car_license_front'), + await ctrl.choosImage(primaryLink, 'car_license_front', backupLink: backupLink), ), _buildImagePickerBox( 'Car Registration (Back)'.tr, ctrl.docUrls['car_license_back'], - () async => await ctrl.choosImage(linkUpload, 'car_license_back'), + () async => + await ctrl.choosImage(primaryLink, 'car_license_back', backupLink: backupLink), ), ], ),