Files
Siro/backend/auth/otp/request.php
2026-06-12 20:40:40 +03:00

261 lines
11 KiB
PHP

<?php
// File: backend/auth/otp/request.php
// Unified OTP request endpoint with geographical routing (Syria, Egypt, Jordan)
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
require_once __DIR__ . '/providers.php';
// 1. Rate Limiting check (max 3 requests per 5 minutes per IP)
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'otp');
// 2. Fetch input parameters
$receiver = filterRequest("receiver");
if (empty($receiver)) {
$receiver = filterRequest("phone_number");
}
$user_type = filterRequest("user_type");
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? (function_exists('apache_request_headers') ? (apache_request_headers()['Authorization'] ?? null) : null);
if (!empty($authHeader) && preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
$jwtToken = $matches[1];
$tokenParts = explode('.', $jwtToken);
if (count($tokenParts) === 3) {
$payload = json_decode(base64_decode($tokenParts[1]), true);
if (isset($payload['role'])) {
$user_type = $payload['role'];
}
}
}
$country = filterRequest("country"); // Egypt | Syria | Jordan
$method = filterRequest("method"); // whatsapp | sms | voice | flash_call | bearer_send
$context = filterRequest("context"); // token_change | login (default)
// For driver registration context
$driverId = filterRequest("driverId");
$email = filterRequest("email");
if (empty($receiver)) {
jsonError("Phone number (receiver) is required.");
exit;
}
// Auto-detect country if empty
if (empty($country)) {
$cleanReceiver = preg_replace('/\D+/', '', $receiver);
if (strpos($cleanReceiver, '20') === 0 || (strlen($cleanReceiver) === 11 && strpos($cleanReceiver, '01') === 0)) {
$country = 'Egypt';
} elseif (strpos($cleanReceiver, '962') === 0 || (strlen($cleanReceiver) === 9 && strpos($cleanReceiver, '7') === 0)) {
$country = 'Jordan';
} elseif (strpos($cleanReceiver, '963') === 0 || (strlen($cleanReceiver) === 9 && strpos($cleanReceiver, '9') === 0)) {
$country = 'Syria';
} else {
$country = 'Jordan'; // Default fallback
}
}
// Auto-detect user_type if empty
if (empty($user_type)) {
if (!empty($driverId) || strpos($_SERVER['REQUEST_URI'], 'driver') !== false) {
$user_type = 'driver';
} else {
$user_type = 'passenger';
}
}
if (empty($user_type) || !in_array($user_type, ['passenger', 'driver', 'admin', 'service'])) {
jsonError("User type must be 'passenger', 'driver', 'admin', or 'service'.");
exit;
}
if ($user_type === 'admin') {
$allowedPhones = explode(',', getenv('ADMIN_PHONE_NUMBERS'));
if (!in_array($receiver, $allowedPhones)) {
error_log("⚠️ [Admin OTP] Unauthorized phone number attempted: $receiver");
jsonError("رقم الهاتف غير مصرح له.");
exit;
}
}
// 3. Establish DB Connection
try {
$con = Database::get('main');
} catch (Exception $e) {
http_response_code(500);
exit(json_encode(['error' => 'Database connection failed']));
}
// 4. Generate 3-digit OTP code
$otp = str_pad((string)random_int(0, 999), 3, '0', STR_PAD_LEFT);
// 5. Geographical Routing & Dispatch
$sentSuccessfully = false;
switch (strtolower($country)) {
case 'egypt':
$sentSuccessfully = sendKazumiSms($receiver, $otp);
if (!$sentSuccessfully) {
error_log("⚠️ [Egypt OTP Failover 1] Kazumi SMS failed. Falling back to Intaleq OTP WhatsApp.");
$sentSuccessfully = sendIntaleqOtp($receiver, $otp, 'whatsapp');
if (!$sentSuccessfully) {
error_log("⚠️ [Egypt OTP Failover 2] Intaleq OTP WhatsApp failed. Falling back to Nabeh JWT OTP text.");
$sentSuccessfully = sendNabehOtp($receiver, $otp, 'text');
}
}
break;
case 'syria':
// Syria uses dynamic Nabeh JWT for voice/image, static Intaleq app_key for whatsapp/sms
if ($method === 'bearer_send' || $method === 'voice' || $method === 'image') {
$sentSuccessfully = sendNabehOtp($receiver, $otp, $method);
if (!$sentSuccessfully) {
error_log("⚠️ [Syria OTP Failover] Nabeh JWT method failed. Falling back to Intaleq OTP WhatsApp.");
$sentSuccessfully = sendIntaleqOtp($receiver, $otp, 'whatsapp');
}
} else {
$sentSuccessfully = sendIntaleqOtp($receiver, $otp, $method ?: 'whatsapp');
if (!$sentSuccessfully) {
error_log("⚠️ [Syria OTP Failover] Intaleq OTP WhatsApp failed. Falling back to Nabeh JWT OTP text.");
$sentSuccessfully = sendNabehOtp($receiver, $otp, 'text');
}
}
break;
case 'jordan':
// Jordan uses dynamic Nabeh JWT for voice/image, static Intaleq app_key for sms/whatsapp
if ($method === 'bearer_send' || $method === 'voice' || $method === 'image') {
$sentSuccessfully = sendNabehOtp($receiver, $otp, $method);
if (!$sentSuccessfully) {
error_log("⚠️ [Jordan OTP Failover] Nabeh JWT method failed. Falling back to Intaleq OTP SMS.");
$sentSuccessfully = sendIntaleqOtp($receiver, $otp, 'sms');
}
} else {
$sentSuccessfully = sendIntaleqOtp($receiver, $otp, $method ?: 'sms');
if (!$sentSuccessfully) {
error_log("⚠️ [Jordan OTP Failover] Intaleq OTP SMS failed. Falling back to Nabeh JWT OTP text.");
$sentSuccessfully = sendNabehOtp($receiver, $otp, 'text');
}
}
break;
default:
// Default fallback to Kazumi SMS
$sentSuccessfully = sendKazumiSms($receiver, $otp);
if (!$sentSuccessfully) {
error_log("⚠️ [Default OTP Failover] Kazumi SMS failed. Falling back to Intaleq OTP WhatsApp.");
$sentSuccessfully = sendIntaleqOtp($receiver, $otp, 'whatsapp');
}
break;
}
// 6. DB Storage on Success
if ($sentSuccessfully) {
$encryptedPhone = $encryptionHelper->encryptData($receiver);
$encryptedOtp = $encryptionHelper->encryptData($otp);
$encryptedEmail = !empty($email) ? $encryptionHelper->encryptData($email) : '';
$expirationTime = date('Y-m-d H:i:s', strtotime('+5 minutes'));
try {
if ($user_type === 'admin') {
$stmt = $con->prepare("INSERT INTO token_verification_admin (phone_number, token, expiration_time)
VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE))
ON DUPLICATE KEY UPDATE token = VALUES(token), expiration_time = VALUES(expiration_time)");
$stmt->execute([$encryptedPhone, $encryptedOtp]);
} elseif ($user_type === 'service') {
$stmtDel = $con->prepare("DELETE FROM `phone_verification_service` WHERE `phone_number` = ?");
$stmtDel->execute([$encryptedPhone]);
$stmtIns = $con->prepare("
INSERT INTO `phone_verification_service`
(`phone_number`, `token_code`, `expiration_time`, `is_verified`, `created_at`)
VALUES (?, ?, ?, 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$encryptedOtp,
$expirationTime
]);
} elseif ($user_type === 'driver') {
if ($context === 'token_change') {
// Delete old verification attempts
$stmtDel = $con->prepare("DELETE FROM `token_verification_driver` WHERE `phone_number` = ?");
$stmtDel->execute([$encryptedPhone]);
// Insert new attempt
$stmtIns = $con->prepare("
INSERT INTO `token_verification_driver`
(`phone_number`, `token`, `expiration_time`, `verified`, `created_at`)
VALUES (?, ?, ?, 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$encryptedOtp,
$expirationTime
]);
} else {
// Delete old verification attempts
$stmtDel = $con->prepare("DELETE FROM `phone_verification` WHERE `phone_number` = ?");
$stmtDel->execute([$encryptedPhone]);
// Insert new attempt
$stmtIns = $con->prepare("
INSERT INTO `phone_verification`
(`phone_number`, `driverId`, `email`, `token_code`, `expiration_time`, `is_verified`, `created_at`)
VALUES (?, ?, ?, ?, ?, 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$driverId ?: '',
$encryptedEmail,
$encryptedOtp,
$expirationTime
]);
}
} else {
if ($context === 'token_change') {
// Delete old verification attempts
$stmtDel = $con->prepare("DELETE FROM `token_verification` WHERE `phone_number` = ?");
$stmtDel->execute([$encryptedPhone]);
// Insert new attempt
$stmtIns = $con->prepare("
INSERT INTO `token_verification`
(`phone_number`, `token`, `expiration_time`, `verified`, `created_at`)
VALUES (?, ?, ?, 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$encryptedOtp,
$expirationTime
]);
} else {
// Delete old verification attempts
$stmtDel = $con->prepare("DELETE FROM `phone_verification_passenger` WHERE `phone_number` = ?");
$stmtDel->execute([$encryptedPhone]);
// Insert new attempt
$stmtIns = $con->prepare("
INSERT INTO `phone_verification_passenger`
(`phone_number`, `token`, `expiration_time`, `verified`, `created_at`)
VALUES (?, ?, ?, 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$encryptedOtp,
$expirationTime
]);
}
}
jsonSuccess(null, "OTP sent and saved successfully");
} catch (PDOException $e) {
error_log("⚠️ [OTP DB Save] Error: " . $e->getMessage());
jsonError("OTP sent but failed to save verification data");
}
} else {
jsonError("Failed to send verification code. Please try again.");
}