enforce(RateLimiter::identifier(), 'otp_verify'); // 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) // user_type is taken from request only (JWT not trusted without signature verification) 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 // 4. Verify based on user type try { $encryptedPhoneSearch = otpPhoneKey($phone_number); if ($user_type === 'admin') { $sql = "SELECT * FROM token_verification_admin WHERE expiration_time >= NOW() AND verified = 0 AND phone_number = ?"; $stmt = $con->prepare($sql); $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRow = null; foreach ($rows as $row) { $decryptedToken = $encryptionHelper->decryptData($row['token']); if ($decryptedToken === $token_code) { $matchedRow = $row; break; } } if ($matchedRow) { $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"); // Mark token as verified $updateToken = $con->prepare("UPDATE token_verification_admin SET verified = 1 WHERE phone_number = ? AND token = ?"); $updateToken->execute([$matchedRow['phone_number'], $matchedRow['token']]); 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`, `phone_number`, `token_code` FROM `phone_verification_service` WHERE `expiration_time` > NOW() AND `is_verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { $decryptedToken = $encryptionHelper->decryptData($row['token_code']); if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } } if ($matchedRowId) { $sqlUpdate = "UPDATE `phone_verification_service` SET `is_verified` = 1 WHERE `id` = :id"; $stmtUpd = $con->prepare($sqlUpdate); $stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT); $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`, `phone_number`, `token` FROM `token_verification_driver` WHERE `expiration_time` > NOW() AND `verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { $decryptedToken = $encryptionHelper->decryptData($row['token']); if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } } if ($matchedRowId) { $sqlUpdate = "UPDATE `token_verification_driver` SET `verified` = 1 WHERE `id` = :id"; $stmtUpd = $con->prepare($sqlUpdate); $stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT); $stmtUpd->execute(); // ═══════════════════════════════════════════════════════ // تغيير الجهاز: التحقق وحده لا يكفي. لا بد من ربط بصمة // الجهاز الجديد ثم إصدار توكن driver — وإلا بقي التطبيق // على توكن التسجيل ورجع كل endpoint محمي 403، وهو ما كان // يجعل شاشة OTP "تنجح" ثم لا يعمل شيء بعدها. // ═══════════════════════════════════════════════════════ $chk = $con->prepare("SELECT id FROM driver WHERE phone = ? OR (? IS NOT NULL AND phone_bidx = ?)"); global $blindIndex; $phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone_number) : null; $chk->execute([$encryptionHelper->encryptData($phone_number), $phoneBidx, $phoneBidx]); $driverRow = $chk->fetch(PDO::FETCH_ASSOC); if (!$driverRow) { jsonSuccess(null, "Your phone number has been verified."); exit; } $driverID = (string)$driverRow['id']; $fpRaw = filterRequest("fingerprint") ?? filterRequest("fingerPrint") ?? ($_SERVER['HTTP_X_DEVICE_FP'] ?? null); if ($fpRaw === null && function_exists('getallheaders')) { $hdrs = array_change_key_case(getallheaders(), CASE_LOWER); $fpRaw = $hdrs['x-device-fp'] ?? null; } if (empty($fpRaw)) { jsonError("Device fingerprint is required to complete device change."); exit; } $fpPepper = getenv('FP_PEPPER') ?: ''; $fpHashed = !empty($fpPepper) ? hash('sha256', $fpRaw . $fpPepper) : $fpRaw; $fcmToken = filterRequest("token"); // ربط الجهاز الجديد — نفس الصيغة التي يتوقعها loginJwtDriver. $hasRow = $con->prepare("SELECT id FROM driverToken WHERE captain_id = ?"); $hasRow->execute([$driverID]); if ($hasRow->fetch(PDO::FETCH_ASSOC)) { if (!empty($fcmToken)) { $con->prepare("UPDATE driverToken SET fingerPrint = ?, token = ? WHERE captain_id = ?") ->execute([$fpHashed, $encryptionHelper->encryptData($fcmToken), $driverID]); } else { $con->prepare("UPDATE driverToken SET fingerPrint = ? WHERE captain_id = ?") ->execute([$fpHashed, $driverID]); } } else { $con->prepare("INSERT INTO driverToken (token, fingerPrint, captain_id, created_at) VALUES (?, ?, ?, NOW())") ->execute([ !empty($fcmToken) ? $encryptionHelper->encryptData($fcmToken) : '', $fpHashed, $driverID, ]); } $audDriver = filterRequest("aud") ?: filterRequest("audience") ?: (getenv('allowedDriver2') ?: 'driver-app:ios'); $jwtSvc = new JwtService($redis); $accessToken = $jwtSvc->generateAccessToken($driverID, 'driver', $audDriver, $fpRaw); jsonSuccess([ "driverID" => $driverID, "jwt" => $accessToken, "access_token" => $accessToken, ], "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`, `phone_number`, `token_code` FROM `phone_verification` WHERE `expiration_time` > NOW() AND `is_verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { $decryptedToken = $encryptionHelper->decryptData($row['token_code']); if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } } if ($matchedRowId) { $sqlUpdate = "UPDATE `phone_verification` SET `is_verified` = 1 WHERE `id` = :id"; $stmtUpd = $con->prepare($sqlUpdate); $stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT); $stmtUpd->execute(); // Check registration status $isRegistered = false; $driverData = null; $chkStmt = $con->prepare("SELECT id, first_name, last_name, email, phone FROM driver WHERE phone = ? OR (? IS NOT NULL AND phone_bidx = ?)"); global $blindIndex; $phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone_number) : null; $chkStmt->execute([$encryptionHelper->encryptData($phone_number), $phoneBidx, $phoneBidx]); $driver = $chkStmt->fetch(PDO::FETCH_ASSOC); // Generate driverID for unregistered users (hash of phone) $driverID = ''; if ($driver) { $isRegistered = true; $driver['first_name'] = $encryptionHelper->decryptData($driver['first_name']); $driver['last_name'] = $encryptionHelper->decryptData($driver['last_name']); $driver['email'] = $encryptionHelper->decryptData($driver['email']); $driver['phone'] = $encryptionHelper->decryptData($driver['phone']); $driverData = $driver; $driverID = (string)$driver['id']; } else { // driverID ثابت ومشتق من رقم الهاتف (نفس الرقم = نفس الـ ID) $driverID = substr(md5($phone_number), 0, 16); } // Generate JWT tokens for driver $audDriver = filterRequest("aud") ?: filterRequest("audience") ?: (getenv('allowedDriver2') ?: 'driver-app:ios'); $fpDriver = filterRequest("fingerprint") ?? filterRequest("fingerPrint") ?? ($_SERVER['HTTP_X_DEVICE_FP'] ?? null); if ($fpDriver === null && function_exists('getallheaders')) { $hdrs = array_change_key_case(getallheaders(), CASE_LOWER); $fpDriver = $hdrs['x-device-fp'] ?? null; } $jwtSvc = new JwtService($redis); $accessToken = $jwtSvc->generateAccessToken($driverID, 'driver', $audDriver, $fpDriver); $refreshToken = $jwtSvc->generateRefreshToken($driverID, 'driver', $audDriver); jsonSuccess([ "isRegistered" => $isRegistered, "driver" => $driverData, "driverID" => $driverID, "jwt" => $accessToken, "token" => $accessToken, "access_token" => $accessToken, "refresh_token" => $refreshToken ], "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`, `phone_number`, `token` FROM `token_verification` WHERE `expiration_time` > NOW() AND `verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { $decryptedToken = $encryptionHelper->decryptData($row['token']); if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } } if ($matchedRowId) { $sqlUpdate = "UPDATE `token_verification` SET `verified` = 1 WHERE `id` = :id"; $stmtUpd = $con->prepare($sqlUpdate); $stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT); $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`, `phone_number`, `token` FROM `phone_verification_passenger` WHERE `expiration_time` > NOW() AND `verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { $decryptedToken = $encryptionHelper->decryptData($row['token']); if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } } if ($matchedRowId) { $sqlUpdate = "UPDATE `phone_verification_passenger` SET `verified` = 1 WHERE `id` = :id"; $stmtUpd = $con->prepare($sqlUpdate); $stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT); $stmtUpd->execute(); // Check registration status $isRegistered = false; $passengerData = null; $passengerID = ''; $chkStmt = $con->prepare("SELECT id, first_name, last_name, email, phone FROM passengers WHERE phone = ? OR (? IS NOT NULL AND phone_bidx = ?)"); global $blindIndex; $phoneBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone_number) : null; $chkStmt->execute([$encryptionHelper->encryptData($phone_number), $phoneBidx, $phoneBidx]); $passenger = $chkStmt->fetch(PDO::FETCH_ASSOC); if ($passenger) { $isRegistered = true; $passenger['first_name'] = $encryptionHelper->decryptData($passenger['first_name']); $passenger['last_name'] = $encryptionHelper->decryptData($passenger['last_name']); $passenger['email'] = $encryptionHelper->decryptData($passenger['email']); $passenger['phone'] = $encryptionHelper->decryptData($passenger['phone']); $passengerData = $passenger; $passengerID = (string)$passenger['id']; } else { $passengerID = substr(md5($phone_number), 0, 16); } // Generate JWT tokens for passenger $audPass = filterRequest("aud") ?: filterRequest("audience") ?: (getenv('allowed2') ?: 'passenger-app:ios'); $fpPass = filterRequest("fingerprint") ?? filterRequest("fingerPrint") ?? ($_SERVER['HTTP_X_DEVICE_FP'] ?? null); if ($fpPass === null && function_exists('getallheaders')) { $hdrs = array_change_key_case(getallheaders(), CASE_LOWER); $fpPass = $hdrs['x-device-fp'] ?? null; } $jwtSvc = new JwtService($redis); $accessToken = $jwtSvc->generateAccessToken($passengerID, 'passenger', $audPass, $fpPass); $refreshToken = $jwtSvc->generateRefreshToken($passengerID, 'passenger', $audPass); jsonSuccess([ "isRegistered" => $isRegistered, "passenger" => $passengerData, "jwt" => $accessToken, "token" => $accessToken, "access_token" => $accessToken, "refresh_token" => $refreshToken ], "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."); }