'required', ]); if ($errors) { json_error('رقم الهاتف مطلوب', 422, $errors); } $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 SMS // TODO: Replace with your actual SMS provider $smsSent = sendOtpSms($phone, $otp); if (!$smsSent) { error_log("WARN: Failed to send OTP SMS to phone hash: {$phoneHash}"); // Still return success to not reveal info, but log the issue } // Log for development (REMOVE IN PRODUCTION!) if (env('APP_DEBUG', 'false') === 'true') { error_log("DEV OTP for {$phone}: {$otp}"); } json_success(null, 'إذا كان الرقم مسجلاً، سيتم إرسال رمز التحقق'); // ─── SMS Helper ────────────────────────────────────────── function sendOtpSms(string $phone, string $otp): bool { $smsProvider = env('SMS_PROVIDER', 'log'); // 'log', 'twilio', 'jordan_sms', 'custom' $message = "رمز التحقق لتطبيق مُصادَق: {$otp}\nصالح لمدة 5 دقائق."; switch ($smsProvider) { case 'custom': // Custom SMS API (your own provider) $apiUrl = env('SMS_API_URL'); $apiKey = env('SMS_API_KEY'); if (!$apiUrl || !$apiKey) return false; try { $ch = curl_init($apiUrl); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ 'to' => $phone, 'message' => $message, 'api_key' => $apiKey, ]), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return $httpCode >= 200 && $httpCode < 300; } catch (\Exception $e) { error_log("SMS send error: " . $e->getMessage()); return false; } case 'log': default: // Development: just log the OTP error_log("SMS OTP [{$phone}]: {$otp}"); return true; } }