Update: 2026-08-08 13:33:53
This commit is contained in:
@@ -261,38 +261,75 @@ class LoginDriverController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// يفكّ payload الـ JWT محلياً (بدون التحقق من التوقيع) لفحص نوعه فقط.
|
||||
static Map<String, dynamic>? 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<String, dynamic>.from(decoded) : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// توكن تسجيل (`registration` / `new`): يصدره loginFirstTimeDriver قبل أن
|
||||
/// يصير للسائق id. الباك إند يسمح له بمسارات التسجيل فقط، وأي endpoint
|
||||
/// آخر يردّ 403 "Token not authorized for this action".
|
||||
static bool isRegistrationToken(Map<String, dynamic> payload) {
|
||||
final type = payload['token_type']?.toString();
|
||||
return type == 'registration' ||
|
||||
type == 'new' ||
|
||||
payload['user_id']?.toString() == 'new';
|
||||
}
|
||||
|
||||
Future<bool> _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<bool> _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',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user