refactor: implement single-flight JWT renewal with rate-limiting cooldown and fix storage token handling in auth controllers
This commit is contained in:
@@ -141,7 +141,9 @@ class LoginDriverController extends GetxController {
|
||||
}
|
||||
|
||||
var dev = '';
|
||||
Future<String>? _walletJwtFuture;
|
||||
// static: لأن CRUD ينشئ LoginDriverController() جديد بكل طلب،
|
||||
// فلو كانت instance field ما بيشتغل الـ single-flight أبداً.
|
||||
static Future<String>? _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<bool>? _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<bool> 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<bool> _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<void> 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']));
|
||||
|
||||
Reference in New Issue
Block a user