From 3d4e636fbe4448a5afca3755e5b0371d75673712 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Wed, 6 May 2026 17:13:24 +0300 Subject: [PATCH] Update: 2026-05-06 17:13:24 --- app/modules_app/auth/mobile_request_otp.php | 157 ++++++++++-------- .../auth/controllers/auth_controller.dart | 11 +- 2 files changed, 94 insertions(+), 74 deletions(-) diff --git a/app/modules_app/auth/mobile_request_otp.php b/app/modules_app/auth/mobile_request_otp.php index 1ded98a..51ce7ff 100644 --- a/app/modules_app/auth/mobile_request_otp.php +++ b/app/modules_app/auth/mobile_request_otp.php @@ -17,81 +17,94 @@ use App\Middleware\RateLimitMiddleware; // Rate limit: 3 OTP requests per minute per IP RateLimitMiddleware::check(3, 60); -$data = Security::sanitize(input()); +try { + $data = Security::sanitize(input()); -// 1. Validate -$errors = Validator::validate($data, [ - 'phone' => 'required', -]); + // 1. Validate + $errors = Validator::validate($data, [ + 'phone' => 'required', + ]); -if ($errors) { - json_error('رقم الهاتف مطلوب', 422, $errors); + if ($errors) { + json_error('رقم الهاتف مطلوب', 422, $errors); + } + + $phone = preg_replace('/[^0-9+]/', '', $data['phone']); + $phoneHash = hash('sha256', $phone); + + // 2. Find user by phone hash OR plain phone (Support both schemas) + $db = Database::getInstance(); + + // First, try to find by phone_hash. If it fails, we'll catch it. + try { + $stmt = $db->prepare("SELECT id, tenant_id, name, is_active FROM users WHERE phone_hash = ? LIMIT 1"); + $stmt->execute([$phoneHash]); + $user = $stmt->fetch(); + } catch (\PDOException $e) { + // Fallback to searching by plain phone if phone_hash column doesn't exist + $stmt = $db->prepare("SELECT id, tenant_id, name, is_active FROM users WHERE phone = ? LIMIT 1"); + $stmt->execute([$phone]); + $user = $stmt->fetch(); + } + + if (!$user) { + // Don't reveal if phone exists — generic message + json_success(null, 'إذا كان الرقم مسجلاً، سيتم إرسال رمز التحقق'); + exit; + } + + if (!$user['is_active']) { + json_error('الحساب معطّل. تواصل مع المسؤول.', 403); + } + + // 3. Generate OTP (6 digits) + $otp = str_pad((string)random_int(100000, 999999), 6, '0', STR_PAD_LEFT); + $otpHash = password_hash($otp, PASSWORD_DEFAULT); + $expiresAt = date('Y-m-d H:i:s', time() + 300); // 5 minutes + + // 4. Store OTP in database (or Redis if available) + $cacheDir = STORAGE_PATH . '/cache/otp'; + if (!is_dir($cacheDir)) { + mkdir($cacheDir, 0755, true); + } + + $otpData = [ + 'hash' => $otpHash, + 'user_id' => $user['id'], + 'attempts' => 0, + 'max_attempts' => 5, + 'expires_at' => time() + 300, + 'created_at' => time(), + ]; + + $fp = fopen($cacheDir . '/otp_' . $phoneHash . '.json', 'w'); + if ($fp) { + flock($fp, LOCK_EX); + fwrite($fp, json_encode($otpData)); + flock($fp, LOCK_UN); + fclose($fp); + } + + // 5. Send OTP via WhatsApp Proxy + $whatsappService = new \App\Services\WhatsAppProxyService(); + $message = "رمز التحقق لتطبيق مُصادَق:\n*{$otp}*\n\nصالح لمدة 5 دقائق."; + $result = $whatsappService->sendMessage($phone, $message); + + if (!$result['success']) { + error_log("ERROR: Failed to send OTP WhatsApp to phone: {$phone}"); + json_error('عذراً، فشل في إرسال رمز التحقق. الرجاء التأكد من صحة رقم الواتساب الخاص بك والمحاولة مرة أخرى.', 500, ['whatsapp_debug' => $result]); + } + + // Log for development (REMOVE IN PRODUCTION!) + if (env('APP_DEBUG', 'false') === 'true') { + error_log("DEV OTP for {$phone}: {$otp}"); + } + + json_success(['whatsapp_debug' => $result], 'إذا كان الرقم مسجلاً، سيتم إرسال رمز التحقق عبر واتساب'); + +} catch (\Exception $e) { + json_error('Internal Server Error: ' . $e->getMessage(), 500); } -$phone = preg_replace('/[^0-9+]/', '', $data['phone']); -$phoneHash = hash('sha256', $phone); - -// 2. Find user by phone hash -$db = Database::getInstance(); -$stmt = $db->prepare("SELECT id, tenant_id, name, is_active FROM users WHERE phone_hash = ? LIMIT 1"); -$stmt->execute([$phoneHash]); -$user = $stmt->fetch(); - -if (!$user) { - // Don't reveal if phone exists — generic message - json_success(null, 'إذا كان الرقم مسجلاً، سيتم إرسال رمز التحقق'); - exit; -} - -if (!$user['is_active']) { - json_error('الحساب معطّل. تواصل مع المسؤول.', 403); -} - -// 3. Generate OTP (6 digits) -$otp = str_pad((string)random_int(100000, 999999), 6, '0', STR_PAD_LEFT); -$otpHash = password_hash($otp, PASSWORD_DEFAULT); -$expiresAt = date('Y-m-d H:i:s', time() + 300); // 5 minutes - -// 4. Store OTP in database (or Redis if available) -// Using a simple approach: store in a cache file per phone -$cacheDir = STORAGE_PATH . '/cache/otp'; -if (!is_dir($cacheDir)) { - mkdir($cacheDir, 0755, true); -} - -$otpData = [ - 'hash' => $otpHash, - 'user_id' => $user['id'], - 'attempts' => 0, - 'max_attempts' => 5, - 'expires_at' => time() + 300, - 'created_at' => time(), -]; - -$fp = fopen($cacheDir . '/otp_' . $phoneHash . '.json', 'w'); -if ($fp) { - flock($fp, LOCK_EX); - fwrite($fp, json_encode($otpData)); - flock($fp, LOCK_UN); - fclose($fp); -} - -// 5. Send OTP via WhatsApp Proxy -$whatsappService = new \App\Services\WhatsAppProxyService(); -$message = "رمز التحقق لتطبيق مُصادَق:\n*{$otp}*\n\nصالح لمدة 5 دقائق."; -$result = $whatsappService->sendMessage($phone, $message); - -if (!$result['success']) { - error_log("ERROR: Failed to send OTP WhatsApp to phone: {$phone}"); - json_error('عذراً، فشل في إرسال رمز التحقق. الرجاء التأكد من صحة رقم الواتساب الخاص بك والمحاولة مرة أخرى.', 500, ['whatsapp_debug' => $result]); -} - -// Log for development (REMOVE IN PRODUCTION!) -if (env('APP_DEBUG', 'false') === 'true') { - error_log("DEV OTP for {$phone}: {$otp}"); -} - -json_success(['whatsapp_debug' => $result], 'إذا كان الرقم مسجلاً، سيتم إرسال رمز التحقق عبر واتساب'); - diff --git a/musadaq-app/lib/features/auth/controllers/auth_controller.dart b/musadaq-app/lib/features/auth/controllers/auth_controller.dart index 4b56baa..f742860 100644 --- a/musadaq-app/lib/features/auth/controllers/auth_controller.dart +++ b/musadaq-app/lib/features/auth/controllers/auth_controller.dart @@ -18,6 +18,10 @@ class AuthController extends GetxController { Future requestOtp(String phoneNumber) async { try { + if (phoneNumber.trim().isEmpty) { + AppSnackbar.showError('خطأ', 'الرجاء إدخال رقم الهاتف أولاً'); + return; + } isLoading.value = true; phone.value = phoneNumber; @@ -31,8 +35,11 @@ class AuthController extends GetxController { Get.toNamed(AppRoutes.OTP_VERIFY); } } on DioException catch (e, stackTrace) { - AppLogger.error('OTP Request Failed', e.response?.data, stackTrace); - AppSnackbar.showError('خطأ', e.response?.data['message'] ?? 'فشل الاتصال بالخادم'); + String errorMessage = 'فشل الاتصال بالخادم'; + if (e.response?.data != null && e.response?.data is Map) { + errorMessage = e.response?.data['message'] ?? errorMessage; + } + AppSnackbar.showError('خطأ', errorMessage); } finally { isLoading.value = false; }