Compare commits
7
Commits
d695a4e812
...
0af4eed1ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0af4eed1ce | ||
|
|
b5e2bf2fed | ||
|
|
c4fd859257 | ||
|
|
29d3a8ae7e | ||
|
|
9cc14864a3 | ||
|
|
03f26ce825 | ||
|
|
012b334885 |
@@ -120,9 +120,10 @@ NABEH_API_KEY=<CHANGE_ME_SHARED_SECRET>
|
|||||||
SECRET_KEY_HMAC=<CHANGE_ME_HMAC_SECRET_FOR_SIGNED_URLS>
|
SECRET_KEY_HMAC=<CHANGE_ME_HMAC_SECRET_FOR_SIGNED_URLS>
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Security Configuration - Fingerprint
|
# Security Configuration - Fingerprint & Testers
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
FP_PEPPER=<CHANGE_ME_FINGERPRINT_PEPPER>
|
FP_PEPPER=<CHANGE_ME_FINGERPRINT_PEPPER>
|
||||||
|
ALLOWED_TESTER_EMAILS=driver_tester@siromove.com,passenger_tester@siromove.com
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Gemini AI Configuration
|
# Gemini AI Configuration
|
||||||
|
|||||||
@@ -11,6 +11,31 @@ $password = filterRequest('password');
|
|||||||
$audience = filterRequest('aud') ?? 'siro-driver-android'; // الافتراضي
|
$audience = filterRequest('aud') ?? 'siro-driver-android'; // الافتراضي
|
||||||
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
||||||
|
|
||||||
|
// 1. تطبيق حد معدل الطلبات (Rate Limiting) للفاحصين: 3 محاولات بالدقيقة لكل IP
|
||||||
|
$rateLimiter = new RateLimiter($redis);
|
||||||
|
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
|
||||||
|
|
||||||
|
if (!$email || !$password) {
|
||||||
|
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. التحقق من أن الحساب مخصص للفحص فقط (isTest check)
|
||||||
|
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: '';
|
||||||
|
$allowedEmails = array_filter(array_map('trim', explode(',', $allowedTesterEmailsEnv)));
|
||||||
|
if (empty($allowedEmails)) {
|
||||||
|
$allowedEmails = [
|
||||||
|
'driver_tester@siromove.com',
|
||||||
|
'passenger_tester@siromove.com',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$cleanEmail = strtolower(trim($email));
|
||||||
|
$isTester = in_array($cleanEmail, $allowedEmails) || substr($cleanEmail, -13) === '@siromove.com';
|
||||||
|
if (!$isTester) {
|
||||||
|
echo json_encode(["status" => "failure", "message" => "Access denied. Only tester accounts are allowed."]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
// تشفير الإيميل لاستخدامه في الاستعلام
|
// تشفير الإيميل لاستخدامه في الاستعلام
|
||||||
$encryptedEmail = $encryptionHelper->encryptData($email);
|
$encryptedEmail = $encryptionHelper->encryptData($email);
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,27 @@ $password = filterRequest("password");
|
|||||||
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
||||||
$audience = filterRequest('aud') ?: 'siro_passenger';
|
$audience = filterRequest('aud') ?: 'siro_passenger';
|
||||||
|
|
||||||
|
// 1. تطبيق حد معدل الطلبات (Rate Limiting) للفاحصين: 3 محاولات بالدقيقة لكل IP
|
||||||
|
$rateLimiter = new RateLimiter($redis);
|
||||||
|
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
|
||||||
|
|
||||||
if (!$email || !$password) {
|
if (!$email || !$password) {
|
||||||
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
|
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. التحقق من أن الحساب مخصص للفحص فقط (isTest check)
|
||||||
|
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: '';
|
||||||
|
$allowedEmails = array_filter(array_map('trim', explode(',', $allowedTesterEmailsEnv)));
|
||||||
|
|
||||||
|
|
||||||
|
$cleanEmail = strtolower(trim($email));
|
||||||
|
$isTester = in_array($cleanEmail, $allowedEmails) || substr($cleanEmail, -13) === '@siromove.com';
|
||||||
|
if (!$isTester) {
|
||||||
|
echo json_encode(["status" => "failure", "message" => "Access denied. Only tester accounts are allowed."]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$con = Database::get('main');
|
$con = Database::get('main');
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class RateLimiter
|
|||||||
// حدود مختلفة لكل نوع endpoint
|
// حدود مختلفة لكل نوع endpoint
|
||||||
private const LIMITS = [
|
private const LIMITS = [
|
||||||
'login' => ['requests' => 5, 'window' => 60], // 5 محاولات / دقيقة
|
'login' => ['requests' => 5, 'window' => 60], // 5 محاولات / دقيقة
|
||||||
|
'tester_login' => ['requests' => 3, 'window' => 60], // 3 محاولات / دقيقة
|
||||||
'otp' => ['requests' => 3, 'window' => 300], // 3 محاولات / 5 دقائق
|
'otp' => ['requests' => 3, 'window' => 300], // 3 محاولات / 5 دقائق
|
||||||
'register' => ['requests' => 3, 'window' => 3600], // 3 محاولات / ساعة
|
'register' => ['requests' => 3, 'window' => 3600], // 3 محاولات / ساعة
|
||||||
'api' => ['requests' => 120, 'window' => 60], // 120 طلب / دقيقة
|
'api' => ['requests' => 120, 'window' => 60], // 120 طلب / دقيقة
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ CREATE TABLE `adminUser` (
|
|||||||
`email` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'البريد مشفر (AES-GCM)',
|
`email` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'البريد مشفر (AES-GCM)',
|
||||||
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'bcrypt',
|
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'bcrypt',
|
||||||
`role` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'admin' COMMENT 'admin | super_admin | service',
|
`role` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'admin' COMMENT 'admin | super_admin | service',
|
||||||
|
`country` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'Jordan' COMMENT 'اسم الدولة: Syria, Egypt, Jordan, Iraq, ...',
|
||||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ class OtpHelper extends GetxController {
|
|||||||
static final String _checkAdminLogin =
|
static final String _checkAdminLogin =
|
||||||
'${AppLink.server}/Admin/auth/login.php';
|
'${AppLink.server}/Admin/auth/login.php';
|
||||||
|
|
||||||
|
var selectedCountry = 'Jordan'.obs;
|
||||||
|
bool isFirstRun = box.read(BoxName.countryCode) == null;
|
||||||
|
|
||||||
|
void changeCountry(String country) {
|
||||||
|
selectedCountry.value = country;
|
||||||
|
box.write(BoxName.countryCode, country);
|
||||||
|
}
|
||||||
|
|
||||||
/// إرسال OTP
|
/// إرسال OTP
|
||||||
static Future<bool> sendOtp(String phoneNumber) async {
|
static Future<bool> sendOtp(String phoneNumber) async {
|
||||||
try {
|
try {
|
||||||
@@ -163,6 +171,10 @@ class OtpHelper extends GetxController {
|
|||||||
String role = data['role'].toString().trim();
|
String role = data['role'].toString().trim();
|
||||||
await box.write('admin_role', role);
|
await box.write('admin_role', role);
|
||||||
Log.print('Admin role saved: $role');
|
Log.print('Admin role saved: $role');
|
||||||
|
|
||||||
|
if (role != 'super_admin' && data['country'] != null) {
|
||||||
|
await box.write(BoxName.countryCode, data['country']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (data['phone'] != null) {
|
if (data['phone'] != null) {
|
||||||
await box.write(BoxName.adminPhone, data['phone']);
|
await box.write(BoxName.adminPhone, data['phone']);
|
||||||
@@ -277,6 +289,10 @@ class OtpHelper extends GetxController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
|
if (box.read(BoxName.countryCode) == null) {
|
||||||
|
box.write(BoxName.countryCode, 'Jordan');
|
||||||
|
}
|
||||||
|
selectedCountry.value = box.read(BoxName.countryCode) ?? 'Jordan';
|
||||||
super.onInit();
|
super.onInit();
|
||||||
DeviceHelper.getDeviceFingerprint().then((deviceFingerprint) {
|
DeviceHelper.getDeviceFingerprint().then((deviceFingerprint) {
|
||||||
box.write(BoxName.fingerPrint, deviceFingerprint);
|
box.write(BoxName.fingerPrint, deviceFingerprint);
|
||||||
|
|||||||
@@ -297,7 +297,60 @@ class _AdminLoginPageState extends State<AdminLoginPage>
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (Get.find<OtpHelper>().isFirstRun) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// ── Country Dropdown ───────────────────────────
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.public_rounded,
|
||||||
|
color: _C.accent, size: 16),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'الدولة',
|
||||||
|
style: TextStyle(
|
||||||
|
color: _C.textSec,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Obx(() {
|
||||||
|
final otpHelper = Get.find<OtpHelper>();
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _C.inputBg,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: _C.border, width: 1),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
value: otpHelper.selectedCountry.value,
|
||||||
|
icon: const Icon(Icons.arrow_drop_down, color: _C.accent),
|
||||||
|
isExpanded: true,
|
||||||
|
dropdownColor: _C.card,
|
||||||
|
style: const TextStyle(color: _C.textPrimary, fontSize: 16),
|
||||||
|
items: ['Jordan', 'Egypt', 'Syria'].map((String country) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: country,
|
||||||
|
child: Text(country.tr),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
otpHelper.changeCountry(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
|
],
|
||||||
|
|
||||||
// ── Submit button ────────────────────────────
|
// ── Submit button ────────────────────────────
|
||||||
_isLoading
|
_isLoading
|
||||||
|
|||||||
@@ -21,13 +21,15 @@ class LoginController extends GetxController {
|
|||||||
|
|
||||||
final FlutterSecureStorage storage = const FlutterSecureStorage();
|
final FlutterSecureStorage storage = const FlutterSecureStorage();
|
||||||
|
|
||||||
void login() async {
|
var selectedCountry = 'Jordan'.obs;
|
||||||
final emailStr = email.text.trim();
|
bool isFirstRun = box.read(BoxName.countryCode) == null;
|
||||||
final detectedCountry = AppLink.detectCountryFromPhone(emailStr);
|
|
||||||
if (detectedCountry.isNotEmpty) {
|
void changeCountry(String country) {
|
||||||
await box.write(BoxName.countryCode, detectedCountry);
|
selectedCountry.value = country;
|
||||||
|
box.write(BoxName.countryCode, country);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void login() async {
|
||||||
// Ensure fingerprint is ready
|
// Ensure fingerprint is ready
|
||||||
String fingerprint = box.read(BoxName.fingerPrint) ?? '';
|
String fingerprint = box.read(BoxName.fingerPrint) ?? '';
|
||||||
if (fingerprint.isEmpty) {
|
if (fingerprint.isEmpty) {
|
||||||
@@ -183,6 +185,10 @@ class LoginController extends GetxController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() async {
|
void onInit() async {
|
||||||
|
if (box.read(BoxName.countryCode) == null) {
|
||||||
|
await box.write(BoxName.countryCode, 'Jordan');
|
||||||
|
}
|
||||||
|
selectedCountry.value = box.read(BoxName.countryCode) ?? 'Jordan';
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
await DeviceHelper.getDeviceFingerprint();
|
await DeviceHelper.getDeviceFingerprint();
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,37 @@ class LoginPage extends StatelessWidget {
|
|||||||
hint: 'أدخل كلمة المرور',
|
hint: 'أدخل كلمة المرور',
|
||||||
type: TextInputType.visiblePassword,
|
type: TextInputType.visiblePassword,
|
||||||
),
|
),
|
||||||
|
if (controller.isFirstRun) ...[
|
||||||
|
Obx(() => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
value: controller.selectedCountry.value,
|
||||||
|
icon: const Icon(Icons.arrow_drop_down, color: AppColor.blueColor),
|
||||||
|
isExpanded: true,
|
||||||
|
dropdownColor: Colors.white,
|
||||||
|
style: const TextStyle(color: Colors.black87, fontSize: 16),
|
||||||
|
items: ['Jordan', 'Egypt', 'Syria'].map((String country) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: country,
|
||||||
|
child: Text(country.tr),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
controller.changeCountry(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
|
|
||||||
// Login Button
|
// Login Button
|
||||||
Container(
|
Container(
|
||||||
|
|||||||
Reference in New Issue
Block a user