Files
Siro/backend/auth/otp/verify.php
T

387 lines
19 KiB
PHP

<?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_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.");
}