From cc9c5885b3ffad6994d1968b99606ce74cdc901f Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 2 Aug 2026 01:39:21 +0300 Subject: [PATCH] refactor: implement single-flight JWT renewal with rate-limiting cooldown and fix storage token handling in auth controllers --- .../auth/captin/login_captin_controller.dart | 145 +++++++++++++++--- .../lib/controller/functions/crud.dart | 97 ++++++------ .../lib/controller/auth/login_controller.dart | 114 ++++++++++---- .../lib/controller/functions/crud.dart | 90 ++++++++--- 4 files changed, 325 insertions(+), 121 deletions(-) diff --git a/intaleq_driver/lib/controller/auth/captin/login_captin_controller.dart b/intaleq_driver/lib/controller/auth/captin/login_captin_controller.dart index 48972b8..e4e1dfd 100755 --- a/intaleq_driver/lib/controller/auth/captin/login_captin_controller.dart +++ b/intaleq_driver/lib/controller/auth/captin/login_captin_controller.dart @@ -141,7 +141,9 @@ class LoginDriverController extends GetxController { } var dev = ''; - Future? _walletJwtFuture; + // static: لأن CRUD ينشئ LoginDriverController() جديد بكل طلب، + // فلو كانت instance field ما بيشتغل الـ single-flight أبداً. + static Future? _walletJwtFuture; getJwtWallet() async { if (_walletJwtFuture != null) { @@ -228,7 +230,41 @@ class LoginDriverController extends GetxController { return ''; } - getJWT() async { + // ═══════════════════════════════════════════════════════════════ + // getJWT — تجديد توكن السائق + // • single-flight: طلب تجديد واحد فقط بنفس اللحظة (static) + // • بيرجّع true/false حتى المستدعي يعرف نجح ولا لأ + // • cooldown بعد الفشل: يمنع ضرب /loginJwtDriver.php (حده 5/دقيقة) + // بحلقة لا نهائية تنتهي بـ 429 "Please slow down" + // ═══════════════════════════════════════════════════════════════ + static Future? _jwtFuture; + static DateTime _jwtCooldownUntil = DateTime(2000); + static int _jwtFailures = 0; + + /// الوقت المتبقّي على انتهاء الـ cooldown (0 يعني مسموح التجديد) + static Duration get jwtCooldownRemaining { + final d = _jwtCooldownUntil.difference(DateTime.now()); + return d.isNegative ? Duration.zero : d; + } + + Future getJWT() async { + if (_jwtFuture != null) { + Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.'); + return _jwtFuture!; + } + _jwtFuture = _getJwtInternal().catchError((e) { + // أي استثناء (timeout/socket) لازم يتحوّل لـ false + cooldown، + // مش يطلع للمستدعي ويكسر الطلب اللي فوق + return _onJwtFailure('exception: $e'); + }); + try { + return await _jwtFuture!; + } finally { + _jwtFuture = null; + } + } + + Future _getJwtInternal() async { await EncryptionHelper.initialize(); // 1. Check secure storage first to avoid redundant API calls @@ -259,14 +295,31 @@ class LoginDriverController extends GetxController { if (isTokenValid) { Log.print('🔑 Valid JWT found in secure storage. Skipping generation.'); - return; + _jwtFailures = 0; + _jwtCooldownUntil = DateTime(2000); + return true; } } + // التوكن منتهي فعلاً — بس لو آخر محاولة فشلت لازم ننتظر قبل ما نعيد + if (DateTime.now().isBefore(_jwtCooldownUntil)) { + Log.print( + '🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.'); + return false; + } + dev = Platform.isAndroid ? 'android' : 'ios'; Log.print( 'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}'); - if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') { + // ⚠️ loginFirstTimeDriver.php بيرجّع توكن نوعه "registration" (صالح ساعة + // وone-time)، وباقي الـ endpoints بترفضه بـ 401. فما بنستخدمه إلا لما ما + // يكون عنا driverID أصلاً (سائق لسا ما تسجّل). أي سائق عنده ID → تجديد عادي + // عبر loginJwtDriver.php حتى لو firstTimeLoadKey ما انكتب. + 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, @@ -276,12 +329,21 @@ class LoginDriverController extends GetxController { }; // Log.print('payload: ${payload}'); - var response0 = await http.post( - Uri.parse(AppLink.loginFirstTimeDriver), - body: payload, - ); + var response0 = await http + .post( + Uri.parse(AppLink.loginFirstTimeDriver), + body: payload, + ) + .timeout(const Duration(seconds: 30)); Log.print('response0: ${response0.body}'); Log.print('request: ${response0.request}'); + 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); Log.print('decodedResponse1: ${decodedResponse1}'); @@ -297,31 +359,47 @@ class LoginDriverController extends GetxController { if (jwt != null) { // box.write(BoxName.jwt, c(jwt)); await storage.write(key: BoxName.jwt, value: jwt); + // ✅ بعد التأكد أن كل المفاتيح موجودة + await EncryptionHelper.initialize(); + return _onJwtSuccess(); } - - // ✅ بعد التأكد أن كل المفاتيح موجودة - await EncryptionHelper.initialize(); - - // await AppInitializer().getKey(); - } else {} + return _onJwtFailure('firstTime: لا يوجد jwt بالرد'); + } + return _onJwtFailure('firstTime: HTTP ${response0.statusCode}'); } else { await EncryptionHelper.initialize(); + // بدون driverID التجديد مضمون الفشل — نوقف بدون ما نضرب السيرفر + if (isRegistering) { + return _onJwtFailure('renew: لا يوجد driverID'); + } + var payload = { - 'id': box.read(BoxName.driverID), + 'id': driverId, 'password': box.read(BoxName.emailDriver), 'aud': '${AK.allowed}$dev', 'fingerPrint': box.read(BoxName.deviceFingerprint) ?? await DeviceHelper.getDeviceFingerprint(), }; // print(payload); - var response1 = await http.post( - Uri.parse(AppLink.loginJwtDriver), - body: payload, - ); + var response1 = await http + .post( + Uri.parse(AppLink.loginJwtDriver), + body: payload, + ) + .timeout(const Duration(seconds: 30)); Log.print('response1.request: ${response1.request}'); Log.print('response1.body: ${response1.body}'); + // 429 → السيرفر عامل rate limit؛ نحترم Retry-After ولا نعيد المحاولة + if (response1.statusCode == 429) { + final retryAfter = + int.tryParse(response1.headers['retry-after'] ?? '') ?? 60; + _jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter)); + Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث'); + return false; + } + if (response1.statusCode == 200) { final decodedResponse1 = jsonDecode(response1.body); // Log.print('decodedResponse1: ${decodedResponse1}'); @@ -337,13 +415,32 @@ class LoginDriverController extends GetxController { if (jwt != null) { // await box.write(BoxName.jwt, c(jwt)); await storage.write(key: BoxName.jwt, value: jwt); + return _onJwtSuccess(); } - - // await AppInitializer().getKey(); + return _onJwtFailure('renew: لا يوجد jwt بالرد'); } + return _onJwtFailure('renew: HTTP ${response1.statusCode}'); } } + // نجاح التجديد → تصفير العدّاد والـ cooldown + bool _onJwtSuccess() { + _jwtFailures = 0; + _jwtCooldownUntil = DateTime(2000); + Log.print('✅ getJWT: تم توليد توكن جديد.'); + return true; + } + + // فشل التجديد → backoff تصاعدي (2,4,8,16,32,60ث كحد أقصى) + // هذا هو اللي بيمنع الحلقة اللانهائية اللي بتوصل لـ rate limit + bool _onJwtFailure(String reason) { + _jwtFailures++; + final seconds = _jwtFailures >= 6 ? 60 : (1 << _jwtFailures); + _jwtCooldownUntil = DateTime.now().add(Duration(seconds: seconds)); + Log.print('❌ getJWT فشل ($reason) — محاولة #$_jwtFailures، cooldown ${seconds}ث'); + return false; + } + Future getLocationPermission() async { var status = await Permission.locationAlways.status; if (!status.isGranted) { @@ -654,9 +751,11 @@ class LoginDriverController extends GetxController { var jwt = jsonDecoeded['jwt']; // حفظ التوكن أولاً + // ⚠️ لازم يُخزَّن خام (بدون c()) — CRUD._getJwt() بيقرأه مباشرة + // ويحطه بـ Authorization، ولو كان مشفّر بصير كل طلب 401 → حلقة تجديد if (jwt != null) { - box.write(BoxName.jwt, c(jwt)); - await storage.write(key: BoxName.jwt, value: c(jwt)); + box.write(BoxName.jwt, jwt); + await storage.write(key: BoxName.jwt, value: jwt.toString()); } box.write(BoxName.emailDriver, (d['email'])); diff --git a/intaleq_driver/lib/controller/functions/crud.dart b/intaleq_driver/lib/controller/functions/crud.dart index c70eeb7..88b0819 100755 --- a/intaleq_driver/lib/controller/functions/crud.dart +++ b/intaleq_driver/lib/controller/functions/crud.dart @@ -22,7 +22,6 @@ import 'ssl_pinning.dart'; class CRUD { final NetGuard _netGuard = NetGuard(); final _client = SslPinning.createPinnedClient(); - static bool _isRefreshingJWT = false; static String _lastErrorSignature = ''; static DateTime _lastErrorTimestamp = DateTime(2000); static const Duration _errorLogDebounceDuration = Duration(minutes: 1); @@ -107,6 +106,21 @@ class CRUD { } } + // ═══════════════════════════════════════════════════════════════ + // _ensureJwt — يضمن وجود توكن صالح قبل الإرسال + // • getJWT() نفسها single-flight + فيها cooldown بعد الفشل، + // فما في داعي لأي flag هون — ولا في خطر حلقة لا نهائية. + // • بترجع التوكن الصالح أو '' لو التجديد فشل/بـ cooldown. + // ═══════════════════════════════════════════════════════════════ + Future _ensureJwt() async { + String token = await _getJwt(); + if (_isJwtValid(token)) return token; + + final ok = await Get.put(LoginDriverController()).getJWT(); + if (!ok) return ''; + return await _getJwt(); + } + // ═══════════════════════════════════════════════════════════════ // _makeRequest — دالة مركزية لكل الطلبات // ─────────────────────────────────────────────────────────────── @@ -119,6 +133,7 @@ class CRUD { required String link, Map? payload, required Map headers, + bool allowRefresh = true, }) async { // timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا const totalTimeout = Duration(seconds: 60); @@ -186,19 +201,33 @@ class CRUD { } } - // 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية) + // 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة + if (sc == 429) { + Log.print('🛑 [RES-$requestId] 429 rate limited — $link'); + return 'rate_limited'; + } + + // 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط if (sc == 401) { // تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء) final isNonCritical = link.contains('errorApp.php'); - if (!_isRefreshingJWT && !isNonCritical) { - _isRefreshingJWT = true; - try { - await Get.put(LoginDriverController()).getJWT(); - } finally { - _isRefreshingJWT = false; - } - } - return 'token_expired'; + if (isNonCritical || !allowRefresh) return 'token_expired'; + + final refreshed = await Get.put(LoginDriverController()).getJWT(); + if (!refreshed) return 'token_expired'; + + final newToken = await _getJwt(); + if (newToken.isEmpty) return 'token_expired'; + + // إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي + final retryHeaders = Map.from(headers) + ..['Authorization'] = 'Bearer $newToken'; + return await _makeRequest( + link: link, + payload: payload, + headers: retryHeaders, + allowRefresh: false, + ); } // 5xx @@ -218,18 +247,9 @@ class CRUD { required String link, Map? payload, }) async { - String token = await _getJwt(); - // فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض - if (!_isJwtValid(token) && !_isRefreshingJWT) { - _isRefreshingJWT = true; - try { - await Get.put(LoginDriverController()).getJWT(); - token = await _getJwt(); - } finally { - _isRefreshingJWT = false; - } - } + final String token = await _ensureJwt(); + if (token.isEmpty) return 'token_expired'; final headers = { 'Content-Type': 'application/x-www-form-urlencoded', @@ -250,16 +270,8 @@ class CRUD { }) async { try { // فحص صلاحية التوكن قبل الإرسال - String token = await _getJwt(); - if (!_isJwtValid(token) && !_isRefreshingJWT) { - _isRefreshingJWT = true; - try { - await Get.put(LoginDriverController()).getJWT(); - token = await _getJwt(); - } finally { - _isRefreshingJWT = false; - } - } + final String token = await _ensureJwt(); + if (token.isEmpty) return 'token_expired'; var url = Uri.parse(link); var response = await _client.post( @@ -279,15 +291,12 @@ class CRUD { var jsonData = jsonDecode(response.body); if (jsonData['status'] == 'success') return response.body; return jsonData['status']; + } else if (response.statusCode == 429) { + Log.print('🛑 get: 429 rate limited — $link'); + return 'rate_limited'; } else if (response.statusCode == 401) { - if (!_isRefreshingJWT) { - _isRefreshingJWT = true; - try { - await Get.put(LoginDriverController()).getJWT(); - } finally { - _isRefreshingJWT = false; - } - } + // تجديد واحد فقط؛ getJWT فيها single-flight + cooldown + await Get.put(LoginDriverController()).getJWT(); return 'token_expired'; } else if (response.statusCode >= 500) { addError('Non-200: ${response.statusCode}', 'crud().get - Other', @@ -572,12 +581,8 @@ class CRUD { // ── sendEmail — إصلاح: استخدام r() بدل X.r() القديم ───────── Future sendEmail(String link, Map? payload) async { // r() هي نفس دالة فك التشفير الثلاثي المختصرة - String token = await _getJwt(); - - if (!_isJwtValid(token)) { - await LoginDriverController().getJWT(); - token = await _getJwt(); - } + final String token = await _ensureJwt(); + if (token.isEmpty) return; final headers = { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/intaleq_rider/lib/controller/auth/login_controller.dart b/intaleq_rider/lib/controller/auth/login_controller.dart index 090b497..ba9e275 100644 --- a/intaleq_rider/lib/controller/auth/login_controller.dart +++ b/intaleq_rider/lib/controller/auth/login_controller.dart @@ -82,26 +82,55 @@ class LoginController extends GetxController { // • firstTimeLoadKey != false ← أول مرة يفتح التطبيق → loginFirstTime // • firstTimeLoadKey == false ← مستخدم موجود → loginJwtRider // ───────────────────────────────────────────────────────────── - Future getJWT({bool force = false}) async { - // إذا كان التوكن الحالي لا يزال صالحاً، لا داعي لطلب واحد جديد + static Future? _jwtFuture; + static DateTime _jwtCooldownUntil = DateTime(2000); + static int _jwtFailures = 0; + + static Duration get jwtCooldownRemaining { + final d = _jwtCooldownUntil.difference(DateTime.now()); + return d.isNegative ? Duration.zero : d; + } + + Future getJWT({bool force = false}) async { + if (_jwtFuture != null) { + Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.'); + return _jwtFuture!; + } + _jwtFuture = _getJwtInternal(force: force).catchError((e) { + return _onJwtFailure('exception: $e'); + }); + try { + return await _jwtFuture!; + } finally { + _jwtFuture = null; + } + } + + Future _getJwtInternal({bool force = false}) async { if (!force && isTokenValid()) { Log.print("JWT is still valid. Skipping request."); - return; + _jwtFailures = 0; + _jwtCooldownUntil = DateTime(2000); + return true; + } + + if (DateTime.now().isBefore(_jwtCooldownUntil)) { + Log.print( + '🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.'); + return false; } try { dev = Platform.isAndroid ? 'android' : 'ios'; - - // تأكد إن البصمة محدّثة قبل أي طلب await DeviceHelper.getDeviceFingerprint(); final String fp = box.read(BoxName.deviceFpEncrypted) ?? ''; + + final passengerId = box.read(BoxName.passengerID); + final isRegistering = passengerId == null || passengerId.toString().isEmpty; - if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') { - // ── أول تسجيل ───────────────────────────────────────── - // نرسل البصمة المشفرة مع باقي البيانات - // السيرفر سيعمل hash لها ويخزنها في JWT payload + if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') { var payload = { - 'id': box.read(BoxName.passengerID) ?? AK.newId, + 'id': passengerId ?? AK.newId, 'password': AK.passnpassenger, 'aud': '${AK.allowed}$dev', 'fingerPrint': fp, @@ -110,12 +139,14 @@ class LoginController extends GetxController { var response = await http.post( Uri.parse(AppLink.loginFirstTime), body: payload, - ); - Log.print('AppLink.loginFirstTime: ${AppLink.loginFirstTime}'); - - Log.print('payload: $payload'); - Log.print('response code: ${response.statusCode}'); - Log.print('response body: ${response.body}'); + ).timeout(const Duration(seconds: 30)); + + if (response.statusCode == 429) { + final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60; + _jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter)); + Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s'); + return false; + } if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -126,18 +157,20 @@ class LoginController extends GetxController { : decoded['jwt']); if (jwt != null) { - // نشفر الـ JWT بالتشفير الثلاثي قبل التخزين في GetStorage - box.write(BoxName.jwt, c(jwt)); - storage.write(key: BoxName.jwt, value: c(jwt)); + await storage.write(key: BoxName.jwt, value: jwt); + await EncryptionHelper.initialize(); + return _onJwtSuccess(); } - - await EncryptionHelper.initialize(); + return _onJwtFailure('firstTime: لا يوجد jwt بالرد'); } + return _onJwtFailure('firstTime: HTTP ${response.statusCode}'); } else { - // ── مستخدم موجود: تجديد التوكن + if (isRegistering) { + return _onJwtFailure('renew: لا يوجد passengerID'); + } var payload = { - 'id': box.read(BoxName.passengerID), + 'id': passengerId, 'fingerPrint': fp, 'aud': '${AK.allowed}$dev', }; @@ -145,11 +178,15 @@ class LoginController extends GetxController { var response = await http.post( Uri.parse(AppLink.loginJwtRider), body: payload, - ); - Log.print('AppLink.loginJwtRider: ${AppLink.loginJwtRider}'); + ).timeout(const Duration(seconds: 30)); + + if (response.statusCode == 429) { + final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60; + _jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter)); + Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث'); + return false; + } - Log.print('payload: $payload'); - Log.print('response: ${response.body}'); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); final String? jwt = decoded['data'] != null @@ -159,16 +196,33 @@ class LoginController extends GetxController { : decoded['jwt']); if (jwt != null) { - box.write(BoxName.jwt, c(jwt)); - storage.write(key: BoxName.jwt, value: c(jwt)); + await storage.write(key: BoxName.jwt, value: jwt); + return _onJwtSuccess(); } + return _onJwtFailure('renew: لا يوجد jwt بالرد'); } + return _onJwtFailure('renew: HTTP ${response.statusCode}'); } } catch (e) { - Log.print('Error in getJWT: $e'); + return _onJwtFailure('Error: $e'); } } + bool _onJwtSuccess() { + _jwtFailures = 0; + _jwtCooldownUntil = DateTime(2000); + Log.print('✅ getJWT: تم توليد توكن جديد بنجاح.'); + return true; + } + + bool _onJwtFailure(String reason) { + _jwtFailures++; + final seconds = _jwtFailures >= 6 ? 60 : (1 << _jwtFailures); + _jwtCooldownUntil = DateTime.now().add(Duration(seconds: seconds)); + Log.print('❌ getJWT فشل ($reason) — محاولة #$_jwtFailures، cooldown ${seconds}ث'); + return false; + } + // ───────────────────────────────────────────────────────────── // التحقق من صلاحية التوكن يدوياً (بدون مكاتب خارجية) // ───────────────────────────────────────────────────────────── diff --git a/intaleq_rider/lib/controller/functions/crud.dart b/intaleq_rider/lib/controller/functions/crud.dart index 6a9b90e..afd585f 100644 --- a/intaleq_rider/lib/controller/functions/crud.dart +++ b/intaleq_rider/lib/controller/functions/crud.dart @@ -24,7 +24,6 @@ class CRUD { final NetGuard _netGuard = NetGuard(); final _client = SslPinning.createPinnedClient(); - static bool _isRefreshingJWT = false; static String _lastErrorSignature = ''; static DateTime _lastErrorTimestamp = DateTime(2000); static const Duration _errorLogDebounceDuration = Duration(minutes: 1); @@ -98,31 +97,50 @@ class CRUD { Future _getJwt() async { try { - final String? encryptedJwt = await storage.read(key: BoxName.jwt); - if (encryptedJwt == null || encryptedJwt.isEmpty) { - final String? fallback = box.read(BoxName.jwt); + final jwt = await storage.read(key: BoxName.jwt); + if (jwt == null || jwt.toString().isEmpty) { + // إذا كان التخزين الآمن فارغاً، نحاول استخراج التوكن القديم من GetStorage للركاب القدامى + final fallback = box.read(BoxName.jwt); if (fallback != null) { - return r(fallback).toString().split(Env.addd)[0]; + try { + return r(fallback).toString().split(Env.addd)[0]; // فك تشفير القديم + } catch (_) { + return fallback.toString(); // ربما تم تخزينه بدون تشفير + } } return ''; } - return r(encryptedJwt).toString().split(Env.addd)[0]; - } catch (e) { - Log.print('Error reading JWT from SecureStorage: $e'); - final String? fallback = box.read(BoxName.jwt); - if (fallback != null) { - return r(fallback).toString().split(Env.addd)[0]; + // التحقق السريع إذا كان التوكن لا يزال مشفراً (يبدأ برموز غريبة وليس ey) + if (!jwt.startsWith('ey')) { + try { + return r(jwt).toString().split(Env.addd)[0]; + } catch (_) {} } + return jwt; + } catch (_) { return ''; } } + // ═══════════════════════════════════════════════════════════════ + // _ensureJwt — يضمن وجود توكن صالح قبل الإرسال + // ═══════════════════════════════════════════════════════════════ + Future _ensureJwt() async { + String token = await _getJwt(); + if (_isJwtValid(token)) return token; + + final ok = await Get.put(LoginController()).getJWT(); + if (!ok) return ''; + return await _getJwt(); + } + /// Centralized request handler with retry for weak networks. /// For Syria (3G): 60s total timeout, 3 retries, exponential backoff. Future _makeRequest({ required String link, Map? payload, required Map headers, + bool allowRefresh = true, }) async { const totalTimeout = Duration(seconds: 60); @@ -180,17 +198,33 @@ class CRUD { } } + // 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة + if (sc == 429) { + Log.print('🛑 [RES] 429 rate limited — $link'); + return 'rate_limited'; + } + + // 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط if (sc == 401) { + // تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء) final isNonCritical = link.contains('errorApp.php'); - if (!_isRefreshingJWT && !isNonCritical) { - _isRefreshingJWT = true; - try { - await Get.put(LoginController()).getJWT(); - } finally { - _isRefreshingJWT = false; - } - } - return 'token_expired'; + if (isNonCritical || !allowRefresh) return 'token_expired'; + + final refreshed = await Get.put(LoginController()).getJWT(); + if (!refreshed) return 'token_expired'; + + final newToken = await _getJwt(); + if (newToken.isEmpty) return 'token_expired'; + + // إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي + final retryHeaders = Map.from(headers) + ..['Authorization'] = 'Bearer $newToken'; + return await _makeRequest( + link: link, + payload: payload, + headers: retryHeaders, + allowRefresh: false, + ); } if (sc >= 500) { @@ -206,7 +240,14 @@ class CRUD { required String link, Map? payload, }) async { - String token = await _getJwt(); + String token = await _ensureJwt(); + if (token.isEmpty) { + // إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً. + // باستثناء تسجيل الدخول لأنه لا يحتاج توكن + if (!link.contains('login') && !link.contains('errorApp.php')) { + return 'token_expired'; + } + } final headers = { 'Content-Type': 'application/x-www-form-urlencoded', @@ -221,7 +262,12 @@ class CRUD { required String link, Map? payload, }) async { - String token = await _getJwt(); + String token = await _ensureJwt(); + if (token.isEmpty) { + if (!link.contains('login') && !link.contains('errorApp.php')) { + return 'token_expired'; + } + } final headers = { 'Content-Type': 'application/x-www-form-urlencoded',