Update: 2026-07-10 03:04:06

This commit is contained in:
Hamza-Ayed
2026-07-10 03:04:06 +03:00
parent 2ff7450d0b
commit b18fd027b6
22 changed files with 2661 additions and 96 deletions
+65
View File
@@ -0,0 +1,65 @@
<?php
// ride_server/intaleq/connect.php
// Bootstrap for standalone REST endpoints under ride_server/intaleq/.
// Modeled on loction_server/siro/connect.php (the JWT-enforcing variant).
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/load_env.php';
// .env path mirrors ride_server/passenger_socket.php's own already-deployed
// convention on this box — confirm/adjust with ops at deploy time.
$env_file = file_exists(__DIR__ . '/../ride-keys/.env')
? __DIR__ . '/../ride-keys/.env'
: (file_exists('/home/intaleq-rides/env/.env') ? '/home/intaleq-rides/env/.env' : '');
if (!empty($env_file)) {
loadEnvironment($env_file);
}
require_once __DIR__ . '/functions.php';
// --- CORS + JSON headers ---
header("Access-Control-Allow-Origin: https://intaleqapp.com");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header('Content-Type: application/json');
date_default_timezone_set('Asia/Amman');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// --- JWT auth — enforced. This endpoint returns ride status tied to a
// specific passenger/driver, and backend's connect.php enforces JWT for the
// same call today; dropping auth here would be a regression. ---
$decodedToken = authenticateJWT();
$user_id = $decodedToken->sub ?? $decodedToken->user_id ?? null;
// --- DB connection: same physical database backend's Database::get('main')
// resolves to (confirmed distinct from Database::get('ride') — the getRideStatus
// endpoint this replaces has always read from 'main', so the fallback query
// below stays on 'main' too, for zero behavior change vs. today). Env var
// names match backend/core/Database/Database.php's 'main' entry exactly;
// falling back to loction_server's simpler names only if ops sets this box
// up differently. Actual values must be confirmed/provisioned by ops. ---
$dbname = getenv('DB_PRIMARY_NAME_V2') ?: getenv('dbname');
$dbhost = getenv('DB_PRIMARY_HOST_V2') ?: 'localhost';
$dbuser = getenv('DB_PRIMARY_USER_V2') ?: getenv('USER');
$dbpass = getenv('DB_PRIMARY_PASS_V2') ?: getenv('PASS');
try {
$dsn = "mysql:host=$dbhost;dbname=$dbname;charset=utf8mb4";
$con = new PDO($dsn, $dbuser, $dbpass, [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF8",
PDO::ATTR_TIMEOUT => 5,
]);
} catch (PDOException $e) {
error_log("[ride_server/connect] DB connection failed: " . $e->getMessage());
http_response_code(500);
echo json_encode(['status' => 'failure', 'message' => 'A database error occurred.']);
exit;
}
+95
View File
@@ -0,0 +1,95 @@
<?php
// ride_server/intaleq/functions.php
// Bootstrap helpers for standalone REST endpoints under ride_server/intaleq/.
// Mirrors the env/key conventions already deployed on this box for
// ride_server/passenger_socket.php (/home/intaleq-rides/...), not invented.
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
// Shared secret used to authenticate to loction_server's internal HTTP API —
// same mechanism ride_server/passenger_socket.php already uses on this box.
function getInternalSocketKey(): string {
$key = getenv('INTERNAL_SOCKET_KEY');
if ($key) return trim($key);
$path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/intaleq-rides/.internal_socket_key';
if (file_exists($path)) return trim((string) @file_get_contents($path));
return '';
}
// JWT signing secret — must resolve to the same value backend's
// core/Auth/JwtService.php signs tokens with, or every token fails here.
function getJwtSecret(): string {
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) {
return trim(file_get_contents($keyPath));
}
return getenv('JWT_SECRET_KEY') ?: '';
}
function authenticateJWT() {
$secretKey = getJwtSecret();
if (!$secretKey) {
error_log("[ride_server] JWT secret not configured.");
http_response_code(500);
echo json_encode(['status' => 'failure', 'message' => 'Internal server configuration error.']);
exit;
}
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = null;
if (preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
$token = $matches[1];
}
if (!$token) {
http_response_code(401);
echo json_encode(['status' => 'failure', 'message' => 'Authorization token required']);
exit;
}
try {
return JWT::decode($token, new Key($secretKey, 'HS256'));
} catch (ExpiredException $e) {
http_response_code(401);
echo json_encode(['status' => 'failure', 'message' => 'Token expired']);
exit;
} catch (SignatureInvalidException $e) {
http_response_code(401);
echo json_encode(['status' => 'failure', 'message' => 'Invalid token signature']);
exit;
} catch (BeforeValidException $e) {
http_response_code(401);
echo json_encode(['status' => 'failure', 'message' => 'Token not yet valid']);
exit;
} catch (Exception $e) {
http_response_code(401);
echo json_encode(['status' => 'failure', 'message' => 'Invalid token']);
exit;
}
// NOTE: signature/expiry only — does not replicate backend's
// core/Auth/JwtService::authenticate() JTI-blacklist (revoked-token)
// check or X-Device-FP verification. Flagged as a follow-up in the
// plan; not blocking for this read-only endpoint.
}
// Flutter's CRUD().get() always issues an HTTP POST under the hood
// (see siro_rider/lib/controller/functions/crud.dart _makeRequest/doPost) —
// so every field this endpoint reads comes through $_POST, never $_GET.
function filterRequest($requestname, $type = 'string') {
if (isset($_POST[$requestname]) && $_POST[$requestname] !== '') {
$value = trim($_POST[$requestname]);
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value);
if ($type === 'numeric') {
return filter_var($value, FILTER_VALIDATE_FLOAT) !== false ? $value : null;
}
return $value;
}
return null;
}
function printFailure($message = "none") {
echo json_encode(["status" => "failure", "message" => $message]);
}
+23
View File
@@ -0,0 +1,23 @@
<?php
function loadEnvironment($env_file) {
if (!file_exists($env_file)) {
error_log("❌ .env not found: $env_file");
return false;
}
$lines = file($env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line) || strpos($line, '#') === 0) continue;
$parts = explode('=', $line, 2);
if (count($parts) === 2) {
[$keyName, $value] = $parts;
$value = trim($value, "\"'");
putenv("$keyName=$value");
$_ENV[$keyName] = $value;
$_SERVER[$keyName] = $value;
}
}
return true;
}
@@ -0,0 +1,88 @@
<?php
// ride_server/intaleq/ride/rides/getRideStatus.php
// Serves https://rides.intaleq.xyz/intaleq/ride/rides/getRideStatus.php
// Same response contract as the legacy backend/ride/rides/getRideStatus.php:
// {"status":"success","data":"<ride status string>"}
//
// Reads Redis truth via loction_server's internal HTTP API first (same
// "ask location server, fall back to DB" shape as backend/ride/location/get.php);
// on any miss/timeout/error, falls back to a direct MySQL SELECT so behavior
// never regresses to "no answer."
require_once __DIR__ . '/../../connect.php';
$id = filterRequest("id");
$rideId = (int) $id;
if (empty($id) || $rideId <= 0) {
printFailure("Missing ride ID.");
exit;
}
// ── 1. Try Redis via the location server's internal HTTP API ──────────────
$status = null;
$locationServerUrl = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
$internalKey = getInternalSocketKey();
if ($internalKey) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $locationServerUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'action' => 'get_ride_state',
'ride_id' => $rideId,
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Foreground/synchronous read — the answer IS the response, so this is
// deliberately longer than the 200-500ms fire-and-forget write timeouts
// used elsewhere in this codebase.
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 500);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1500);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if (!$curlErr && $httpCode === 200 && $response) {
$json = json_decode($response, true);
if (is_array($json) && ($json['status'] ?? false) === true
&& !empty($json['data']['status'])) {
$status = $json['data']['status'];
}
} else {
error_log("[getRideStatus] location-server read miss/error for ride=$rideId: "
. ($curlErr ?: "HTTP $httpCode"));
}
} else {
error_log("[getRideStatus] internal key not configured — skipping Redis read, using DB fallback.");
}
// ── 2. Fallback: direct MySQL read (identical query to the legacy file) ───
if ($status === null) {
try {
$stmt = $con->prepare("SELECT `status` FROM `ride` WHERE `id` = :id");
$stmt->bindParam(':id', $rideId, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row && isset($row['status'])) {
$status = $row['status'];
}
} catch (PDOException $e) {
error_log("[getRideStatus] DB fallback error for ride=$rideId: " . $e->getMessage());
http_response_code(500);
echo json_encode(["status" => "failure", "message" => "An internal error occurred."]);
exit;
}
}
if ($status !== null) {
echo json_encode([
"status" => "success",
"data" => $status
]);
} else {
printFailure("Ride not found.");
}