diff --git a/backend/auth/otp/verify.php b/backend/auth/otp/verify.php index 66cfb03e..11201da6 100644 --- a/backend/auth/otp/verify.php +++ b/backend/auth/otp/verify.php @@ -147,7 +147,70 @@ try { $stmtUpd = $con->prepare($sqlUpdate); $stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT); $stmtUpd->execute(); - jsonSuccess(null, "Your phone number has been verified."); + + // ═══════════════════════════════════════════════════════ + // تغيير الجهاز: التحقق وحده لا يكفي. لا بد من ربط بصمة + // الجهاز الجديد ثم إصدار توكن driver — وإلا بقي التطبيق + // على توكن التسجيل ورجع كل endpoint محمي 403، وهو ما كان + // يجعل شاشة OTP "تنجح" ثم لا يعمل شيء بعدها. + // ═══════════════════════════════════════════════════════ + $chk = $con->prepare("SELECT id FROM driver WHERE phone = ? OR (? IS NOT NULL AND phone_bidx = ?)"); + global $blindIndex; + $phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone_number) : null; + $chk->execute([$encryptionHelper->encryptData($phone_number), $phoneBidx, $phoneBidx]); + $driverRow = $chk->fetch(PDO::FETCH_ASSOC); + + if (!$driverRow) { + jsonSuccess(null, "Your phone number has been verified."); + exit; + } + + $driverID = (string)$driverRow['id']; + + $fpRaw = filterRequest("fingerprint") ?? filterRequest("fingerPrint") ?? ($_SERVER['HTTP_X_DEVICE_FP'] ?? null); + if ($fpRaw === null && function_exists('getallheaders')) { + $hdrs = array_change_key_case(getallheaders(), CASE_LOWER); + $fpRaw = $hdrs['x-device-fp'] ?? null; + } + + if (empty($fpRaw)) { + jsonError("Device fingerprint is required to complete device change."); + exit; + } + + $fpPepper = getenv('FP_PEPPER') ?: ''; + $fpHashed = !empty($fpPepper) ? hash('sha256', $fpRaw . $fpPepper) : $fpRaw; + $fcmToken = filterRequest("token"); + + // ربط الجهاز الجديد — نفس الصيغة التي يتوقعها loginJwtDriver. + $hasRow = $con->prepare("SELECT id FROM driverToken WHERE captain_id = ?"); + $hasRow->execute([$driverID]); + if ($hasRow->fetch(PDO::FETCH_ASSOC)) { + if (!empty($fcmToken)) { + $con->prepare("UPDATE driverToken SET fingerPrint = ?, token = ? WHERE captain_id = ?") + ->execute([$fpHashed, $encryptionHelper->encryptData($fcmToken), $driverID]); + } else { + $con->prepare("UPDATE driverToken SET fingerPrint = ? WHERE captain_id = ?") + ->execute([$fpHashed, $driverID]); + } + } else { + $con->prepare("INSERT INTO driverToken (token, fingerPrint, captain_id, created_at) VALUES (?, ?, ?, NOW())") + ->execute([ + !empty($fcmToken) ? $encryptionHelper->encryptData($fcmToken) : '', + $fpHashed, + $driverID, + ]); + } + + $audDriver = filterRequest("aud") ?: filterRequest("audience") ?: (getenv('allowedDriver2') ?: 'driver-app:ios'); + $jwtSvc = new JwtService($redis); + $accessToken = $jwtSvc->generateAccessToken($driverID, 'driver', $audDriver, $fpRaw); + + jsonSuccess([ + "driverID" => $driverID, + "jwt" => $accessToken, + "access_token" => $accessToken, + ], "Your phone number has been verified."); } else { jsonError("Your phone number could not be verified or the code is expired. Please try again."); } diff --git a/backend/loginJwtDriver.php b/backend/loginJwtDriver.php index 56aadf0c..7e47411b 100644 --- a/backend/loginJwtDriver.php +++ b/backend/loginJwtDriver.php @@ -78,15 +78,28 @@ try { unauthorizedDriver(); } - $fpPepper = getenv('FP_PEPPER') ?: ''; - $expectedFp = !empty($fpPepper) ? hash('sha256', $fingerprint . $fpPepper) : $fingerprint; + // driverToken.fingerPrint مكتوب بصيغتين حسب آخر endpoint لمسه: + // • خام — من verify.php و ride/firebase/addDriver.php + // • hash مملّح — من loginJwtWalletDriver.php ومن هنا + // القبول على صيغة واحدة فقط كان يرفض كل سائق OTP لم يفتح المحفظة + // بعد (401 دائم ⇒ يستحيل ترقية توكن التسجيل). نقبل الاثنتين ثم + // نُوحّد الصف على الصيغة المُهشّمة. + $fpPepper = getenv('FP_PEPPER') ?: ''; + $storedFp = $fpRow['fingerPrint']; + $hashedFp = !empty($fpPepper) ? hash('sha256', $fingerprint . $fpPepper) : $fingerprint; - if (!hash_equals($fpRow['fingerPrint'], $expectedFp)) { + if (!hash_equals($storedFp, $hashedFp) && !hash_equals($storedFp, $fingerprint)) { securityLog("LoginDriver(OTP): fingerprint mismatch", [ 'driver_id' => $driver['id'], ]); unauthorizedDriver(); } + + // توحيد التخزين على الصيغة المُهشّمة (لا نحفظ البصمة الخام). + if (!hash_equals($storedFp, $hashedFp)) { + $con->prepare('UPDATE driverToken SET fingerPrint = :fp WHERE captain_id = :cid') + ->execute([':fp' => $hashedFp, ':cid' => $driver['id']]); + } // ✅ تم التحقق عبر fingerprint — نتابع لتوليد JWT } else { // ── المسار التقليدي: التحقق عبر password + HMAC ────────── diff --git a/siro_driver/lib/controller/auth/captin/login_captin_controller.dart b/siro_driver/lib/controller/auth/captin/login_captin_controller.dart index 1e722062..5066fbf9 100755 --- a/siro_driver/lib/controller/auth/captin/login_captin_controller.dart +++ b/siro_driver/lib/controller/auth/captin/login_captin_controller.dart @@ -261,38 +261,75 @@ class LoginDriverController extends GetxController { } } + /// يفكّ payload الـ JWT محلياً (بدون التحقق من التوقيع) لفحص نوعه فقط. + static Map? decodeJwtPayload(String? token) { + if (token == null || token.isEmpty) return null; + try { + final parts = token.split('.'); + if (parts.length != 3) return null; + String payload = parts[1]; + switch (payload.length % 4) { + case 2: + payload += '=='; + break; + case 3: + payload += '='; + break; + } + final decoded = jsonDecode(utf8.decode(base64Url.decode(payload))); + return decoded is Map ? Map.from(decoded) : null; + } catch (_) { + return null; + } + } + + /// توكن تسجيل (`registration` / `new`): يصدره loginFirstTimeDriver قبل أن + /// يصير للسائق id. الباك إند يسمح له بمسارات التسجيل فقط، وأي endpoint + /// آخر يردّ 403 "Token not authorized for this action". + static bool isRegistrationToken(Map payload) { + final type = payload['token_type']?.toString(); + return type == 'registration' || + type == 'new' || + payload['user_id']?.toString() == 'new'; + } + Future _getJwtInternal() async { await EncryptionHelper.initialize(); - String? secureJwt = await storage.read(key: BoxName.jwt); - if (secureJwt != null && secureJwt.isNotEmpty) { - bool isTokenValid = false; - try { - final parts = secureJwt.split('.'); - if (parts.length == 3) { - 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) { - isTokenValid = - DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000); - } - } - } catch (_) {} + dev = Platform.isAndroid ? 'android' : 'ios'; + final driverId = box.read(BoxName.driverID)?.toString() ?? ''; + final isRegistering = driverId.isEmpty; - if (isTokenValid) { - Log.print('🔑 Valid JWT found in secure storage. Skipping generation.'); - _jwtFailures = 0; - _jwtCooldownUntil = DateTime(2000); - return true; + String? secureJwt = await storage.read(key: BoxName.jwt); + final cached = decodeJwtPayload(secureJwt); + if (cached != null) { + final exp = cached['exp']; + final notExpired = exp is int && + DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000); + + if (notExpired) { + // ═══════════════════════════════════════════════════════════ + // الصلاحية الزمنية وحدها لا تكفي: توكن التسجيل يبقى صالحاً + // ساعة كاملة لكنه بلا صلاحيات. لو اكتفينا بفحص exp يبقى + // مخزّناً بعد اكتمال الدخول فيردّ كل endpoint محمي 403. + // ═══════════════════════════════════════════════════════════ + if (isRegistrationToken(cached) && !isRegistering) { + Log.print( + '♻️ getJWT: توكن registration مع driverID موجود — ترقية إلى توكن driver.', + ); + } else if (!isRegistering && + cached['user_id'] != null && + cached['user_id'].toString() != driverId) { + // توكن يخص سائقاً/جهازاً سابقاً على نفس التثبيت. + Log.print( + '♻️ getJWT: التوكن مُصدر لـ ${cached['user_id']} لا $driverId — إعادة توليد.', + ); + } else { + Log.print('🔑 Valid JWT found in secure storage. Skipping generation.'); + _jwtFailures = 0; + _jwtCooldownUntil = DateTime(2000); + return true; + } } } @@ -303,51 +340,9 @@ class LoginDriverController extends GetxController { return false; } - dev = Platform.isAndroid ? 'android' : 'ios'; - final driverId = box.read(BoxName.driverID); - final isRegistering = driverId == null || driverId.toString().isEmpty; - if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') { - var payload = { - 'id': box.read(BoxName.driverID) ?? AK.newId, - 'password': AK.passnpassenger, - 'aud': '${AK.allowed}$dev', - 'fingerPrint': - box.read(BoxName.deviceFingerprint) ?? - await DeviceHelper.getDeviceFingerprint(), - }; - - var response0 = await http - .post(Uri.parse(AppLink.loginFirstTimeDriver), body: payload) - .timeout(const Duration(seconds: 30)); - - if (response0.statusCode == 429) { - final retryAfter = - int.tryParse(response0.headers['retry-after'] ?? '') ?? 60; - _jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter)); - Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s'); - return false; - } - - if (response0.statusCode == 200) { - final decodedResponse1 = jsonDecode(response0.body); - String? jwt; - if (decodedResponse1['message'] is Map && - decodedResponse1['message']['jwt'] != null) { - jwt = decodedResponse1['message']['jwt']; - } else { - jwt = decodedResponse1['jwt']; - } - - if (jwt != null) { - await storage.write(key: BoxName.jwt, value: jwt); - await EncryptionHelper.initialize(); - return _onJwtSuccess(); - } - return _onJwtFailure('firstTime: لا يوجد jwt بالرد'); - } - return _onJwtFailure('firstTime: HTTP ${response0.statusCode}'); + return _getRegistrationJwt(); } else { await EncryptionHelper.initialize(); @@ -387,14 +382,80 @@ class LoginDriverController extends GetxController { if (jwt != null) { await storage.write(key: BoxName.jwt, value: jwt); + needsDeviceVerification = false; return _onJwtSuccess(); } return _onJwtFailure('renew: لا يوجد jwt بالرد'); } + + if (response1.statusCode == 401) { + // سائق لم يُكمل التسجيل بعد: عنده driverID من تحقق الهاتف لكن لا + // صفّ له في جدول driver، فـ loginJwtDriver يردّ 401 دائماً. هنا + // توكن التسجيل هو الصحيح — وبدون هذا الرجوع يعلق في منتصف التسجيل. + if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') { + Log.print('↩️ getJWT: التسجيل لم يكتمل — الرجوع لتوكن التسجيل.'); + return _getRegistrationJwt(); + } + + // سائق مكتمل: البصمة لا تطابق المخزّنة (جهاز جديد). الحل الوحيد + // OTP يعيد ربط البصمة — لا فائدة من إعادة المحاولة، فنرفع العلم + // ليحوّله loginDriver لصفحة التحقق. + needsDeviceVerification = true; + Log.print('🔐 getJWT: 401 من loginJwtDriver — الجهاز يحتاج تحقق OTP.'); + } return _onJwtFailure('renew: HTTP ${response1.statusCode}'); } } + /// يُرفع عندما يرفض loginJwtDriver بصمة هذا الجهاز (401): السائق مسجّل + /// لكن الجهاز غير مربوط، ولا يمكن إصدار توكن driver قبل تحقق OTP. + static bool needsDeviceVerification = false; + + /// توكن التسجيل — صالح لمسارات التسجيل وحدها (loginFromGoogle، register، + /// رفع المستندات…). يُستعمل قبل وجود صفّ للسائق، ويُستبدل بتوكن driver + /// فور اكتمال الدخول. + Future _getRegistrationJwt() async { + final payload = { + 'id': box.read(BoxName.driverID) ?? AK.newId, + 'password': AK.passnpassenger, + 'aud': '${AK.allowed}$dev', + 'fingerPrint': + box.read(BoxName.deviceFingerprint) ?? + await DeviceHelper.getDeviceFingerprint(), + }; + + final response0 = await http + .post(Uri.parse(AppLink.loginFirstTimeDriver), body: payload) + .timeout(const Duration(seconds: 30)); + + if (response0.statusCode == 429) { + final retryAfter = + int.tryParse(response0.headers['retry-after'] ?? '') ?? 60; + _jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter)); + Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s'); + return false; + } + + if (response0.statusCode == 200) { + final decodedResponse1 = jsonDecode(response0.body); + String? jwt; + if (decodedResponse1['message'] is Map && + decodedResponse1['message']['jwt'] != null) { + jwt = decodedResponse1['message']['jwt']; + } else { + jwt = decodedResponse1['jwt']; + } + + if (jwt != null) { + await storage.write(key: BoxName.jwt, value: jwt); + await EncryptionHelper.initialize(); + return _onJwtSuccess(); + } + return _onJwtFailure('firstTime: لا يوجد jwt بالرد'); + } + return _onJwtFailure('firstTime: HTTP ${response0.statusCode}'); + } + bool _onJwtSuccess() { _jwtFailures = 0; _jwtCooldownUntil = DateTime(2000); @@ -605,13 +666,37 @@ class LoginDriverController extends GetxController { box.write(BoxName.carTypeOfDriver, 'Awfar Car'); } - // ✅ الحصول على توكن access بدل registration قبل أي طلبات بعد تسجيل الدخول + // ═══════════════════════════════════════════════════════════ + // ترقية التوكن: حتى هنا كنا نستعمل توكن registration (المسموح + // له بـ loginFromGoogle فقط). الآن صار عندنا driverID، فنستبدله + // بتوكن driver قبل أي طلب لاحق — بدون هذه الخطوة يردّ كل + // endpoint محمي 403 "Token not authorized for this action". + // ═══════════════════════════════════════════════════════════ Log.print('🔑 Getting access token after login...'); String fingerPrint = await DeviceHelper.getDeviceFingerprint(); - // await storage.write( - // key: BoxName.fingerPrint, value: fingerPrint.toString()); - // await getJWT(); - // Log.print('🔑 Access token obtained.'); + await storage.write(key: BoxName.fingerPrint, value: fingerPrint); + final upgraded = await getJWT(force: true); + Log.print( + upgraded + ? '🔑 Access token obtained.' + : '⚠️ تعذّر الحصول على توكن driver بعد الدخول.', + ); + + // جهاز جديد: البصمة غير مربوطة، فلا يمكن إصدار توكن driver إلا + // بعد OTP يعيد الربط. نحوّله مباشرة بدل إدخاله بتوكن بلا صلاحيات. + if (!upgraded && needsDeviceVerification) { + isloading = false; + update(); + Get.offAll( + () => OtpVerificationPage( + phone: d['phone'].toString(), + deviceToken: fingerPrint, + token: '', + ptoken: '', + ), + ); + return true; + } // add invitations if (box.read(BoxName.isInstall) == null || @@ -638,8 +723,15 @@ class LoginDriverController extends GetxController { final serverFp = serverData['data'][0]['fingerPrint'].toString(); final tokenMismatch = storedToken.isNotEmpty && serverToken != storedToken; - final fingerprintMismatch = - serverFp.isNotEmpty && serverFp != fingerPrint.toString(); + // البصمة تُخزَّن مُهشّمة (sha256 + FP_PEPPER) بعد أول تجديد + // توكن. التطبيق لا يملك الـ pepper فلا يستطيع مقارنتها — + // ومقارنتها خاماً تعطي "اختلاف" دائماً وتفرض OTP بلا سبب. + // في هذه الحالة نتّكل على تحقّق السيرفر عند إصدار التوكن + // (loginJwtDriver يردّ 401 لجهاز غير مربوط). + final isHashedFp = RegExp(r'^[a-f0-9]{64}$').hasMatch(serverFp); + final fingerprintMismatch = serverFp.isNotEmpty && + !isHashedFp && + serverFp != fingerPrint.toString(); Log.print( '🔍 [FP_COMPARE] serverFp: $serverFp | localFp: ${fingerPrint.toString()} | fingerprintMismatch: $fingerprintMismatch | tokenMismatch: $tokenMismatch | serverToken: $serverToken | storedToken: $storedToken', ); diff --git a/siro_driver/lib/controller/auth/captin/opt_token_controller.dart b/siro_driver/lib/controller/auth/captin/opt_token_controller.dart index ac1af47a..c36ec200 100644 --- a/siro_driver/lib/controller/auth/captin/opt_token_controller.dart +++ b/siro_driver/lib/controller/auth/captin/opt_token_controller.dart @@ -10,6 +10,7 @@ import '../../../main.dart'; import '../../../views/widgets/error_snakbar.dart'; import '../../firebase/notification_service.dart'; import '../../functions/crud.dart'; +import 'login_captin_controller.dart'; class OtpVerificationController extends GetxController { final String phone; @@ -95,6 +96,25 @@ class OtpVerificationController extends GetxController { response != 'no_internet') { Log.print('response (already decoded): ${response}'); + // ═══════════════════════════════════════════════════════════ + // التحقق نجح ⇒ السيرفر ربط بصمة هذا الجهاز وأصدر توكن driver. + // نحفظه فوراً: هو التوكن الوحيد الذي يملك صلاحيات بعد تغيير + // الجهاز، وبدونه تبقى كل الطلبات على 403. + // ═══════════════════════════════════════════════════════════ + final issuedJwt = (response is Map && response['message'] is Map) + ? response['message']['jwt']?.toString() + : null; + if (issuedJwt != null && issuedJwt.isNotEmpty) { + await storage.write(key: BoxName.jwt, value: issuedJwt); + LoginDriverController.needsDeviceVerification = false; + Log.print('🔑 تم حفظ توكن driver بعد تحقق تغيير الجهاز.'); + } else { + // السيرفر لم يرجع توكناً — نطلب واحداً الآن بعد أن صارت + // البصمة مربوطة، وإلا دخل الكابتن بتوكن بلا صلاحيات. + LoginDriverController.needsDeviceVerification = false; + await Get.put(LoginDriverController()).getJWT(force: true); + } + // توجه إلى الصفحة التالية await CRUD().post( link: AppLink.addTokensDriver, @@ -104,15 +124,19 @@ class OtpVerificationController extends GetxController { 'captain_id': box.read(BoxName.driverID).toString(), }); - await NotificationService.sendNotification( - target: ptoken.toString(), - title: 'token change'.tr, - body: 'token change'.tr, - isTopic: false, - tone: 'cancel', - driverList: [], - category: 'token change', - ); + // إشعار الجهاز القديم — يُرسل فقط إذا عرفنا توكنه. عند دخول جهاز + // جديد لا نملك ptoken (تعذّر جلبه بلا صلاحيات) فنتخطّاه. + if (ptoken.isNotEmpty) { + await NotificationService.sendNotification( + target: ptoken, + title: 'token change'.tr, + body: 'token change'.tr, + isTopic: false, + tone: 'cancel', + driverList: [], + category: 'token change', + ); + } Get.offAll(() => HomeCaptain()); } else { diff --git a/siro_driver/lib/controller/auth/captin/phone_helper_controller.dart b/siro_driver/lib/controller/auth/captin/phone_helper_controller.dart index 0357ccfc..152c0abd 100644 --- a/siro_driver/lib/controller/auth/captin/phone_helper_controller.dart +++ b/siro_driver/lib/controller/auth/captin/phone_helper_controller.dart @@ -5,6 +5,7 @@ import 'package:siro_driver/print.dart'; import 'package:siro_driver/views/home/on_boarding_page.dart'; import 'package:siro_driver/views/widgets/error_snakbar.dart'; import 'package:siro_driver/controller/functions/country_logic.dart'; +import 'package:siro_driver/controller/functions/package_info.dart'; import '../../../constant/box_name.dart'; import '../../../constant/links.dart'; @@ -66,13 +67,18 @@ class PhoneAuthHelper { try { final fixedPhone = CountryLogic.formatCurrentCountryPhone(phoneNumber); Log.print('fixedPhone: $fixedPhone'); + final fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? + await DeviceHelper.getDeviceFingerprint(); final response = await CRUD().post( link: _verifyOtpUrl, payload: { 'phone_number': fixedPhone, 'token_code': otpCode, 'context': 'login', - 'user_type': 'driver' + 'user_type': 'driver', + // البصمة تُدمج داخل التوكن الذي يصدره السيرفر، وبدونها يُرفض + // كل طلب لاحق بـ 403 "Device verification required". + 'fingerPrint': fingerPrint, }, ); @@ -85,6 +91,14 @@ class PhoneAuthHelper { box.write(BoxName.phoneDriver, phoneNumber); box.write(BoxName.driverID, data['message']['driverID']); + // ✅ التحقق ينجح ⇒ السيرفر يصدر توكن driver كامل الصلاحيات. + // حفظه هنا هو ما يمنع بقاء توكن التسجيل مستعملاً بعد الدخول. + final issuedJwt = data['message']['jwt']?.toString(); + if (issuedJwt != null && issuedJwt.isNotEmpty) { + await storage.write(key: BoxName.jwt, value: issuedJwt); + Log.print('🔑 تم حفظ توكن driver الصادر من التحقق بالـ OTP.'); + } + if (isRegistered) { // ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية final driver = data['message']['driver']; diff --git a/siro_driver/lib/controller/functions/crud.dart b/siro_driver/lib/controller/functions/crud.dart index d52569dd..8810d16d 100755 --- a/siro_driver/lib/controller/functions/crud.dart +++ b/siro_driver/lib/controller/functions/crud.dart @@ -28,31 +28,42 @@ class CRUD { // ── فحص صلاحية JWT بدون مكتبات خارجية ────────────────────── static bool _isJwtValid(String? token) { - if (token == null || token.isEmpty) return false; - try { - final parts = token.split('.'); - if (parts.length != 3) return false; - // فك تشفير الـ payload (الجزء الثاني) - String payload = parts[1]; - // إضافة padding للـ base64 - 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 ثانية من انتهاء الصلاحية (buffer) - return DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000); - } catch (_) { + final decoded = LoginDriverController.decodeJwtPayload(token); + if (decoded == null) return false; + + final exp = decoded['exp']; + if (exp is! int) return false; + // نعتبر التوكن منتهي قبل 30 ثانية من انتهاء الصلاحية (buffer) + if (DateTime.now().millisecondsSinceEpoch >= (exp * 1000 - 30000)) { return false; } + + // توكن التسجيل صالح زمنياً لكن الباك إند يرفضه بـ 403 على أي endpoint + // محمي. ما دام عندنا driverID فالتسجيل انتهى ويلزمنا توكن driver. + if (LoginDriverController.isRegistrationToken(decoded) && + (box.read(BoxName.driverID)?.toString().isNotEmpty ?? false)) { + return false; + } + + return true; } + /// الباك إند يردّ 403 بهذه الرسالة تحديداً عندما يكون التوكن من نوع + /// registration على endpoint محمي — وهي الحالة الوحيدة من حالات الـ 403 + /// التي يحلّها تجديد التوكن (بقيّتها: بصمة جهاز / HMAC / سلامة التطبيق). + static bool _isUnauthorizedTokenType(String body) => + body.contains('Token not authorized for this action'); + + /// endpoints عامة لا يتحقق فيها الباك إند من التوكن (طلب/تحقق OTP، + /// تسجيل الدخول، التسجيل، تسجيل الأخطاء). حجب الطلب عنها عند غياب + /// التوكن يقفل بالضبط المسار الذي يستعيد به التطبيق توكناً صالحاً — + /// وهو ما كان يترك جهازاً جديداً عالقاً بلا مخرج. + static bool _isPublicEndpoint(String link) => + link.contains('/auth/otp/') || + link.contains('login') || + link.contains('register') || + link.contains('errorApp.php'); + static Future addError( String error, String details, String where) async { try { @@ -204,8 +215,9 @@ class CRUD { return 'rate_limited'; } - // 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط - if (sc == 401) { + // 401 (منتهي) أو 403 بتوكن تسجيل (بلا صلاحيات) → + // تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط + if (sc == 401 || (sc == 403 && _isUnauthorizedTokenType(body))) { // تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء) final isNonCritical = link.contains('errorApp.php'); if (isNonCritical || !allowRefresh) return 'token_expired'; @@ -245,11 +257,9 @@ class CRUD { Map? payload, }) async { String token = await _ensureJwt(); - if (token.isEmpty) { + if (token.isEmpty && !_isPublicEndpoint(link)) { // إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً. - if (!link.contains('login') && !link.contains('errorApp.php')) { - return 'token_expired'; - } + return 'token_expired'; } final headers = { @@ -292,7 +302,9 @@ class CRUD { var jsonData = jsonDecode(response.body); if (jsonData['status'] == 'success') return response.body; return jsonData['status']; - } else if (response.statusCode == 401) { + } else if (response.statusCode == 401 || + (response.statusCode == 403 && + _isUnauthorizedTokenType(response.body))) { // تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط // (allowRefresh: false تمنع أي تكرار إضافي) — نفس سلوك _makeRequest. if (!allowRefresh) return 'token_expired'; diff --git a/siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart b/siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart index 5f136b49..6cdc3016 100755 --- a/siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart +++ b/siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart @@ -350,29 +350,32 @@ class UserHeader extends StatelessWidget { ), child: controller.isloading ? const CircularProgressIndicator(color: Colors.white) - : CircleAvatar( - backgroundImage: (box - .read(BoxName.driverPhotoUrl) - ?.toString() - .isNotEmpty == - true) - ? NetworkImage(box.read(BoxName.driverPhotoUrl)) - : null, + : Builder(builder: (context) { + final photoUrl = + box.read(BoxName.driverPhotoUrl)?.toString() ?? ''; + final avatarImage = photoUrl.isNotEmpty + ? NetworkImage(photoUrl) + : null; - // [تعديل هام]: في حال فشل تحميل الصورة (غير موجودة) - onBackgroundImageError: (exception, stackTrace) { - // طباعة الخطأ في الكونسول للتوضيح - debugPrint( - "Profile image not found or error loading: $exception"); - // استدعاء نافذة التنبيه - _showUploadPhotoDialog(context, controller); - }, + return CircleAvatar( + backgroundImage: avatarImage, - // أيقونة بديلة تظهر في الخلفية إذا لم تكن الصورة موجودة - backgroundColor: Colors.grey.shade300, - child: const Icon(Icons.person, - size: 40, color: Colors.white), - ), + // Flutter يمنع onBackgroundImageError بلا صورة + // (assertion)، فنمرّره فقط عند وجود صورة فعلية. + onBackgroundImageError: avatarImage == null + ? null + : (exception, stackTrace) { + debugPrint( + "Profile image not found or error loading: $exception"); + _showUploadPhotoDialog(context, controller); + }, + + // أيقونة بديلة تظهر في الخلفية إذا لم تكن الصورة موجودة + backgroundColor: Colors.grey.shade300, + child: const Icon(Icons.person, + size: 40, color: Colors.white), + ); + }), ), Positioned( bottom: -5,