Update: 2026-06-12 20:40:40

This commit is contained in:
Hamza-Ayed
2026-06-12 20:40:40 +03:00
parent 305ae01d52
commit f907212c57
294 changed files with 3592 additions and 3581 deletions

View File

@@ -0,0 +1,229 @@
<?php
// File: backend/auth/otp/providers.php
// Encapsulates external OTP gateway API calls for Kazumi, Intaleq, and Nabeh.
/**
* Send SMS OTP via Kazumi SMS Gateway (Egypt)
*
* @param string $receiver Recipient phone number (e.g. +2010xxxxxxxx)
* @param string $otp 3-digit verification code
* @return bool True if OTP was sent successfully
*/
function sendKazumiSms(string $receiver, string $otp): bool {
$username = getenv('SMS_USERNAME');
$password = getenv('SMS_PASSWORD_EGYPT');
$sender = getenv('SMS_SENDER');
if (!$username || !$password || !$sender) {
error_log("⚠️ [Kazumi OTP] Missing credentials in environment variables.");
return false;
}
$message = "Siro app code is " . $otp;
$apiUrl = 'https://sms.kazumi.me/api/sms/send-sms';
$payload = [
'username' => $username,
'password' => $password,
'language' => 'e',
'sender' => $sender,
'receiver' => $receiver,
'message' => $message
];
$response = curlCall("POST", $apiUrl, json_encode($payload), [
"Content-Type: application/json"
]);
if ($response) {
$decoded = json_decode($response, true);
if (isset($decoded['message']) && $decoded['message'] === 'Success') {
return true;
}
error_log("❌ [Kazumi OTP] API returned failure response: " . $response);
}
return false;
}
/**
* Retrieve Nabeh JWT Bearer Token, caching it in Redis for 24 hours.
*
* @return string|null The Bearer token, or null on failure.
*/
function getNabehBearerToken(): ?string {
global $redis;
// 1. Try to read cached token from Redis (TTL 24 hours)
if ($redis) {
try {
$cachedToken = $redis->get('nabeh_bearer_token');
if ($cachedToken) {
return $cachedToken;
}
} catch (Exception $e) {
error_log("⚠️ [Nabeh Auth Redis] Error reading token: " . $e->getMessage());
}
}
// 2. Token not cached, authenticate via Nabeh Login API
$email = getenv('NABEH_EMAIL');
$password = getenv('NABEH_PASSWORD');
if (!$email || !$password) {
error_log("⚠️ [Nabeh Auth] Missing NABEH_EMAIL or NABEH_PASSWORD environment variables.");
return null;
}
$apiUrl = 'https://nabeh.intaleqapp.com/api/auth/login';
$payload = [
'email' => $email,
'password' => $password
];
$response = curlCall("POST", $apiUrl, json_encode($payload), [
'Content-Type: application/json'
]);
if ($response) {
$decoded = json_decode($response, true);
$token = $decoded['token'] ?? $decoded['message']['token'] ?? $decoded['jwt'] ?? $decoded['access_token'] ?? null;
if ($token) {
// Cache token in Redis for 24 hours (86400 seconds)
if ($redis) {
try {
$redis->setex('nabeh_bearer_token', 86400, $token);
} catch (Exception $e) {
error_log("⚠️ [Nabeh Auth Redis Cache Save] Error saving token: " . $e->getMessage());
}
}
return $token;
}
error_log("❌ [Nabeh Auth] Failed to extract token from login response: " . $response);
}
return null;
}
/**
* Send OTP via Nabeh JWT Auth Gateway (WhatsApp, Voice, etc.)
*
* @param string $receiver Recipient phone number
* @param string $otp 3-digit verification code
* @param string $method text | voice | image | whatsapp
* @return bool True if OTP was sent successfully
*/
function sendNabehOtp(string $receiver, string $otp, string $method = 'text'): bool {
$bearerToken = getNabehBearerToken();
if (!$bearerToken) {
error_log("⚠️ [Nabeh OTP] Failed to obtain dynamic JWT Bearer token.");
return false;
}
// Strip symbols for Nabeh endpoint
$phoneRaw = preg_replace('/\D+/', '', $receiver);
// Map method/type
$type = 'text';
if ($method === 'voice') {
$type = 'voice';
} elseif ($method === 'image') {
$type = 'image';
}
// elseif ($method === 'flash_call') {
// $type = 'flash_call';
// }
$apiUrl = 'https://nabeh.intaleqapp.com/api/otp/send';
$payload = [
'phone' => $phoneRaw,
'type' => $type,
'code' => $otp
];
$response = curlCall("POST", $apiUrl, json_encode($payload), [
'Content-Type: application/json',
"Authorization: Bearer $bearerToken"
]);
if ($response) {
$decoded = json_decode($response, true);
if ($decoded && ($decoded['success'] ?? false)) {
return true;
}
error_log("❌ [Nabeh OTP] API returned failure response: " . $response);
}
return false;
}
/**
* Send OTP via Intaleq Static OTP Gateway (using body app_key parameter)
*
* @param string $receiver Recipient phone number
* @param string $otp 3-digit verification code
* @param string $method whatsapp | sms | voice | flash_call
* @return bool True if OTP was sent successfully
*/
function sendIntaleqOtp(string $receiver, string $otp, string $method = 'whatsapp'): bool {
$appKey = getenv('NABEH_OTP_APP_KEY');
if (!$appKey) {
error_log("⚠️ [Intaleq OTP] Missing NABEH_OTP_APP_KEY in environment.");
return false;
}
// Normalize receiver to start with +
$phoneWithPlus = (strpos($receiver, '+') === 0) ? $receiver : '+' . $receiver;
$apiUrl = 'https://otp.intaleqapp.com/api/request-otp.php';
$payload = [
'phone' => $phoneWithPlus,
'app_key' => $appKey,
'device_type' => 'android',
'method' => $method,
'code' => $otp
];
$response = curlCall("POST", $apiUrl, json_encode($payload), [
'Content-Type: application/json'
]);
if ($response) {
$decoded = json_decode($response, true);
if ($decoded && ($decoded['success'] ?? false)) {
return true;
}
error_log("❌ [Intaleq OTP] API returned failure response: " . $response);
}
return false;
}
/**
* Generic cURL execution helper
*/
function curlCall(string $method, string $url, string $data, array $headers): ?string {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $data,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 5
]);
$response = curl_exec($ch);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($error) {
error_log("⚠️ [OTP cURL] Error calling $url: $error");
return null;
}
if ($httpCode !== 200) {
error_log("⚠️ [OTP cURL] Non-200 HTTP code $httpCode from $url. Response: $response");
}
return $response;
}

View File

@@ -0,0 +1,260 @@
<?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.");
}

222
backend/auth/otp/verify.php Normal file
View File

@@ -0,0 +1,222 @@
<?php
// File: backend/auth/otp/verify.php
// Unified OTP verification endpoint
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
// 0. Rate Limiting: 3 محاولات OTP كل 5 دقائق لكل IP
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'otp');
// 1. Fetch input parameters
$phone_number = filterRequest("phone_number");
if (empty($phone_number)) {
$phone_number = filterRequest("receiver");
}
$token_code = filterRequest("token_code");
if (empty($token_code)) {
$token_code = filterRequest("token");
}
$user_type = filterRequest("user_type");
$context = filterRequest("context"); // token_change | login (default)
$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'];
}
}
}
if (empty($phone_number)) {
jsonError("Phone number is required.");
exit;
}
if (empty($token_code)) {
jsonError("Verification token code is required.");
exit;
}
if (empty($user_type)) {
if (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;
}
// 2. Establish DB Connection
try {
$con = Database::get('main');
} catch (Exception $e) {
http_response_code(500);
exit(json_encode(['error' => 'Database connection failed']));
}
// 3. Encrypt data to query
$encryptedPhone = $encryptionHelper->encryptData($phone_number);
$encryptedToken = $encryptionHelper->encryptData($token_code);
// 4. Verify based on user type
try {
if ($user_type === 'admin') {
$sql = "SELECT * FROM token_verification_admin
WHERE phone_number = :phone AND token = :token
AND expiration_time >= NOW()";
$stmt = $con->prepare($sql);
$stmt->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmt->bindParam(':token', $encryptedToken, PDO::PARAM_STR);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$deviceNumber = filterRequest("device_number") ?? '';
// adminUser stores unencrypted phone
$checkAdmin = $con->prepare("SELECT * FROM adminUser WHERE name = ?");
$checkAdmin->execute([$phone_number]);
$now = date("Y-m-d H:i:s");
if ($checkAdmin->rowCount() > 0) {
$update = $con->prepare("UPDATE adminUser SET device_number = ?, updated_at = ? WHERE name = ?");
$update->execute([$deviceNumber, $now, $phone_number]);
jsonSuccess(["message" => "verified and updated existing admin"]);
} else {
$insert = $con->prepare("INSERT INTO adminUser (device_number, name, created_at, updated_at) VALUES (?, ?, ?, ?)");
$insert->execute([$deviceNumber, $phone_number, $now, $now]);
jsonSuccess(["message" => "verified and new admin created"]);
}
} else {
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
}
} elseif ($user_type === 'service') {
$sql = "SELECT `id` FROM `phone_verification_service`
WHERE `phone_number` = :phone AND `token_code` = :token
AND `expiration_time` > NOW()";
$stmt = $con->prepare($sql);
$stmt->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmt->bindParam(':token', $encryptedToken, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetch();
if ($result) {
$sqlUpdate = "UPDATE `phone_verification_service` SET `is_verified` = 1 WHERE `phone_number` = :phone";
$stmtUpd = $con->prepare($sqlUpdate);
$stmtUpd->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmtUpd->execute();
jsonSuccess(null, "Your phone number has been verified.");
} else {
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
}
} elseif ($user_type === 'driver') {
if ($context === 'token_change') {
$sql = "SELECT `id` FROM `token_verification_driver`
WHERE `phone_number` = :phone
AND `token` = :token
AND `expiration_time` > NOW()";
$stmt = $con->prepare($sql);
$stmt->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmt->bindParam(':token', $encryptedToken, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetch();
if ($result) {
// Update driver verified status
$sqlUpdate = "UPDATE `token_verification_driver` SET `verified` = 1 WHERE `phone_number` = :phone";
$stmtUpd = $con->prepare($sqlUpdate);
$stmtUpd->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmtUpd->execute();
jsonSuccess(null, "Your phone number has been verified.");
} else {
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
}
} else {
$sql = "SELECT `id` FROM `phone_verification`
WHERE `phone_number` = :phone
AND `token_code` = :token
AND `expiration_time` > NOW()";
$stmt = $con->prepare($sql);
$stmt->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmt->bindParam(':token', $encryptedToken, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetch();
if ($result) {
// Update driver is_verified status
$sqlUpdate = "UPDATE `phone_verification` SET `is_verified` = 1 WHERE `phone_number` = :phone";
$stmtUpd = $con->prepare($sqlUpdate);
$stmtUpd->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmtUpd->execute();
jsonSuccess(null, "Your phone number has been verified.");
} else {
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
}
}
} else {
if ($context === 'token_change') {
$sql = "SELECT `id` FROM `token_verification`
WHERE `phone_number` = :phone
AND `token` = :token
AND `expiration_time` > NOW()";
$stmt = $con->prepare($sql);
$stmt->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmt->bindParam(':token', $encryptedToken, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetch();
if ($result) {
// Update passenger verified status
$sqlUpdate = "UPDATE `token_verification` SET `verified` = 1 WHERE `phone_number` = :phone";
$stmtUpd = $con->prepare($sqlUpdate);
$stmtUpd->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmtUpd->execute();
jsonSuccess(null, "Your phone number has been verified.");
} else {
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
}
} else {
$sql = "SELECT `id` FROM `phone_verification_passenger`
WHERE `phone_number` = :phone
AND `token` = :token
AND `expiration_time` > NOW()";
$stmt = $con->prepare($sql);
$stmt->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmt->bindParam(':token', $encryptedToken, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetch();
if ($result) {
// Update passenger verified status
$sqlUpdate = "UPDATE `phone_verification_passenger` SET `verified` = 1 WHERE `phone_number` = :phone";
$stmtUpd = $con->prepare($sqlUpdate);
$stmtUpd->bindParam(':phone', $encryptedPhone, PDO::PARAM_STR);
$stmtUpd->execute();
jsonSuccess(null, "Your phone number has been verified.");
} else {
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
}
}
}
} catch (PDOException $e) {
error_log("⚠️ [OTP DB Verify] Error: " . $e->getMessage());
jsonError("An error occurred during verification. Please try again.");
}