261 lines
11 KiB
PHP
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.");
|
|
}
|