Update: 2026-06-25 03:28:59
This commit is contained in:
75
backend/auth/syria/uploadImage.php
Normal file
75
backend/auth/syria/uploadImage.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================
|
||||||
|
// auth/syria/uploadImage.php
|
||||||
|
// رفع صور وثائق السائق (هوية، رخصة، صورة شخصية، ...)
|
||||||
|
// يخزّنها في مجلدات حسب الدولة: auth/uploads/{country}/
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../connect.php';
|
||||||
|
|
||||||
|
uploadLog("🚀 [uploadImage.php] Document upload started.");
|
||||||
|
|
||||||
|
// --------- قراءة الحقول ---------
|
||||||
|
$driverID = filterRequest('driverID');
|
||||||
|
$imageType = filterRequest('imageType');
|
||||||
|
$country = filterRequest('country');
|
||||||
|
|
||||||
|
if (empty($driverID)) {
|
||||||
|
jsonError('driverID is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($imageType)) {
|
||||||
|
jsonError('imageType is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------- تحديد الدولة ---------
|
||||||
|
$country = strtolower(trim($country ?: ''));
|
||||||
|
if (empty($country)) {
|
||||||
|
try {
|
||||||
|
$stmt = $con->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,
|
||||||
|
]);
|
||||||
@@ -282,13 +282,21 @@ class RegistrationController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type
|
/// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type
|
||||||
Future<void> choosImage(String link, String imageType) async {
|
Future<void> choosImage(String link, String imageType, {String? backupLink}) async {
|
||||||
|
if (isloading) return;
|
||||||
try {
|
try {
|
||||||
|
isloading = true;
|
||||||
|
update();
|
||||||
|
|
||||||
final pickedImage = await picker.pickImage(
|
final pickedImage = await picker.pickImage(
|
||||||
source: ImageSource.camera,
|
source: ImageSource.camera,
|
||||||
preferredCameraDevice: CameraDevice.rear,
|
preferredCameraDevice: CameraDevice.rear,
|
||||||
);
|
);
|
||||||
if (pickedImage == null) return;
|
if (pickedImage == null) {
|
||||||
|
isloading = false;
|
||||||
|
update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
image = File(pickedImage.path);
|
image = File(pickedImage.path);
|
||||||
|
|
||||||
@@ -305,14 +313,15 @@ class RegistrationController extends GetxController {
|
|||||||
IOSUiSettings(title: 'Cropper'.tr),
|
IOSUiSettings(title: 'Cropper'.tr),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
if (croppedFile == null) return;
|
if (croppedFile == null) {
|
||||||
|
isloading = false;
|
||||||
|
update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// صورة للمعاينة داخل التطبيق
|
// صورة للمعاينة داخل التطبيق
|
||||||
myImage = File(croppedFile.path);
|
myImage = File(croppedFile.path);
|
||||||
|
|
||||||
isloading = true;
|
|
||||||
update();
|
|
||||||
|
|
||||||
// ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت)
|
// ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت)
|
||||||
final File compressedImage = await compressImage(File(croppedFile.path));
|
final File compressedImage = await compressImage(File(croppedFile.path));
|
||||||
|
|
||||||
@@ -320,12 +329,14 @@ class RegistrationController extends GetxController {
|
|||||||
final driverId = box.read(BoxName.driverID);
|
final driverId = box.read(BoxName.driverID);
|
||||||
|
|
||||||
final payload = <String, String>{
|
final payload = <String, String>{
|
||||||
'driverID': driverId,
|
'driverID': driverId?.toString() ?? '',
|
||||||
'imageType': imageType, // مثال: driver_license_front
|
'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;
|
docUrls[imageType] = imageUrl;
|
||||||
@@ -340,68 +351,79 @@ class RegistrationController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ترفع الملف وترجع رابط الصورة النهائي كـ String مع إعادة المحاولة في حال فشل الاتصال
|
/// ترفع الملف وترجع رابط الصورة النهائي كـ String مع إعادة المحاولة.
|
||||||
|
/// يحاول الرابط الأساسي first، فإن فشل جرب الاحتياطي (إن وُجد).
|
||||||
Future<String> uploadImage(
|
Future<String> uploadImage(
|
||||||
File file, Map<String, String> data, String link) async {
|
File file,
|
||||||
int maxRetries = 3;
|
Map<String, String> data,
|
||||||
int attempt = 0;
|
String link, {
|
||||||
while (attempt < maxRetries) {
|
String? backupLink,
|
||||||
attempt++;
|
}) async {
|
||||||
try {
|
const int maxRetries = 3;
|
||||||
final uri = Uri.parse(link);
|
|
||||||
final request = http.MultipartRequest('POST', uri);
|
|
||||||
|
|
||||||
final _jwt = box.read(BoxName.jwt);
|
final List<String> urlsToTry = [link, if (backupLink != null && backupLink.trim().isNotEmpty) backupLink];
|
||||||
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
|
|
||||||
final headers = <String, String>{
|
|
||||||
'Authorization': 'Bearer $_token',
|
|
||||||
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
|
||||||
};
|
|
||||||
request.headers.addAll(headers);
|
|
||||||
|
|
||||||
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(
|
final _jwt = box.read(BoxName.jwt);
|
||||||
await http.MultipartFile.fromPath(
|
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
|
||||||
'image',
|
final headers = <String, String>{
|
||||||
file.path,
|
'Authorization': 'Bearer $_token',
|
||||||
filename: forcedName,
|
'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 ثانية لتناسب الاتصالات الضعيفة
|
request.files.add(
|
||||||
final streamed =
|
await http.MultipartFile.fromPath(
|
||||||
await request.send().timeout(const Duration(seconds: 120));
|
'image',
|
||||||
final res = await http.Response.fromStream(streamed);
|
file.path,
|
||||||
|
filename: forcedName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (res.statusCode != 200) {
|
data.forEach((k, v) => request.fields[k] = v);
|
||||||
throw Exception(
|
|
||||||
'Failed to upload image: ${res.statusCode} - ${res.body}');
|
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<File> compressImage(File file) async {
|
Future<File> compressImage(File file) async {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
|
import '../../../constant/links.dart';
|
||||||
import '../../../controller/auth/syria/registration_controller.dart';
|
import '../../../controller/auth/syria/registration_controller.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
|
|
||||||
@@ -116,8 +117,17 @@ class RegistrationView extends StatelessWidget {
|
|||||||
if (v == null || v.isEmpty) {
|
if (v == null || v.isEmpty) {
|
||||||
return 'Required field'.tr;
|
return 'Required field'.tr;
|
||||||
}
|
}
|
||||||
if (v.length != 11) {
|
final country = box.read(BoxName.countryCode) ?? 'Jordan';
|
||||||
return 'National ID must be 11 digits'.tr;
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -355,7 +365,9 @@ class RegistrationView extends StatelessWidget {
|
|||||||
|
|
||||||
// STEP 3
|
// STEP 3
|
||||||
Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) {
|
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';
|
'https://syria.intaleq.xyz/siro/auth/syria/uploadImage.php';
|
||||||
|
|
||||||
return GetBuilder<RegistrationController>(
|
return GetBuilder<RegistrationController>(
|
||||||
@@ -371,45 +383,51 @@ class RegistrationView extends StatelessWidget {
|
|||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'National ID (Front)'.tr,
|
'National ID (Front)'.tr,
|
||||||
ctrl.docUrls['id_front'],
|
ctrl.docUrls['id_front'],
|
||||||
() async => await ctrl.choosImage(linkUpload, 'id_front'),
|
() async =>
|
||||||
|
await ctrl.choosImage(primaryLink, 'id_front', backupLink: backupLink),
|
||||||
),
|
),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'National ID (Back)'.tr,
|
'National ID (Back)'.tr,
|
||||||
ctrl.docUrls['id_back'],
|
ctrl.docUrls['id_back'],
|
||||||
() async => await ctrl.choosImage(linkUpload, 'id_back'),
|
() async =>
|
||||||
|
await ctrl.choosImage(primaryLink, 'id_back', backupLink: backupLink),
|
||||||
),
|
),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'Driver License (Front)'.tr,
|
'Driver License (Front)'.tr,
|
||||||
ctrl.docUrls['driver_license'],
|
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')
|
if (box.read(BoxName.countryCode) == 'Syria')
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'Driver License (Back)'.tr,
|
'Driver License (Back)'.tr,
|
||||||
ctrl.docUrls['driver_license_back'],
|
ctrl.docUrls['driver_license_back'],
|
||||||
() async =>
|
() async =>
|
||||||
await ctrl.choosImage(linkUpload, 'driver_license_back'),
|
await ctrl.choosImage(primaryLink, 'driver_license_back', backupLink: backupLink),
|
||||||
),
|
),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'Profile Picture'.tr,
|
'Profile Picture'.tr,
|
||||||
ctrl.docUrls['profile_picture'],
|
ctrl.docUrls['profile_picture'],
|
||||||
() async => await ctrl.choosImage(linkUpload, 'profile_picture'),
|
() async =>
|
||||||
|
await ctrl.choosImage(primaryLink, 'profile_picture', backupLink: backupLink),
|
||||||
),
|
),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
ctrl.getCriminalRecordTitle(),
|
ctrl.getCriminalRecordTitle(),
|
||||||
ctrl.docUrls['criminal_record'],
|
ctrl.docUrls['criminal_record'],
|
||||||
() async => await ctrl.choosImage(linkUpload, 'criminal_record'),
|
() async =>
|
||||||
|
await ctrl.choosImage(primaryLink, 'criminal_record', backupLink: backupLink),
|
||||||
),
|
),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'Car Registration (Front)'.tr,
|
'Car Registration (Front)'.tr,
|
||||||
ctrl.docUrls['car_license_front'],
|
ctrl.docUrls['car_license_front'],
|
||||||
() async =>
|
() async =>
|
||||||
await ctrl.choosImage(linkUpload, 'car_license_front'),
|
await ctrl.choosImage(primaryLink, 'car_license_front', backupLink: backupLink),
|
||||||
),
|
),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'Car Registration (Back)'.tr,
|
'Car Registration (Back)'.tr,
|
||||||
ctrl.docUrls['car_license_back'],
|
ctrl.docUrls['car_license_back'],
|
||||||
() async => await ctrl.choosImage(linkUpload, 'car_license_back'),
|
() async =>
|
||||||
|
await ctrl.choosImage(primaryLink, 'car_license_back', backupLink: backupLink),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user